@geops/rvf-mobility-web-component 0.1.74 → 0.1.76

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 (29) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +17 -55
  3. package/docutils.js +1 -1
  4. package/index.html +9 -1
  5. package/index.js +114 -110
  6. package/package.json +2 -2
  7. package/src/FeatureDetails/FeatureDetails.tsx +9 -18
  8. package/src/LayerTreeMenu/LayerTreeMenu.tsx +1 -1
  9. package/src/LayoutState/LayoutState.tsx +14 -1
  10. package/src/LinesNetworkPlanDetails/LinesNetworkPlanDetails.tsx +37 -68
  11. package/src/LinesNetworkPlanLayerHighlight/LinesNetworkPlanLayerHighlight.tsx +11 -3
  12. package/src/MobilityMap/MobilityMap.tsx +1 -5
  13. package/src/MobilityMap/MobilityMapAttributes.ts +9 -5
  14. package/src/MobilityNotifications/MobilityNotifications.tsx +1 -6
  15. package/src/NotificationsLayer/NotificationsLayer.tsx +1 -1
  16. package/src/OverlayContent/OverlayContent.tsx +1 -2
  17. package/src/OverlayDetails/OverlayDetails.tsx +23 -2
  18. package/src/OverlayDetailsHeader/OverlayDetailsHeader.tsx +3 -0
  19. package/src/OverlayHeader/OverlayHeader.tsx +1 -1
  20. package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +5 -4
  21. package/src/RvfFeatureDetails/RvfSellingPointDetails/RvfSellingPointDetails.tsx +1 -1
  22. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +95 -84
  23. package/src/SingleClickListener/SingleClickListener.tsx +1 -1
  24. package/src/utils/constants.ts +2 -1
  25. package/src/utils/hooks/useLayersConfig.tsx +3 -0
  26. package/src/utils/hooks/{useLnpLineInfo.tsx → useLnp.tsx} +24 -9
  27. package/src/utils/hooks/useMapContext.tsx +4 -0
  28. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +0 -238
  29. package/src/RvfSingleClickListener/index.tsx +0 -1
@@ -95,90 +95,6 @@ function RvfSharedMobilityLayerGroup(props: GroupOptions) {
95
95
  return baseLayer?.mapLibreMap;
96
96
  }, [baseLayer?.mapLibreMap]);
97
97
 
98
- const updateData = useCallback(() => {
99
- const abortController = new AbortController();
100
- const extent = transformExtent(
101
- map.getView().calculateExtent(),
102
- "EPSG:3857",
103
- "EPSG:4326",
104
- );
105
-
106
- const fetchData = async () => {
107
- // Fill stations data
108
- // The stations are also loaded in the style to have them displayed during export
109
- const stations = await fetchStations(
110
- WFS_STATIONS_TYPE,
111
- abortController,
112
- extent,
113
- );
114
- setStationsData(stations);
115
-
116
- // Fill free float data
117
- const freeFloatData = (await fetchSharingWFS(
118
- WFS_FREE_FLOAT_TYPE,
119
- abortController,
120
- extent,
121
- )) as FeatureCollection<Point, SharingVehicleWFS>;
122
-
123
- const bikes: FeatureCollection<Point, SharingVehicleWFS> = {
124
- features: [],
125
- type: "FeatureCollection",
126
- };
127
- const cargoBikes: FeatureCollection<Point, SharingVehicleWFS> = {
128
- features: [],
129
- type: "FeatureCollection",
130
- };
131
- const cars: FeatureCollection<Point, SharingVehicleWFS> = {
132
- features: [],
133
- type: "FeatureCollection",
134
- };
135
- const scooter: FeatureCollection<Point, SharingVehicleWFS> = {
136
- features: [],
137
- type: "FeatureCollection",
138
- };
139
-
140
- freeFloatData.features.forEach((feature) => {
141
- if (feature.properties.form_factor === BIKE_FORM_FACTOR) {
142
- bikes.features.push(feature);
143
- } else if (feature.properties.form_factor === CARGOBIKE_FORM_FACTOR) {
144
- cargoBikes.features.push(feature);
145
- } else if (feature.properties.form_factor === CAR_FORM_FACTOR) {
146
- cars.features.push(feature);
147
- } else if (feature.properties.form_factor === SCOOTER_FORM_FACTOR) {
148
- scooter.features.push(feature);
149
- }
150
- });
151
-
152
- setBikeFreeFloatData(bikes);
153
- setCargoBikeFreeFloatData(cargoBikes);
154
- setCarFreeFloatData(cars);
155
- setScooterFreeFloatData(scooter);
156
- };
157
-
158
- void fetchData();
159
-
160
- return () => {
161
- abortController.abort();
162
- };
163
- }, [map]);
164
-
165
- // Request all stations and vehicleson each moveend event
166
- useEffect(() => {
167
- const key = map?.on("moveend", updateData);
168
-
169
- // @ts-expect-error - change property can have custom values
170
- const key2 = map?.on(`change:${LAYER_PROP_IS_EXPORTING}`, (evt) => {
171
- // Reupdate the data after finishing eporting the map
172
- if (!evt.target.get(LAYER_PROP_IS_EXPORTING)) {
173
- updateData();
174
- }
175
- });
176
- const key3 = map?.once("rendercomplete", updateData);
177
- return () => {
178
- unByKey([key, key2, key3]);
179
- };
180
- }, [map, updateData, mbMap]);
181
-
182
98
  useEffect(() => {
183
99
  if (!mbMap?.style || !stationsData) {
184
100
  return;
@@ -402,6 +318,101 @@ function RvfSharedMobilityLayerGroup(props: GroupOptions) {
402
318
  } as GroupOptions);
403
319
  }, [baseLayer, props, t]);
