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

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 (50) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/docutils.js +8 -2
  3. package/index.html +14 -2
  4. package/index.js +162 -75
  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 +129 -0
  12. package/src/RvfFeatureDetails/RvfSharedMobilityDetail/StationDetails/StationDetails.tsx +24 -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 +107 -26
  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 +50 -18
  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 +30 -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/getLayersAsFlatArray.ts +22 -0
  46. package/src/utils/getLinkByDevice.ts +28 -0
  47. package/src/utils/getPermalinkParameters.ts +27 -0
  48. package/src/utils/hooks/useInitialLayersVisiblity.tsx +28 -0
  49. package/src/utils/hooks/useMapContext.tsx +2 -0
  50. package/src/utils/hooks/useUpdatePermalink.tsx +44 -11
@@ -9,3 +9,33 @@ export const RVF_EXTENT_3857 = transformExtent(
9
9
  );
10
10
 
11
11
  export const LAYER_PROP_IS_EXPORTING = "isExporting";
12
+
13
+ export const RVF_LAYERS_NAMES = {
14
+ auto: "auto",
15
+ cargobike: "cargobike",
16
+ echtzeit: "echtzeit",
17
+ eroller: "e-roller",
18
+ fahrrad: "fahrrad",
19
+ haltestellen: "haltestellen",
20
+ liniennetz: "liniennetz",
21
+ mitfahrpunkte: "mitfahrpunkte",
22
+ pois: "pois",
23
+ sharedMobility: "shared mobility",
24
+ tarifzonen: "tarifzonen",
25
+ verkaufsstellen: "verkaufsstellen",
26
+ };
27
+
28
+ export const API_REQUEST_FEATURE_TYPE = {
29
+ freeFloat: {
30
+ bike: "sharing_vehicles",
31
+ car: "sharing_vehicles",
32
+ cargoBike: "sharing_vehicles",
33
+ scooter: "sharing_vehicles",
34
+ },
35
+ stations: {
36
+ bike: "sharing_stations_bicycle",
37
+ car: "sharing_stations_car",
38
+ cargoBike: "sharing_stations_cargo_bicycle",
39
+ scooter: "sharing_stations_scooters_standing",
40
+ },
41
+ };
@@ -4,26 +4,38 @@ 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 { Cluster, Vector } from "ol/source";
7
- import { Circle, Fill, Icon, Stroke, Style, Text } from "ol/style";
7
+ import { Circle, Fill, 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 cargoBicycle 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";
17
9
  import { LAYER_PROP_IS_EXPORTING } from "./constants";
10
+ import { iconStyleByFormFactor } from "./createMobiDataBwWfsLayer";
18
11
 
19
- const iconByFormFactor = {
20
- bicycle,
21
- car,
22
- cargo_bicycle: cargoBicycle,
23
- scooter,
12
+ export const getCircleStyle = (color: string) => {
13
+ return new Style({
14
+ image: new Circle({
15
+ declutterMode: "declutter",
16
+ displacement: [-12, 12],
17
+ fill: new Fill({
18
+ color: "white",
19
+ }),
20
+ radius: 9,
21
+ stroke: new Stroke({
22
+ color: color,
23
+ width: 1,
24
+ }),
25
+ }),
26
+ text: new Text({
27
+ declutterMode: "declutter",
28
+ fill: new Fill({
29
+ color: color,
30
+ }),
31
+ font: "bolder 10px arial",
32
+ offsetX: -12,
33
+ offsetY: -11,
34
+ }),
35
+ });
24
36
  };
25
37
 
26
- function createSharedMobilityLayer(
38
+ function createFreeFloatMobilityLayer(
27
39
  name: string,
28
40
  color: string,
29
41
  formFactor: string,
@@ -31,6 +43,8 @@ function createSharedMobilityLayer(
31
43
  minZoom: 17.99,
32
44
  },
33
45
  ): VectorLayer<Vector<Feature<Point>>> {
46
+ // Exemple how to get a specific form factor
47
+ //api.mobidata-bw.de/geoserver/MobiData-BW/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=MobiData-BW%3Asharing_vehicles&CQL_FILTER=form_factor%20%3D%20%27scooter%27&maxFeatures=20000&outputFormat=csv
34
48
  const source = new Vector({
35
49
  format: new GeoJSON(),
36
50
  loader: function (extent, resolution, projection, success, failure) {
@@ -88,54 +102,7 @@ function createSharedMobilityLayer(
88
102
  strategy: bboxStrategy,
89
103
  });
90
104
 
91
- const style = new Style({
92
- image: new Circle({
93
- declutterMode: "declutter",
94
- displacement: [-12, 10],
95
- fill: new Fill({
96
- color: "white",
97
- }),
98
- radius: 10,
99
- stroke: new Stroke({
100
- color: color,
101
- width: 2,
102
- }),
103
- }),
104
- text: new Text({
105
- declutterMode: "declutter",
106
- fill: new Fill({
107
- color: color,
108
- }),
109
- font: "bold 12px inherit",
110
- offsetX: -12,
111
- offsetY: -10,
112
- }),
113
- });
114
-
115
- const iconStyleByFormFactor = {};
116
- Object.entries(iconByFormFactor).forEach(([key, value]) => {
117
- iconStyleByFormFactor[key] = new Style({
118
- image: new Icon({
119
- src: value || iconByFormFactor["scooter"],
120
- width: 25,
121
- }),
122
- });
123
- });
124
-
125
- // const backgroundStyle = new Style({
126
- // image: new Circle({
127
- // declutterMode: "declutter",
128
- // displacement: [0, 0],
129
- // fill: new Fill({
130
- // color: "#00973c",
131
- // }),
132
- // radius: 12,
133
- // }),
134
- // stroke: new Stroke({
135
- // color: "white",
136
- // width: 2,
137
- // }),
138
- // });
105
+ const style = getCircleStyle(color);
139
106
 
140
107
  const clusterSource = new Cluster({
141
108
  distance: 40,
@@ -146,21 +113,26 @@ function createSharedMobilityLayer(
146
113
  if (layer.get(LAYER_PROP_IS_EXPORTING)) {
147
114
  return null;
148
115
  }
149
- const clone = style.clone();
116
+ const circleStyle = style.clone();
150
117
 
151
- clone.getText().setText(feature.get("features").length);
152
- clone.setZIndex(parseInt(getUid(feature), 10));
118
+ const featuresCount = feature.get("features").length;
119
+ circleStyle.getText().setText(featuresCount);
153
120
 
154
121
  const formFactor =
155
122
  feature.get("features")?.[0].get?.("form_factor") || "scooter";
156
123
 
157
- return [clone, iconStyleByFormFactor[formFactor]];
124
+ const styles = [iconStyleByFormFactor[formFactor], circleStyle];
125
+
126
+ styles.forEach((style) => {
127
+ style.setZIndex(parseInt(getUid(feature), 10));
128
+ });
129
+
130
+ return styles;
158
131
  };
159
132
 
160
133
  const layer = new VectorLayer({
161
134
  // @ts-expect-error - custom properties
162
135
  isQueryable: true,
163
- name,
164
136
  source: clusterSource,
165
137
  style: styleFunction,
166
138
  ...layerOptions,
@@ -173,4 +145,4 @@ function createSharedMobilityLayer(
173
145
  return layer;
174
146
  }
175
147
 
176
- export default createSharedMobilityLayer;
148
+ export default createFreeFloatMobilityLayer;
@@ -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,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,28 @@
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
+ navigator.userAgent.includes("iPhone") ||
9
+ navigator.userAgent.includes("iPad") ||
10
+ navigator.userAgent.includes("iPod")
11
+ ) {
12
+ if (iosLink) {
13
+ return iosLink;
14
+ } else {
15
+ return webLink;
16
+ }
17
+ } else if (navigator.userAgent.includes("Android")) {
18
+ if (androidLink) {
19
+ return androidLink;
20
+ } else {
21
+ return webLink;
22
+ }
23
+ }
24
+
25
+ return webLink;
26
+ }
27
+
28
+ 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;