@geops/rvf-mobility-web-component 0.1.31 → 0.1.33

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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@geops/rvf-mobility-web-component",
3
3
  "license": "UNLICENSED",
4
4
  "description": "Web components for rvf in the domains of mobility and logistics.",
5
- "version": "0.1.31",
5
+ "version": "0.1.33",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
package/src/Map/Map.tsx CHANGED
@@ -18,6 +18,9 @@ function Map({ children, ...props }: RealtimeMapProps) {
18
18
 
19
19
  const view = useMemo(() => {
20
20
  if (!maxextent) {
21
+ if (maxextent === "") {
22
+ return new View();
23
+ }
21
24
  return;
22
25
  }
23
26
  const bbox = maxextent.split(",").map((c) => {
@@ -74,6 +77,7 @@ function Map({ children, ...props }: RealtimeMapProps) {
74
77
  const bbox = extent.split(",").map((c) => {
75
78
  return parseFloat(c);
76
79
  });
80
+
77
81
  if (bbox) {
78
82
  map.getView().fit(bbox);
79
83
  }
@@ -1,3 +1,4 @@
1
+ import { FeatureCollection } from "geojson";
1
2
  import { MaplibreStyleLayer } from "mobility-toolbox-js/ol";
2
3
  import { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreStyleLayer";
3
4
  import { useEffect, useMemo, useState } from "preact/hooks";
@@ -6,6 +7,7 @@ import useMapContext from "../utils/hooks/useMapContext";
6
7
  import useZoom from "../utils/hooks/useZoom";
7
8
  import {
8
9
  addNotificationsLayers,
10
+ getCurrentGraph,
9
11
  getNotificationsWithStatus,
10
12
  parsePreviewNotification,
11
13
  } from "./notificationUtils";
@@ -16,28 +18,18 @@ interface Metadata {
16
18
  graphs?: Graphs;
17
19
  }
18
20
 
19
- const useNotifications = () => {
20
- const {
21
- baselayer,
22
- baseLayer,
23
- notificationat,
24
- notificationbeforelayerid,
25
- notificationurl,
26
- } = useMapContext();
27
- const zoom = useZoom();
28
- const [notifications, setNotifications] = useState([]);
29
- const [previewNotification, setPreviewNotification] = useState(null);
30
- const [shouldAddPreviewNotifications, setShouldAddPreviewNotifications] =
31
- useState<boolean>(true);
21
+ const useStyleName = (): string => {
22
+ const { baselayer } = useMapContext();
23
+ return baselayer;
24
+ };
32
25
 
33
- const [style, setStyle] = useState<string>();
26
+ const useStyleMetadata = (): Metadata => {
27
+ const { baseLayer } = useMapContext();
34
28
  const [styleMetadata, setStyleMetadata] = useState<Metadata>();
35
-
36
29
  useEffect(() => {
37
30
  if (!baseLayer) {
38
31
  return;
39
32
  }
40
- setStyle(baselayer);
41
33
  if (!baseLayer.loaded) {
42
34
  // @ts-expect-error bad type definition
43
35
  baseLayer.once("load", () => {
@@ -46,24 +38,36 @@ const useNotifications = () => {
46
38
  } else {
47
39
  setStyleMetadata(baseLayer.mapLibreMap?.getStyle()?.metadata);
48
40
  }
49
- }, [baseLayer, baselayer]);
41
+ }, [baseLayer]);
42
+ return styleMetadata;
43
+ };
44
+
45
+ const useGraphMapping = (
46
+ defaultMapping: Record<number, string> = { 1: "osm" },
47
+ ): Record<number, string> => {
48
+ const styleMetadata = useStyleMetadata();
49
+ const graphMapping = useMemo(() => {
50
+ return styleMetadata?.graphs || defaultMapping;
51
+ }, [defaultMapping, styleMetadata?.graphs]);
52
+ return graphMapping;
53
+ };
54
+
55
+ const useNotifications = (): FeatureCollection => {
56
+ const { notificationat, notificationurl } = useMapContext();
57
+ const [notifications, setNotifications] = useState([]);
58
+ const [previewNotification, setPreviewNotification] = useState(null);
59
+ const [shouldAddPreviewNotifications, setShouldAddPreviewNotifications] =
60
+ useState<boolean>(true);
61
+
62
+ const styleName = useStyleName();
63
+ const graphMapping = useGraphMapping();
50
64
 
51
65
  const now = useMemo(() => {
52
66
  return notificationat ? new Date(notificationat) : new Date();
53
67
  }, [notificationat]);
54
68
 
55
- const graphMapping = useMemo(() => {
56
- return styleMetadata?.graphs || { 1: "osm" };
57
- }, [styleMetadata]);
58
-
59
69
  const graphsString = useMemo(() => {
60
- return [
61
- ...new Set(
62
- Object.keys(graphMapping || []).map((key) => {
63
- return graphMapping[key];
64
- }),
65
- ),
66
- ].join(",");
70
+ return [...new Set(Object.values(graphMapping))].join(",");
67
71
  }, [graphMapping]);
68
72
 
69
73
  useEffect(() => {
@@ -105,12 +109,12 @@ const useNotifications = () => {
105
109
  useEffect(() => {
106
110
  // Merge notifications with the previewNotification
107
111
  const newNotifications = [...notifications];
108
- if (shouldAddPreviewNotifications && previewNotification?.[style]) {
109
- const parsedPreviewNotification = parsePreviewNotification(
110
- previewNotification?.[style],
111
- );
112
+ const notifByStyle = previewNotification?.[styleName];
113
+
114
+ if (shouldAddPreviewNotifications && notifByStyle) {
115
+ const parsedPreviewNotification = parsePreviewNotification(notifByStyle);
112
116
  const index = newNotifications.findIndex((n) => {
113
- return n.properties.id === previewNotification[style].id;
117
+ return n.properties.id === notifByStyle.id;
114
118
  });
115
119
 
116
120
  if (index > -1) {
@@ -126,42 +130,47 @@ const useNotifications = () => {
126
130
  previewNotification,
127
131
  notifications,
128
132
  shouldAddPreviewNotifications,
129
- style,
133
+ styleName,
130
134
  now,
131
135
  ]);
132
136
 
133
- useEffect(() => {
134
- // Add the notifications to the map
135
- if (styleMetadata && notifications?.length) {
136
- addNotificationsLayers(
137
- baseLayer,
138
- notifications,
139
- notificationbeforelayerid,
140
- zoom,
141
- graphMapping,
142
- );
137
+ if (previewNotification?.id) {
138
+ const index = notifications.findIndex((n) => {
139
+ return n.properties.id === previewNotification.id;
140
+ });
141
+ if (index > -1) {
142
+ notifications[index].isPreview = true;
143
+ notifications[index].features.forEach((feature) => {
144
+ feature.properties.isPreview = true;
145
+ });
143
146
  }
144
- }, [
145
- notifications,
146
- notificationbeforelayerid,
147
- styleMetadata,
148
- zoom,
149
- graphMapping,
150
- baseLayer,
151
- ]);
152
-
153
- return notifications;
147
+ }
148
+
149
+ const features =
150
+ notifications
151
+ ?.map((notification) => {
152
+ return notification.features;
153
+ })
154
+ .flat() || [];
155
+
156
+ return {
157
+ features: features,
158
+ type: "FeatureCollection",
159
+ };
154
160
  };
155
161
 
156
162
  export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
157
- useNotifications();
158
- const { baseLayer, map } = useMapContext();
163
+ const notifications = useNotifications();
164
+ const { baseLayer, map, notificationbeforelayerid } = useMapContext();
165
+ const graphMapping = useGraphMapping();
166
+ const zoom = useZoom();
159
167
 
160
168
  const layer = useMemo(() => {
161
169
  if (!baseLayer) {
162
170
  return null;
163
171
  }
164
172
  return new MaplibreStyleLayer({
173
+ beforeId: notificationbeforelayerid,
165
174
  isQueryable: true,
166
175
  layersFilter: ({ metadata }) => {
167
176
  return metadata?.["general.filter"] === "notifications";
@@ -169,7 +178,25 @@ export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
169
178
  maplibreLayer: baseLayer,
170
179
  ...(props || {}),
171
180
  });
172
- }, [baseLayer, props]);
181
+ }, [baseLayer, notificationbeforelayerid, props]);
182
+
183
+ // Update data and style layers
184
+ useEffect(() => {
185
+ if (!baseLayer || !graphMapping) {
186
+ return null;
187
+ }
188
+ const graph = getCurrentGraph(graphMapping, zoom);
189
+ if (!graph) {
190
+ return null;
191
+ }
192
+ addNotificationsLayers(
193
+ baseLayer,
194
+ "notifications",
195
+ notifications,
196
+ notificationbeforelayerid,
197
+ graph,
198
+ );
199
+ }, [baseLayer, graphMapping, notificationbeforelayerid, notifications, zoom]);
173
200
 
174
201
  useEffect(() => {
175
202
  if (!map || !layer) {
@@ -2,6 +2,7 @@ import { FeatureCollection } from "geojson";
2
2
  import { Feature } from "ol";
3
3
  import { getCenter } from "ol/extent";
4
4
  import GeoJSON from "ol/format/GeoJSON";
5
+ import { toLonLat } from "ol/proj";
5
6
 
6
7
  import addSourceAndLayers from "../utils/addSourceAndLayers";
7
8
 
@@ -19,14 +20,32 @@ export const getTime = (str) => {
19
20
  */
20
21
  const getNotificationsWithStatus = (notifications, now) => {
21
22
  return notifications
22
- .filter((n) => {
23
+ .filter((notification) => {
23
24
  // TODO: The backend should be responsible to returns only good notifications.
24
- const notOutOfDate = n.properties.affected_time_intervals.some((ati) => {
25
- return now < new Date(ati.end);
26
- });
25
+ let notOutOfDate = notification.properties.affected_time_intervals.some(
26
+ (ati) => {
27
+ return now < new Date(ati.end);
28
+ },
29
+ );
30
+ if (!notOutOfDate) {
31
+ notOutOfDate = notification.properties.publications.some(
32
+ (publication) => {
33
+ return (
34
+ now >= new Date(publication.visible_from) &&
35
+ now <= new Date(publication.visible_until)
36
+ );
37
+ },
38
+ );
39
+ }
27
40
  return notOutOfDate;
28
41
  })
29
42
  .map((n) => {
43
+ const isPublished = n.properties.publications.some((publication) => {
44
+ return (
45
+ now >= new Date(publication.visible_from) &&
46
+ now <= new Date(publication.visible_until)
47
+ );
48
+ });
30
49
  const isActive = n.properties.affected_time_intervals.some((ati) => {
31
50
  const {
32
51
  end,
@@ -88,6 +107,7 @@ const getNotificationsWithStatus = (notifications, now) => {
88
107
  ...n.properties,
89
108
  iconRefPoint,
90
109
  isActive,
110
+ isPublished,
91
111
  starts,
92
112
  };
93
113
 
@@ -98,6 +118,22 @@ const getNotificationsWithStatus = (notifications, now) => {
98
118
  };
99
119
  });
100
120
 
121
+ if (iconRefPoint) {
122
+ features.push({
123
+ geometry: {
124
+ coordinates: toLonLat(iconRefPoint),
125
+ type: "Point",
126
+ },
127
+ id: Math.random() + "",
128
+ properties: {
129
+ ...properties,
130
+ disruption_type: "whatever",
131
+ isIconRefPoint: true,
132
+ },
133
+ type: "Feature",
134
+ });
135
+ }
136
+
101
137
  return {
102
138
  ...n,
103
139
  features,
@@ -106,7 +142,7 @@ const getNotificationsWithStatus = (notifications, now) => {
106
142
  });
107
143
  };
108
144
 
109
- const getCurrentGraph = (mapping: object, zoom: number) => {
145
+ export const getCurrentGraph = (mapping: object, zoom: number) => {
110
146
  const breakPoints = Object.keys(mapping).map((k) => {
111
147
  return parseFloat(k);
112
148
  });
@@ -116,41 +152,76 @@ const getCurrentGraph = (mapping: object, zoom: number) => {
116
152
  return mapping[closest || Math.min(...breakPoints)];
117
153
  };
118
154
 
155
+ // export const getDisruptionActiveStyleLayers = (
156
+ // id = "notificationActive",
157
+ // graph,
158
+ // ) => {
159
+ // return [
160
+ // {
161
+ // filter: [
162
+ // "all",
163
+ // ["==", ["get", "isActive"], true],
164
+ // ["==", ["get", "graph"], graph],
165
+ // ["==", ["get", "disruption_type"], "DISRUPTION"],
166
+ // ],
167
+ // id: id,
168
+ // layout: { visibility: "visible" },
169
+ // metadata: {
170
+ // "general.filter": "notifications",
171
+ // },
172
+ // paint: {
173
+ // "line-color": "rgba(255,0,0,1)",
174
+ // "line-dasharray": [2, 2],
175
+ // "line-width": 5,
176
+ // },
177
+ // source: "notifications",
178
+ // type: "line",
179
+ // },
180
+ // ];
181
+ // };
182
+
119
183
  /**
120
184
  * This function add layers in the mapbox style to show notifications lines.
121
185
  */
122
186
  const addNotificationsLayers = (
123
187
  mapboxLayer: object,
124
- notifications: FeatureCollection[],
188
+ sourceId: string,
189
+ sourceData: FeatureCollection,
125
190
  beforeLayerId: string,
126
- zoom: number,
127
- graphMapping: object,
191
+ graph: number,
128
192
  ) => {
129
193
  if (!mapboxLayer) {
130
194
  return;
131
195
  }
132
- const features = notifications
133
- .map((n) => {
134
- return n.features;
135
- })
136
- .flat();
196
+
137
197
  addSourceAndLayers(
138
198
  mapboxLayer,
139
- "notifications",
140
- {
141
- data: {
142
- features,
143
- type: "FeatureCollection",
144
- },
145
- generateId: true,
146
- type: "geojson",
147
- },
199
+ sourceId,
200
+ sourceData,
148
201
  [
202
+ // {
203
+ // filter: [
204
+ // "all",
205
+ // ["==", ["get", "isPreview"], true],
206
+ // ["==", ["get", "disruption_type"], "DISRUPTION"],
207
+ // ],
208
+ // id: "notificationsPreviewDisruption",
209
+ // layout: { visibility: "visible" },
210
+ // metadata: {
211
+ // "general.filter": "notifications",
212
+ // },
213
+ // paint: {
214
+ // "line-color": "rgba(255,255,0,0.2)",
215
+ // "line-width": 10,
216
+ // },
217
+ // source: "notifications",
218
+ // type: "line",
219
+ // },
149
220
  {
150
221
  filter: [
151
222
  "all",
152
223
  ["==", ["get", "isActive"], true],
153
- ["==", ["get", "graph"], getCurrentGraph(graphMapping, zoom)],
224
+ ["==", ["get", "graph"], graph],
154
225
  ["==", ["get", "disruption_type"], "DISRUPTION"],
155
226
  ],
156
227
  id: "notificationsActive",
@@ -166,6 +237,103 @@ const addNotificationsLayers = (
166
237
  source: "notifications",
167
238
  type: "line",
168
239
  },
240
+ // {
241
+ // filter: [
242
+ // "all",
243
+ // ["==", ["get", "isPreview"], true],
244
+ // // ["==", ["get", "graph"], graph],
245
+ // ["==", ["get", "disruption_type"], "DEVIATION"],
246
+ // ],
247
+ // id: "notificationsPreviewDeviation",
248
+ // layout: { visibility: "visible" },
249
+ // metadata: {
250
+ // "general.filter": "notifications",
251
+ // },
252
+ // paint: {
253
+ // "line-color": "rgba(255,255,0,0.2)",
254
+ // "line-width": 10,
255
+ // },
256
+ // source: "notifications",
257
+ // type: "line",
258
+ // },
259
+ {
260
+ filter: [
261
+ "all",
262
+ ["==", ["get", "isActive"], true],
263
+ ["==", ["get", "disruption_type"], "DEVIATION"],
264
+ ],
265
+ id: "notificationsActiveDeviation",
266
+ layout: { visibility: "visible" },
267
+ paint: {
268
+ "line-color": "#000000",
269
+ "line-dasharray": [2, 2],
270
+ "line-opacity": 0.5,
271
+ "line-width": 5,
272
+ },
273
+ source: "notifications",
274
+ type: "line",
275
+ },
276
+ {
277
+ filter: [
278
+ "all",
279
+ ["==", ["get", "isIconRefPoint"], true],
280
+ // ["==", ["get", "isActive"], true],
281
+ ],
282
+ id: "notificationsIconRefPointActive",
283
+ layout: { visibility: "visible" },
284
+ metadata: {
285
+ "general.filter": "notifications",
286
+ },
287
+ paint: {
288
+ "circle-color": "#ff0000",
289
+ "circle-radius": 10,
290
+ },
291
+ source: "notifications",
292
+ type: "circle",
293
+ },
294
+
295
+ {
296
+ filter: [
297
+ "all",
298
+ ["==", ["get", "isIconRefPoint"], true],
299
+ ["==", ["get", "isActive"], true],
300
+ ],
301
+ id: "notificationsIconRefPointActive",
302
+ layout: {
303
+ "icon-image": "warning",
304
+ "icon-size": 0.15,
305
+ visibility: "visible",
306
+ },
307
+ metadata: {
308
+ "general.filter": "notifications",
309
+ },
310
+ paint: {},
311
+ source: "notifications",
312
+ type: "symbol",
313
+ },
314
+ {
315
+ filter: [
316
+ "all",
317
+ ["==", ["get", "isIconRefPoint"], true],
318
+ ["==", ["get", "isActive"], false],
319
+ ["==", ["get", "isPublished"], true],
320
+ ],
321
+ id: "notificationsIconRefPointNonActive",
322
+ layout: {
323
+ "icon-image": "warningbanner",
324
+ "icon-size": 0.15,
325
+ "text-field": ["get", "starts"],
326
+ "text-offset": [1.5, 0],
327
+ "text-size": 8,
328
+ visibility: "visible",
329
+ },
330
+ metadata: {
331
+ "general.filter": "notifications",
332
+ },
333
+ paint: {},
334
+ source: "notifications",
335
+ type: "symbol",
336
+ },
169
337
  ],
170
338
  beforeLayerId,
171
339
  );
@@ -246,7 +246,7 @@ function RvfMobilityMap({
246
246
 
247
247
  // TODO: this should be removed. The parent application should be responsible to do this
248
248
  // or we should find something that fit more usecases
249
- useUpdatePermalink(map, permalink === "true");
249
+ useUpdatePermalink(map, permalink === "true", eventNodeRef);
250
250
 
251
251
  const mapContextValue = useMemo(() => {
252
252
  return {
@@ -518,7 +518,7 @@ function RvfMobilityMap({
518
518
  <MapContext.Provider value={mapContextValue}>
519
519
  <RvfContext.Provider value={rvfContextValue}>
520
520
  <div
521
- className="relative size-full overflow-hidden rounded-[16px] border font-sans @container/main"
521
+ className="relative size-full overflow-hidden font-sans @container/main"
522
522
  ref={eventNodeRef}
523
523
  style={styleProps}
524
524
  >
Binary file
Binary file
@@ -1,3 +1,11 @@
1
+ import warningbanner from "../icons/warning-banner.png";
2
+ import warning from "../icons/warning.png";
3
+
4
+ const icons = {
5
+ warning: warning,
6
+ warningbanner: warningbanner,
7
+ };
8
+
1
9
  // Add a source and styleLayer using the id in parameter.
2
10
  const addSourceAndLayers = (
3
11
  mapboxLayer,
@@ -20,13 +28,27 @@ const addSourceAndLayers = (
20
28
  }
21
29
  const { mbMap } = mapboxLayer;
22
30
 
31
+ Object.keys(icons).forEach((icon) => {
32
+ if (!mbMap.getImage(icon)) {
33
+ mbMap.loadImage(icons[icon]).then((image) => {
34
+ if (!mbMap.getImage(icon)) {
35
+ mbMap.addImage(icon, image.data);
36
+ }
37
+ });
38
+ }
39
+ });
40
+
23
41
  // Update source
24
42
  if (sourceId && sourceData) {
25
43
  const source = mbMap.getSource(sourceId);
26
44
  if (source) {
27
- source.setData(sourceData.data);
45
+ source.setData(sourceData);
28
46
  } else {
29
- mbMap.addSource(sourceId, sourceData);
47
+ mbMap.addSource(sourceId, {
48
+ data: sourceData,
49
+ generateId: true,
50
+ type: "geojson",
51
+ });
30
52
  }
31
53
  }
32
54
 
@@ -2,17 +2,22 @@ import debounce from "lodash.debounce";
2
2
  import { Map } from "ol";
3
3
  import { EventsKey } from "ol/events";
4
4
  import { unByKey } from "ol/Observable";
5
- import { useEffect } from "preact/hooks";
5
+ import { MutableRef, useEffect } from "preact/hooks";
6
6
 
7
7
  import { LAYER_PROP_IS_EXPORTING } from "../constants";
8
8
  import getLayersAsFlatArray from "../getLayersAsFlatArray";
9
9
  import getPermalinkParameters from "../getPermalinkParameters";
10
+ import MobilityEvent from "../MobilityEvent";
10
11
 
11
12
  /**
12
13
  * This hook only update parameters in the url, it does not apply the url parameters.
13
14
  */
14
15
 
15
- const useUpdatePermalink = (map: Map, permalink: boolean) => {
16
+ const useUpdatePermalink = (
17
+ map: Map,
18
+ permalink: boolean,
19
+ eventNodeRef: MutableRef<HTMLDivElement>,
20
+ ) => {
16
21
  useEffect(() => {
17
22
  let moveEndKey: EventsKey;
18
23
  let loadEndKey: EventsKey;
@@ -23,17 +28,17 @@ const useUpdatePermalink = (map: Map, permalink: boolean) => {
23
28
 
24
29
  // Update x,y,z in URL on moveend
25
30
  moveEndKey = map?.on("moveend", (evt) => {
26
- updatePermalinkDebounced(evt.map);
31
+ updatePermalinkDebounced(evt.map, eventNodeRef);
27
32
  });
28
33
 
29
34
  // Update layers in URL on change:visible event
30
35
  loadEndKey = map.once("loadend", (evt) => {
31
- updatePermalinkDebounced(evt.map);
36
+ updatePermalinkDebounced(evt.map, eventNodeRef);
32
37
  changeVisibleKeys = getLayersAsFlatArray(
33
38
  evt.map.getLayers().getArray(),
34
39
  ).map((layer) => {
35
40
  return layer.on("change:visible", () => {
36
- updatePermalinkDebounced(evt.map);
41
+ updatePermalinkDebounced(evt.map, eventNodeRef);
37
42
  });
38
43
  });
39
44
  });
@@ -44,11 +49,14 @@ const useUpdatePermalink = (map: Map, permalink: boolean) => {
44
49
  unByKey(loadEndKey);
45
50
  unByKey(changeVisibleKeys);
46
51
  };
47
- }, [map, permalink]);
52
+ }, [map, permalink, eventNodeRef]);
48
53
  return null;
49
54
  };
50
55
 
51
- const updatePermalink = (map: Map) => {
56
+ const updatePermalink = (
57
+ map: Map,
58
+ eventNodeRef: MutableRef<HTMLDivElement>,
59
+ ) => {
52
60
  // No update when exporting
53
61
  if (map.get(LAYER_PROP_IS_EXPORTING)) {
54
62
  return;
@@ -57,6 +65,9 @@ const updatePermalink = (map: Map) => {
57
65
  const urlParams = getPermalinkParameters(map, currentUrlParams);
58
66
  urlParams.set("permalink", "true");
59
67
  window.history.replaceState(null, null, `?${urlParams.toString()}`);
68
+ eventNodeRef?.current?.dispatchEvent(
69
+ new MobilityEvent<string>("mwc:permalink", window.location.href),
70
+ );
60
71
  };
61
72
 
62
73
  const updatePermalinkDebounced = debounce(updatePermalink, 1000);
@@ -15,15 +15,27 @@ const useZoom = () => {
15
15
  if (view) {
16
16
  setZoom(view.getZoom());
17
17
  }
18
- const zoomListener = view.on("change:resolution", () => {
18
+ const onZoomChange = (evt) => {
19
19
  clearTimeout(timeout);
20
20
  timeout = setTimeout(() => {
21
- return setZoom(view.getZoom());
21
+ return setZoom(evt.target.getZoom());
22
22
  }, 150);
23
+ };
24
+ let zoomListener = view.on("change:resolution", onZoomChange);
25
+
26
+ const key = map.on("change:view", () => {
27
+ const view = map.getView();
28
+ unByKey(zoomListener);
29
+ if (view) {
30
+ setZoom(view.getZoom());
31
+ }
32
+ zoomListener = view.on("change:resolution", onZoomChange);
23
33
  });
34
+
24
35
  return () => {
25
36
  clearTimeout(timeout);
26
37
  unByKey(zoomListener);
38
+ unByKey(key);
27
39
  };
28
40
  }, [map]);
29
41
  return zoom;