404
320
 
321
+ const updateData = useCallback(() => {
322
+ // Only update when layers are visible
323
+ if (map?.getView()?.getZoom() < 15.5 || !group.getVisible()) {
324
+ return;
325
+ }
326
+ const abortController = new AbortController();
327
+ const extent = transformExtent(
328
+ map.getView().calculateExtent(),
329
+ "EPSG:3857",
330
+ "EPSG:4326",
331
+ );
332
+
333
+ const fetchData = async () => {
334
+ // Fill stations data
335
+ // The stations are also loaded in the style to have them displayed during export
336
+ const stations = await fetchStations(
337
+ WFS_STATIONS_TYPE,
338
+ abortController,
339
+ extent,
340
+ );
341
+ setStationsData(stations);
342
+
343
+ // Fill free float data
344
+ const freeFloatData = (await fetchSharingWFS(
345
+ WFS_FREE_FLOAT_TYPE,
346
+ abortController,
347
+ extent,
348
+ )) as FeatureCollection<Point, SharingVehicleWFS>;
349
+
350
+ const bikes: FeatureCollection<Point, SharingVehicleWFS> = {
351
+ features: [],
352
+ type: "FeatureCollection",
353
+ };
354
+ const cargoBikes: FeatureCollection<Point, SharingVehicleWFS> = {
355
+ features: [],
356
+ type: "FeatureCollection",
357
+ };
358
+ const cars: FeatureCollection<Point, SharingVehicleWFS> = {
359
+ features: [],
360
+ type: "FeatureCollection",
361
+ };
362
+ const scooter: FeatureCollection<Point, SharingVehicleWFS> = {
363
+ features: [],
364
+ type: "FeatureCollection",
365
+ };
366
+
367
+ freeFloatData.features.forEach((feature) => {
368
+ if (feature.properties.form_factor === BIKE_FORM_FACTOR) {
369
+ bikes.features.push(feature);
370
+ } else if (feature.properties.form_factor === CARGOBIKE_FORM_FACTOR) {
371
+ cargoBikes.features.push(feature);
372
+ } else if (feature.properties.form_factor === CAR_FORM_FACTOR) {
373
+ cars.features.push(feature);
374
+ } else if (feature.properties.form_factor === SCOOTER_FORM_FACTOR) {
375
+ scooter.features.push(feature);
376
+ }
377
+ });
378
+
379
+ setBikeFreeFloatData(bikes);
380
+ setCargoBikeFreeFloatData(cargoBikes);
381
+ setCarFreeFloatData(cars);
382
+ setScooterFreeFloatData(scooter);
383
+ };
384
+
385
+ void fetchData();
386
+
387
+ return () => {
388
+ abortController.abort();
389
+ };
390
+ }, [map, group]);
391
+
392
+ // Request all stations and vehicleson each moveend event
393
+ useEffect(() => {
394
+ const key = map?.on("moveend", () => {
395
+ return updateData();
396
+ });
397
+
398
+ // @ts-expect-error - change property can have custom values
399
+ const key2 = map?.on(`change:${LAYER_PROP_IS_EXPORTING}`, (evt) => {
400
+ // Reupdate the data after finishing eporting the map
401
+ if (!evt.target.get(LAYER_PROP_IS_EXPORTING)) {
402
+ updateData();
403
+ }
404
+ });
405
+ const key3 = map?.once("rendercomplete", updateData);
406
+ const key4 = group?.on("change:visible", (evt) => {
407
+ if (evt.target.getVisible()) {
408
+ updateData();
409
+ }
410
+ });
411
+ return () => {
412
+ unByKey([key, key2, key3, key4]);
413
+ };
414
+ }, [map, updateData, mbMap, group]);
415
+
405
416
  // Reload features every minute
