@geops/rvf-mobility-web-component 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docutils.js +8 -2
  3. package/index.html +14 -2
  4. package/index.js +152 -78
  5. package/package.json +2 -1
  6. package/src/RealtimeLayer/RealtimeLayer.tsx +2 -0
  7. package/src/RvfExportMenu/RvfExportMenu.tsx +12 -1
  8. package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +25 -4
  9. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/FloatingVehiclesDetails/FloatingVehiclesDetails.tsx +53 -0
  10. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/FloatingVehiclesDetails/index.tsx +1 -0
  11. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/RvfSharedMobilityDetails.tsx +123 -0
  12. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/StationDetails/StationDetails.tsx +32 -0
  13. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/StationDetails/index.tsx +1 -0
  14. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/index.tsx +1 -0
  15. package/src/RvfLineNetworkPlanLayer/RvfLineNetworkPlanLayer.tsx +2 -0
  16. package/src/RvfMobilityMap/RvfMobilityMap.tsx +123 -34
  17. package/src/RvfOverlayHeader/RvfOverlayHeader.tsx +1 -1
  18. package/src/RvfPoisLayer/RvfPoisLayer.tsx +2 -0
  19. package/src/RvfSelectedFeatureHighlightLayer/RvfSelectedFeatureHighlightLayer.tsx +64 -0
  20. package/src/RvfSelectedFeatureHighlightLayer/index.tsx +1 -0
  21. package/src/RvfSellingPointsLayer/RvfSellingPointsLayer.tsx +2 -0
  22. package/src/RvfShare/RvfShare.tsx +1 -1
  23. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +59 -23
  24. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +2 -10
  25. package/src/RvfTarifZonenLayer/RvfTarifZonenLayer.tsx +2 -0
  26. package/src/RvfTopics/RvfTopics.tsx +40 -8
  27. package/src/StationsLayer/StationsLayer.tsx +2 -0
  28. package/src/icons/Bike/rvf_shared_bike.svg +2 -2
  29. package/src/icons/Car/rvf_shared_car.svg +3 -3
  30. package/src/icons/CargoBike/rvf_shared_cargo_bike.svg +3 -3
  31. package/src/icons/Scooter/rvf_shared_scooter.svg +2 -2
  32. package/src/index.tsx +4 -0
  33. package/src/logos/callabike_logo.png +0 -0
  34. package/src/logos/flinkster_logo.png +0 -0
  35. package/src/logos/gruene_flotte_logo.png +0 -0
  36. package/src/logos/logo_frelo_web_rgb.png +0 -0
  37. package/src/logos/natur_energie_logo.png +0 -0
  38. package/src/logos/yoio_logo.png +0 -0
  39. package/src/logos/zeus_logo.png +0 -0
  40. package/src/utils/applyInitialLayerVisibility.ts +41 -0
  41. package/src/utils/constants.ts +63 -0
  42. package/src/utils/{createSharedMobilityLayer.ts → createFreeFloatMobilityLayer.ts} +41 -69
  43. package/src/utils/createMobiDataBwWfsLayer.ts +108 -67
  44. package/src/utils/exportPdf.ts +1 -4
  45. package/src/utils/getFeatureInformationTitle.ts +37 -0
  46. package/src/utils/getLayersAsFlatArray.ts +22 -0
  47. package/src/utils/getLinkByDevice.ts +23 -0
  48. package/src/utils/getPermalinkParameters.ts +27 -0
  49. package/src/utils/hooks/useInitialLayersVisiblity.tsx +28 -0
  50. package/src/utils/hooks/useMapContext.tsx +2 -0
  51. package/src/utils/hooks/useUpdatePermalink.tsx +44 -11
@@ -4,15 +4,98 @@ import { Point } from "ol/geom";
4
4
  import VectorLayer, { Options } from "ol/layer/Vector";
5
5
  import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
6
6
  import { Vector } from "ol/source";
7
- import { Circle, Fill, Stroke, Style, Text } from "ol/style";
7
+ import { Circle, Fill, Icon, Stroke, Style, Text } from "ol/style";
8
8
 