406
417
  useEffect(() => {
407
418
  const interval = window.setInterval(() => {
@@ -17,7 +17,7 @@ export interface SingleClickListenerProps {
17
17
 
18
18
  function SingleClickListener({
19
19
  debounceOptions,
20
- debounceTimeout = 0,
20
+ debounceTimeout = 150,
21
21
  hover = true,
22
22
  }: SingleClickListenerProps) {
23
23
  const {
@@ -18,9 +18,10 @@ export const LAYER_NAME_LINESNETWORKPLAN = "liniennetz";
18
18
  export const LAYER_NAME_MAPSET = "mapset";
19
19
 
20
20
  export const RVF_EXTENT_4326 = [7.5, 47.7, 8.45, 48.4];
21
+ export const MAX_EXTENT_4326 = RVF_EXTENT_4326;
21
22
 
22
23
  export const MAX_EXTENT = transformExtent(
23
- RVF_EXTENT_4326,
24
+ MAX_EXTENT_4326,
24
25
  "EPSG:4326",
25
26
  "EPSG:3857",
26
27
  );
@@ -5,6 +5,9 @@ import { LAYERS_NAMES } from "../constants";
5
5
  import useMapContext from "./useMapContext";
6
6
 
7
7
  export interface LayerConfig {
8
+ featurelink?: {
9
+ href?: string;
10
+ };
8
11
  link?: {
9
12
  href?: string;
10
13
  show?: boolean;
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo } from "preact/hooks";
1
+ import { useEffect, useState } from "preact/hooks";
2
2
 
3
3
  import { LNP_MD_LINES, LNP_MD_STOPS, LNP_SOURCE_ID } from "../constants";
4
4
 
@@ -36,15 +36,30 @@ let cacheLnpSourceInfo: {
36
36
 
37
37
  export function useLnpSourceInfos() {
38
38
  const { baseLayer } = useMapContext();
39
+ const [sourceUrl, setSourceUrl] = useState<string>(null);
39
40
 
40
- const sourceUrl = useMemo(() => {
41
- return baseLayer?.mapLibreMap?.getSource<VectorTileSource>(LNP_SOURCE_ID)
42
- ?.url;
43
- }, [baseLayer]);
41
+ useEffect(() => {
42
+ const url =
43
+ baseLayer?.mapLibreMap?.getSource<VectorTileSource>(LNP_SOURCE_ID)?.url;
44
+ const onSourceData = (e) => {
45
+ if (e.sourceId === LNP_SOURCE_ID && e.isSourceLoaded) {
46
+ setSourceUrl(e.source?.url);
47
+ baseLayer?.mapLibreMap?.off("sourcedata", onSourceData);
48
+ }
49
+ };
50
+ if (!url) {
51
+ baseLayer?.mapLibreMap?.on("sourcedata", onSourceData);
52
+ } else {
53
+ setSourceUrl(url);
54
+ }
55
+ return () => {
56
+ baseLayer?.mapLibreMap?.off("sourcedata", onSourceData);
57
+ };
58
+ }, [baseLayer?.mapLibreMap]);
44
59
 
45
60
  useEffect(() => {
46
61
  const abortController = new AbortController();
47
- const fetchInfos = async (url) => {
62
+ const fetchInfos = async (url: string) => {
48
63
  if (!cacheLnpSourceInfo) {
49
64
  const response = await fetch(url, { signal: abortController.signal });
50
65
  const data = await response.json();
@@ -73,13 +88,13 @@ export function useLnpStopsInfos(): StopsInfos {
73
88
  }
74
89
 
75
90
  /**
76
- * This hook search lines informations from lnp data. It takes a string in
91
+ * This hook search line informations from lnp data. It takes a string in
77
92
  * parameter then it will search if there is a property that exactly match this value.
78
93
  */
79
94
  function useLnpLineInfo(text: string): LineInfo {
80
95
  const linesInfos = useLnpLinesInfos();
81
96
 
82
- if (!linesInfos) {
97
+ if (!linesInfos || !text) {
83
98
  return null;
84
99
  }
85
100
 
@@ -89,7 +104,7 @@ function useLnpLineInfo(text: string): LineInfo {
89
104
 
90
105
  return Object.values(linesInfos).find((info) => {
91
106
  return ["id", "external_id", "short_name", "long_name"].find((key) => {
92
- return info[key] === text;
107
+ return !!info[key] && info[key] === text;
93
108
  });
94
109
  });
95
110
  }
@@ -50,6 +50,7 @@ export type MapContextType = {
50
50
  linesNetworkPlanLayer: MaplibreStyleLayer;
51
51
  map: Map;
52
52
  mapsetLayer?: MapsetLayer;
53
+ notificationId?: string;
53
54
  notificationsLayer?: MocoLayer;
54
55
  permalinkUrlSearchParams: URLSearchParams;
55
56
  previewNotifications?: SituationType[];
@@ -86,6 +87,7 @@ export type MapContextType = {
86
87
  setLinesNetworkPlanLayer: (layer?: MaplibreStyleLayer) => void;
87
88
  setMap: (map?: Map) => void;
88
89
  setMapsetLayer: (mapsetLayer?: MapsetLayer) => void;
90
+ setNotificationId: (notificationId?: string) => void;
89
91
  setNotificationsLayer: (notificationsLayer?: MocoLayer) => void;
90
92
  setPermalinkUrlSearchParams: (
91
93
  setPermalinkUrlSearchParams: URLSearchParams,
@@ -132,9 +134,11 @@ export const MapContext = createContext<MapContextType>({
132
134
  setBaseLayer: (baseLayer?: MaplibreLayer) => {},
133
135
  setIsFollowing: (isFollowing: boolean) => {},
134
136
  setIsTracking: (isTracking: boolean) => {},
137
+ setLinesIds: (linesIds: string[]) => {},
135
138
  setLinesNetworkPlanLayer: (linesNetworkPlanLayer: MaplibreStyleLayer) => {},
136
139
  setMap: (map?: Map) => {},
137
140
  setMapsetLayer: (mapsetLayer?: MapsetLayer) => {},
141
+ setNotificationId: (notificationId?: string) => {},
138
142
  setNotificationsLayer: (notificationsLayer?: MocoLayer) => {},
139
143
  setPermalinkUrlSearchParams: (
140
144
  permalinkUrlSearchParams: URLSearchParams,
@@ -1,238 +0,0 @@
1
- import { getFeatureInfoAtCoordinate } from "mobility-toolbox-js/ol";
2
- import { GeoJSON } from "ol/format";
3
- import { unByKey } from "ol/Observable";
4
- import { useCallback, useEffect } from "preact/hooks";
5
-
6
- import {
7
- LAYER_NAME_REALTIME,
8
- LAYER_NAME_STATIONS,
9
- PROVIDER_LOGOS_BY_FEED_ID,
10
- } from "../utils/constants";
11
- import useMapContext from "../utils/hooks/useMapContext";
12
- import useRvfContext from "../utils/hooks/useRvfContext";
13
- import MobilityEvent from "../utils/MobilityEvent";
14
- import { fetchSharingStation } from "../utils/sharingGraphqlUtils";
15
-
16
- import type { GeoJSONSource } from "maplibre-gl";
17
- import type { Feature, MapBrowserEvent } from "ol";
18
-
19
- const geojson = new GeoJSON();
20
-
21
- function SingleClickListener({ eventNode }: { eventNode: HTMLElement }) {
22
- const {
23
- baseLayer,
24
- map,
25
- queryablelayers,
26
- realtimeLayer,
27
- setStationId,
28
- setTrainId,
29
- stationId,
30
- stationsLayer,
31
- tenant,
32
- trainId,
33
- } = useMapContext();
34
- const { selectedFeature, setSelectedFeature, setSelectedFeatures } =
35
- useRvfContext();
36
-
37
- // Send the selctedFEature to the parent window
38
- useEffect(() => {
39
- eventNode?.dispatchEvent(
40
- new MobilityEvent("mwc:selectedfeature", {
41
- feature: selectedFeature
42
- ? geojson.writeFeatureObject(selectedFeature)
43
- : null,
44
- }),
45
- );
46
- }, [eventNode, selectedFeature]);
47
-
48
- const onPointerMove = useCallback(
49
- (evt: MapBrowserEvent<PointerEvent>) => {
50
- const [realtimeFeature] = evt.map.getFeaturesAtPixel(evt.pixel, {
51
- hitTolerance: 5,
52
- layerFilter: (l) => {
53
- return l === realtimeLayer;
54
- },
55
- });
56
- realtimeLayer?.highlight(realtimeFeature as Feature);
57
-
58
- const stationsFeatures = evt.map.getFeaturesAtPixel(evt.pixel, {
59
- layerFilter: (l) => {
60
- return l === stationsLayer;
61
- },
62
- });
63
-
64
- const [stationFeature] = stationsFeatures.filter((feat) => {
65
- return feat.get("tralis_network")?.includes(tenant);
66
- });
67
-
68
- // Send all the features under the cursor
69
- const features = evt.map.getFeaturesAtPixel(evt.pixel, {
70
- layerFilter: (l) => {
71
- return queryablelayers
72
- ? queryablelayers.split(",").includes(l.get("name"))
73
- : l.get("isQueryable");
74
- },
75
- }) as Feature[];
76
-
77
- evt.map.getTargetElement().style.cursor =
78
- realtimeFeature || stationFeature || features?.length
79
- ? "pointer"
80
- : "default";
81
- },
82
- [queryablelayers, realtimeLayer, stationsLayer, tenant],
83
- );
84
-
85
- const onSingleClick = useCallback(
86
- async (evt: MapBrowserEvent<PointerEvent>) => {
87
- if (!baseLayer?.mapLibreMap) {
88
- return;
89
- }
90
- const [realtimeFeature] = evt.map.getFeaturesAtPixel(evt.pixel, {
91
- hitTolerance: 5,
92
- layerFilter: (l) => {
93
- return l === realtimeLayer;
94
- },
95
- }) as Feature[];
96
- realtimeFeature?.set("layerName", LAYER_NAME_REALTIME);
97
-
98
- const stationsFeatures = evt.map.getFeaturesAtPixel(evt.pixel, {
99
- layerFilter: (l) => {
100
- return l === stationsLayer;
101
- },
102
- });
103
- const [stationFeature] = stationsFeatures.filter((feat) => {
104
- return feat.get("tralis_network")?.includes(tenant);
105
- }) as Feature[];
106
- stationFeature?.set("layerName", LAYER_NAME_STATIONS);
107
-
108
- const newStationId = stationFeature?.get("uid");
109
-
110
- const newTrainId = realtimeFeature?.get("train_id");
111
-
112
- if (newStationId && stationId !== newStationId) {
113
- setStationId(newStationId);
114
- setTrainId(null);
115
- } else if (newTrainId && newTrainId !== trainId) {
116
- setTrainId(realtimeFeature.get("train_id"));
117
- setStationId(null);
118
- } else {
119
- setTrainId(null);
120
- setStationId(null);
121
- }
122
-
123
- // Send all the features under the cursor
124
- // const features = evt.map.getFeaturesAtPixel(evt.pixel, {
125
- // hitTolerance: 5,
126
- // layerFilter: (l) => {
127
- // console.log(queryablelayers);
128
- // return queryablelayers
129
- // ? queryablelayers.split(",").includes(l.get("name"))
130
- // : l.get("isQueryable");
131
- // },
132
- // }) as Feature[];
133
-
134
- const queryableLayers = evt.map.getAllLayers().filter((l) => {
135
- return queryablelayers
136
- ? queryablelayers.split(",").includes(l.get("name"))
137
- : l.get("isQueryable");
138
- });
139
- const featuresInfos = await getFeatureInfoAtCoordinate(
140
- evt.coordinate,
141
- queryableLayers,
142
- 5,
143
- true,
144
- );
145
-
146
- // Set the layerName as property of the feature
147
- featuresInfos.forEach((f) => {
148
- f.features.forEach((feat) => {
149
- feat.set("layerName", f.layer.get("name"));
150
- });
151
- });
152
-
153
- const features = featuresInfos.flatMap((f) => {
154
- return f.features;
155
- });
156
-
157
- // Append more infos about the features
158
- for (const feature of features) {
159
- const clusterId = feature.get("cluster_id");
160
- if (clusterId) {
161
- const vtFeat = feature.get("vectorTileFeature");
162
- const sourceId = vtFeat.layer.source;
163
- const leaves =
164
- (await baseLayer.mapLibreMap
165
- .getSource<GeoJSONSource>(sourceId)
166
- ?.getClusterLeaves(clusterId, 1000, 0)) || [];
167
-
168
- feature.set(
169
- "clusterLeaves",
170
- leaves.map((l) => {
171
- return geojson.readFeature(l);
172
- }),
173
- );
174
- }
175
-
176
- // Sharing station
177
- const sharingStationId = selectedFeature?.get("station_id");
178
- if (sharingStationId) {
179
- const sharingStationInfo =
180
- await fetchSharingStation(sharingStationId);
181
- selectedFeature.setProperties(sharingStationInfo);
182
- selectedFeature.set(
183
- "provider_logo",
184
- PROVIDER_LOGOS_BY_FEED_ID[selectedFeature?.get("feed_id")],
185
- );
186
- }
187
- }
188
-
189
- if (newStationId || newTrainId || !features.length) {
190
- setSelectedFeature(null);
191
- setSelectedFeatures([]);
192
- } else {
193
- setSelectedFeatures(features);
194
-
195
- // We prioritize some layers like the notification one over the others
196
- const notificationFeature = features.find((feat) => {
197
- return feat.get("situation");
198
- });
199
- if (notificationFeature) {
200
- setSelectedFeature(notificationFeature);
201
- } else {
202
- setSelectedFeature(features[0]);
203
- }
204
- }
205
- },
206
- [
207
- baseLayer?.mapLibreMap,
208
- stationId,
209
- trainId,
210
- realtimeLayer,
211
- stationsLayer,
212
- tenant,
213
- setStationId,
214
- setTrainId,
215
- queryablelayers,
216
- selectedFeature,
217
- setSelectedFeature,
218
- setSelectedFeatures,
219
- ],
220
- );
221
-
222
- useEffect(() => {
223
- const key = map?.on("singleclick", onSingleClick);
224
- return () => {
225
- unByKey(key);
226
- };
227
- }, [map, onSingleClick]);
228
-
229
- useEffect(() => {
230
- const key = map?.on("pointermove", onPointerMove);
231
- return () => {
232
- unByKey(key);
233
- };
234
- }, [map, onPointerMove]);
235
-
236
- return null;
237
- }
238
- export default SingleClickListener;
@@ -1 +0,0 @@
1
- export { default } from "./RvfSingleClickListener";