9
+ // @ts-expect-error - load svg as data url
10
+ import bicycle from "../icons/Bike/rvf_shared_bike.svg";
11
+ // @ts-expect-error - load svg as data url
12
+ import car from "../icons/Car/rvf_shared_car.svg";
13
+ // @ts-expect-error - load svg as data url
14
+ import cargo_bicycle from "../icons/CargoBike/rvf_shared_cargo_bike.svg";
15
+ // @ts-expect-error - load svg as data url
16
+ import scooter from "../icons/Scooter/rvf_shared_scooter.svg";
9
17
  import { LAYER_PROP_IS_EXPORTING } from "./constants";
10
18
 
19
+ const iconByFormFactor = {
20
+ bicycle,
21
+ car,
22
+ cargo_bicycle,
23
+ scooter,
24
+ };
25
+
26
+ export const iconStyleByFormFactor = {};
27
+ Object.entries(iconByFormFactor).forEach(([key, value]) => {
28
+ iconStyleByFormFactor[key] = new Style({
29
+ image: new Icon({
30
+ src: value,
31
+ }),
32
+ });
33
+ });
34
+
35
+ export const getCircleStyle = (color: string) => {
36
+ return new Style({
37
+ image: new Circle({
38
+ displacement: [12, 12],
39
+ fill: new Fill({
40
+ color: "white",
41
+ }),
42
+ radius: 9,
43
+ stroke: new Stroke({
44
+ color: color,
45
+ width: 1,
46
+ }),
47
+ }),
48
+ text: new Text({
49
+ fill: new Fill({
50
+ color: color,
51
+ }),
52
+ font: "bolder 10px arial",
53
+ offsetX: 12,
54
+ offsetY: -11,
55
+ }),
56
+ });
57
+ };
58
+
59
+ const getCompanyNameTextStyle = (feedId: string) => {
60
+ const offsetY =
61
+ feedId === "naturenergie_sharing" || feedId === "gruene_flotte_freiburg"
62
+ ? 25
63
+ : 20;
64
+ return new Style({
65
+ text: new Text({
66
+ declutterMode: "declutter",
67
+ fill: new Fill({ color: "rgba(6, 76, 10, 1)" }),
68
+ font: "bold 12px/1.1 source-sans-pro",
69
+ offsetY,
70
+ stroke: new Stroke({ color: "white", width: 4 }),
71
+ text: companyTextByFeedId[feedId] || feedId,
72
+ // textBaseline: "middle",
73
+ }),
74
+ });
75
+ };
76
+
77
+ const companyTextByFeedId = {
78
+ callabike_ice: "Call a Bike",
79
+ flinkster_carsharing: "Flinkster",
80
+ gruene_flotte_freiburg: "Die Grüne\nFlotte",
81
+ lastenvelo_fr: "LastenVelo",
82
+ naturenergie_sharing: "naturenergie\nsharing",
83
+ nextbike_df: "Frelo",
84
+ // "nextbike",
85
+ // "EinfachMobil",
86
+ // "Donkey Republic"
87
+ // "PubliBike",
88
+ // "Voi",
89
+ // "Velospot",
90
+ // "carvelo",
91
+ // "CarSharing"
92
+ };
93
+
11
94
  function createMobiDataBwWfsLayer(
12
95
  name: string,
13
96
  color: string,
14
97
  layerOptions: Options = {
15
- minZoom: 17.99,
98
+ minZoom: 18,
16
99
  },
17
100
  ): VectorLayer<Vector<Feature<Point>>> {
18
101
  const source = new Vector({
@@ -36,86 +119,44 @@ function createMobiDataBwWfsLayer(
36
119
  },
37
120
  });
38
121
 
39
- const style = new Style({
40
- image: new Circle({
41
- declutterMode: "declutter",
42
- displacement: [11, 12],
43
- fill: new Fill({
44
- color: "white",
45
- }),
46
- radius: 10,
47
- stroke: new Stroke({
48
- color: color,
49
- width: 2,
50
- }),
51
- }),
52
- text: new Text({
53
- declutterMode: "declutter",
54
- fill: new Fill({
55
- color: color,
56
- }),
57
- font: "bold 12px inherit",
58
- offsetX: 11,
59
- offsetY: -12,
60
- // stroke: new Stroke({ color: "black", width: 1 }),
61
- // text: ["to-string", ["get", "num_vehicles_available"]],
62
- }),
63
- });
122
+ const style = getCircleStyle(color);
64
123
 
65
124
  const layer = new VectorLayer({
66
125
  // @ts-expect-error - custom properties
67
126
  isQueryable: true,
68
- name,
69
127
  // declutter: true,
70
128
  source: source,
71
129
  ...layerOptions,
72
- // style: (feature) => {
73
- // console.log("feature", feature);
74
- // style
75
- // .getText()
76
- // .setText(feature.get("num_vehicles_available").toString());
77
- // return style;
78
- // },
79
-
80
- // style: {
81
- // "circle-fill-color": "white",
82
- // "circle-radius": 15,
83
- // "circle-stroke-color": color,
84
- // "circle-stroke-width": 2,
85
- // // "fill-color": "rgba(100,100,100,0.25)",
86
- // // "stroke-color": "white",
87
- // // "stroke-width": 0.75,
88
- // // "text-background-fill-color": "black",
89
- // "text-fill-color": color,
90
- // "text-font": "bold 12px sans-serif",
91
- // "text-stroke-color": color,
92
- // "text-value": ["to-string", ["get", "num_vehicles_available"]],
93
- // },
94
130
  });
95
131
 
96
132
  const styleFunction = (feature: Feature) => {
97
133
  if (layer.get(LAYER_PROP_IS_EXPORTING)) {
98
134
  return null;
99
135
  }
100
- const clone = style.clone();
136
+ const circleStyle = style.clone();
101
137
 
102
- clone.getText().setText(feature.get("num_vehicles_available")?.toString());
103
- const isFreeFloat = !feature.get("num_vehicles_available")?.toString();
104
- if (isFreeFloat) {
105
- (clone.getImage() as Circle).setRadius(6);
106
- }
107
- clone.setZIndex(parseInt(getUid(feature), 10));
108
- return [
109
- clone,
110
- // new Style({
111
- // text: new Text({
112
- // fill: new Fill({ color: color }),
113
- // offsetY: 15,
114
- // stroke: new Stroke({ color: "white", width: 2 }),
115
- // text: feature.get("form_factor") || name,
116
- // }),
117
- // }),
138
+ const numVehiclesAvailable = feature
139
+ .get("num_vehicles_available")
140
+ ?.toString();
141
+ circleStyle.getText().setText(numVehiclesAvailable);
142
+
143
+ const formFactor = (feature.getId() as string)
144
+ .split(".")[0]
145
+ .replace("sharing_stations_", "");
146
+
147
+ const feedId = feature.get("feed_id").replace("-", "_"); // to handle id's like gruene-flotte_freiburg
148
+
149
+ const styles = [
150
+ iconStyleByFormFactor[formFactor],
151
+ getCompanyNameTextStyle(feedId),
152
+ circleStyle,
118
153
  ];
154
+
155
+ styles.forEach((style) => {
156
+ style.setZIndex(parseInt(getUid(feature), 10));
157
+ });
158
+
159
+ return styles;
119
160
  };
120
161
 
121
162
  source.on("addfeature", function (event) {
@@ -554,9 +554,7 @@ async function exportPdf(
554
554
  const size = sizePt.map((n) => {
555
555
  return (n * 96) / 72;
556
556
  });
557
- const extent = useMaxExtent
558
- ? RVF_EXTENT_3857
559
- : map.getView().calculateExtent();
557
+ const extent = useMaxExtent ? RVF_EXTENT_3857 : null; //map.getView().calculateExtent();
560
558
 
561
559
  // Save current pixel ratio
562
560
  const actualPixelRatio = window.devicePixelRatio;
@@ -602,7 +600,6 @@ async function exportPdf(
602
600
  const mapToExport = await createMapToExport(map, layers, extent, size, {
603
601
  pixelRatio: pixelRatio,
604
602
  });
605
-
606
603
  Object.defineProperty(window, "devicePixelRatio", {
607
604
  get() {
608
605
  return actualPixelRatio;
@@ -0,0 +1,37 @@
1
+ import { Feature } from "ol";
2
+
3
+ import {
4
+ RVF_LAYERS_TITLES,
5
+ TITLE_BY_CATEGORY,
6
+ TITLE_BY_FEED_ID,
7
+ } from "./constants";
8
+
9
+ const defaultTitle = "Informations";
10
+
11
+ const getFeatureInformationTitle = (feature: Feature) => {
12
+ const features = feature.get("features");
13
+ const selectedFeature = features?.[0] || feature;
14
+ const {
15
+ category,
16
+ feed_id: feedId,
17
+ long_name: longName,
18
+ tickets,
19
+ } = selectedFeature.getProperties();
20
+
21
+ if (category) {
22
+ return TITLE_BY_CATEGORY[category] || defaultTitle;
23
+ }
24
+ if (feedId) {
25
+ return TITLE_BY_FEED_ID[feedId] || defaultTitle;
26
+ }
27
+
28
+ if (longName) {
29
+ return RVF_LAYERS_TITLES.liniennetz;
30
+ }
31
+ if (tickets) {
32
+ return RVF_LAYERS_TITLES.verkaufsstellen;
33
+ }
34
+ return defaultTitle;
35
+ };
36
+
37
+ export default getFeatureInformationTitle;
@@ -0,0 +1,22 @@
1
+ import { Group } from "ol/layer";
2
+ import BaseLayer from "ol/layer/Base";
3
+
4
+ const getLayersAsFlatArray = (
5
+ layersOrLayer: BaseLayer | BaseLayer[],
6
+ ): BaseLayer[] => {
7
+ let layers = layersOrLayer;
8
+ if (!Array.isArray(layers)) {
9
+ layers = [layersOrLayer as BaseLayer];
10
+ }
11
+ let flatLayers: BaseLayer[] = [];
12
+ layers.forEach((layer: BaseLayer) => {
13
+ flatLayers.push(layer);
14
+ // Handle children property and ol.layer.Group
15
+ const children =
16
+ layer.get("children") || (layer as Group).getLayers?.()?.getArray();
17
+ flatLayers = flatLayers.concat(getLayersAsFlatArray(children || []));
18
+ });
19
+ return flatLayers;
20
+ };
21
+
22
+ export default getLayersAsFlatArray;
@@ -0,0 +1,23 @@
1
+ import { Feature } from "ol";
2
+
3
+ function getLinkByDevice(feature: Feature): string {
4
+ const iosLink = feature.get("rental_uris_ios");
5
+ const androidLink = feature.get("rental_uris_android");
6
+ const webLink = feature.get("rental_uris_web");
7
+ if (
8
+ iosLink &&
9
+ (navigator.userAgent.includes("iPhone") ||
10
+ navigator.userAgent.includes("iPad") ||
11
+ navigator.userAgent.includes("iPod"))
12
+ ) {
13
+ return iosLink;
14
+ }
15
+
16
+ if (androidLink && navigator.userAgent.includes("Android")) {
17
+ return androidLink;
18
+ }
19
+
20
+ return webLink;
21
+ }
22
+
23
+ export default getLinkByDevice;
@@ -0,0 +1,27 @@
1
+ import { Map } from "ol";
2
+
3
+ import getLayersAsFlatArray from "./getLayersAsFlatArray";
4
+
5
+ // This function return URL parameters representing a map.
6
+ const getPermalinkParameters = (
7
+ map: Map,
8
+ urlParams: URLSearchParams = new URLSearchParams(),
9
+ ): URLSearchParams => {
10
+ const z = map.getView().getZoom();
11
+ const [x, y] = map.getView().getCenter();
12
+ const layers = getLayersAsFlatArray(map.getLayers().getArray())
13
+ .filter((layer) => {
14
+ return layer.get("name") && layer.getVisible();
15
+ })
16
+ .map((layer) => {
17
+ return layer.get("name");
18
+ });
19
+
20
+ urlParams.set("layers", layers.join(","));
21
+ urlParams.set("x", x.toFixed(2));
22
+ urlParams.set("y", y.toFixed(2));
23
+ urlParams.set("z", z.toFixed(1));
24
+ return urlParams;
25
+ };
26
+
27
+ export default getPermalinkParameters;
@@ -0,0 +1,28 @@
1
+ import { Map } from "ol";
2
+ import { unByKey } from "ol/Observable";
3
+ import { useEffect } from "preact/hooks";
4
+
5
+ import applyInitialLayerVisibility from "../applyInitialLayerVisibility";
6
+ import getLayersAsFlatArray from "../getLayersAsFlatArray";
7
+
8
+ const useInitialLayersVisiblity = (map: Map, layers: string) => {
9
+ // Apply initial visibility of layers from layers attribute
10
+ useEffect(() => {
11
+ if (!map) {
12
+ return;
13
+ }
14
+ getLayersAsFlatArray(map.getLayers().getArray()).forEach((layer) => {
15
+ applyInitialLayerVisibility(layers, layer);
16
+ });
17
+
18
+ const key = map.getLayers().on("add", (event) => {
19
+ applyInitialLayerVisibility(layers, event.element);
20
+ });
21
+ return () => {
22
+ unByKey(key);
23
+ };
24
+ }, [map, layers]);
25
+ return null;
26
+ };
27
+
28
+ export default useInitialLayersVisiblity;
@@ -21,6 +21,7 @@ export type MapContextType = {
21
21
  baseLayer: MaplibreLayer;
22
22
  isFollowing: boolean;
23
23
  isTracking: boolean;
24
+ layers: string;
24
25
  map: Map;
25
26
  realtimeLayer: RealtimeLayer;
26
27
  setBaseLayer: (baseLayer: MaplibreLayer) => void;
@@ -44,6 +45,7 @@ export const MapContext = createContext<MapContextType>({
44
45
  baseLayer: null,
45
46
  isFollowing: false,
46
47
  isTracking: false,
48
+ layers: null,
47
49
  map: null,
48
50
  realtimeLayer: null,
49
51
  setBaseLayer: (baseLayer?: MaplibreLayer) => {},
@@ -1,26 +1,59 @@
1
+ import debounce from "lodash.debounce";
1
2
  import { Map } from "ol";
3
+ import { EventsKey } from "ol/events";
2
4
  import { unByKey } from "ol/Observable";
3
5
  import { useEffect } from "preact/hooks";
4
6
 
7
+ import getLayersAsFlatArray from "../getLayersAsFlatArray";
8
+ import getPermalinkParameters from "../getPermalinkParameters";
9
+
10
+ /**
11
+ * This hook only update parameters in the url, it does not apply the url parameters.
12
+ */
13
+
5
14
  const useUpdatePermalink = (map: Map, permalink: boolean) => {
6
15
  useEffect(() => {
7
- let listener;
16
+ let moveEndKey: EventsKey;
17
+ let loadEndKey: EventsKey;
18
+ let changeVisibleKeys: EventsKey[];
19
+
8
20
  if (map && permalink) {
9
- listener = map.on("moveend", () => {
10
- const urlParams = new URLSearchParams(window.location.search);
11
- const newX = map.getView().getCenter()[0].toFixed(2);
12
- const newY = map.getView().getCenter()[1].toFixed(2);
13
- const newZ = map.getView().getZoom().toFixed(1);
14
- urlParams.set("x", newX);
15
- urlParams.set("y", newY);
16
- urlParams.set("z", newZ);
17
- window.history.replaceState(null, null, `?${urlParams.toString()}`);
21
+ updatePermalinkDebounced(map);
22
+
23
+ // Update x,y,z in URL on moveend
24
+ moveEndKey = map?.on("moveend", (evt) => {
25
+ updatePermalinkDebounced(evt.map);
26
+ });
27
+
28
+ // Update layers in URL on change:visible event
29
+ loadEndKey = map.once("loadend", (evt) => {
30
+ updatePermalinkDebounced(evt.map);
31
+ changeVisibleKeys = getLayersAsFlatArray(
32
+ evt.map.getLayers().getArray(),
33
+ ).map((layer) => {
34
+ return layer.on("change:visible", () => {
35
+ updatePermalinkDebounced(evt.map);
36
+ });
37
+ });
18
38
  });
19
39
  }
40
+
20
41
  return () => {
21
- unByKey(listener);
42
+ unByKey(moveEndKey);
43
+ unByKey(loadEndKey);
44
+ unByKey(changeVisibleKeys);
22
45
  };
23
46
  }, [map, permalink]);
24
47
  return null;
25
48
  };
49
+
50
+ const updatePermalink = (map: Map) => {
51
+ const currentUrlParams = new URLSearchParams(window.location.search);
52
+ const urlParams = getPermalinkParameters(map, currentUrlParams);
53
+ urlParams.set("permalink", "true");
54
+ window.history.replaceState(null, null, `?${urlParams.toString()}`);
55
+ };
56
+
57
+ const updatePermalinkDebounced = debounce(updatePermalink, 1000);
58
+
26
59
  export default useUpdatePermalink;