@geops/rvf-mobility-web-component 0.1.9 → 0.1.11

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 (80) hide show
  1. package/.github/CODEOWNERS +37 -0
  2. package/.github/workflows/conventional-pr-title.yml +36 -0
  3. package/CHANGELOG.md +55 -0
  4. package/README.md +3 -1
  5. package/doc/package.json +5 -5
  6. package/doc/src/app/components/GeopsMobilityDoc.tsx +19 -0
  7. package/docutils.js +198 -0
  8. package/index.html +48 -217
  9. package/index.js +683 -91
  10. package/input.css +15 -1
  11. package/jest-setup.js +3 -2
  12. package/package.json +9 -8
  13. package/scripts/dev.mjs +1 -1
  14. package/search.html +38 -69
  15. package/src/GeolocationButton/GeolocationButton.tsx +6 -17
  16. package/src/LayerTree/LayerTree.tsx +44 -0
  17. package/src/LayerTree/TreeItem/TreeItem.tsx +145 -0
  18. package/src/LayerTree/TreeItem/index.tsx +1 -0
  19. package/src/LayerTree/TreeItemContainer/TreeItemContainer.tsx +16 -0
  20. package/src/LayerTree/TreeItemContainer/index.tsx +1 -0
  21. package/src/LayerTree/index.tsx +1 -0
  22. package/src/LayerTree/layersTreeContext.ts +4 -0
  23. package/src/LayerTree/layersTreeReducer.ts +156 -0
  24. package/src/Map/Map.tsx +57 -12
  25. package/src/MobilityMap/MobilityMap.tsx +22 -9
  26. package/src/MobilityMap/index.css +0 -13
  27. package/src/RealtimeLayer/RealtimeLayer.tsx +1 -1
  28. package/src/RvfButton/RvfButton.tsx +45 -0
  29. package/src/RvfButton/index.tsx +1 -0
  30. package/src/RvfExportMenu/RvfExportMenu.tsx +95 -0
  31. package/src/RvfExportMenu/index.tsx +1 -0
  32. package/src/RvfExportMenuButton/RvfExportMenuButton.tsx +27 -0
  33. package/src/RvfExportMenuButton/index.tsx +1 -0
  34. package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +29 -0
  35. package/src/RvfFeatureDetails/index.tsx +1 -0
  36. package/src/RvfIconButton/RvfIconButton.tsx +35 -0
  37. package/src/RvfIconButton/index.tsx +1 -0
  38. package/src/RvfMobilityMap/RvfMobilityMap.tsx +132 -52
  39. package/src/RvfMobilityMap/index.css +0 -13
  40. package/src/RvfModal/RvfModal.tsx +52 -0
  41. package/src/RvfModal/index.tsx +1 -0
  42. package/src/RvfPoisLayer/RvfPoisLayer.tsx +39 -0
  43. package/src/RvfPoisLayer/index.tsx +1 -0
  44. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +88 -0
  45. package/src/RvfSharedMobilityLayerGroup/index.tsx +1 -0
  46. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +137 -0
  47. package/src/RvfSingleClickListener/index.tsx +1 -0
  48. package/src/RvfZoomButtons/RvfZoomButtons.tsx +73 -0
  49. package/src/RvfZoomButtons/index.tsx +1 -0
  50. package/src/Search/Search.tsx +11 -9
  51. package/src/SingleClickListener/index.tsx +1 -1
  52. package/src/StationsLayer/StationsLayer.tsx +0 -1
  53. package/src/StopsSearch/StopsSearch.tsx +38 -6
  54. package/src/TopicMenu/TopicMenu.tsx +143 -0
  55. package/src/TopicMenu/index.tsx +1 -0
  56. package/src/icons/Cancel/Cancel.tsx +21 -0
  57. package/src/icons/Cancel/cancel.svg +7 -0
  58. package/src/icons/Cancel/index.tsx +1 -0
  59. package/src/icons/Download/Download.tsx +20 -0
  60. package/src/icons/Download/download.svg +15 -0
  61. package/src/icons/Download/index.tsx +1 -0
  62. package/src/icons/Elevator/Elevator.tsx +1 -1
  63. package/src/icons/Geolocation/Geolocation.tsx +21 -0
  64. package/src/icons/Geolocation/index.tsx +1 -0
  65. package/src/icons/Menu/Menu.tsx +32 -0
  66. package/src/icons/Menu/index.tsx +1 -0
  67. package/src/icons/Menu/menu.svg +9 -0
  68. package/src/icons/Minus/Minus.tsx +19 -0
  69. package/src/icons/Minus/index.tsx +1 -0
  70. package/src/icons/Minus/minus.svg +7 -0
  71. package/src/icons/Plus/Plus.tsx +19 -0
  72. package/src/icons/Plus/index.tsx +1 -0
  73. package/src/icons/Plus/plus.svg +7 -0
  74. package/src/index.tsx +2 -0
  75. package/src/utils/constants.ts +9 -0
  76. package/src/utils/createMobiDataBwWfsLayer.ts +120 -0
  77. package/src/utils/exportPdf.ts +677 -0
  78. package/src/utils/hooks/useRvfContext.tsx +37 -0
  79. package/src/utils/hooks/useUpdatePermalink.tsx +2 -9
  80. package/tailwind.config.mjs +60 -8
@@ -9,49 +9,67 @@ import {
9
9
  RealtimeStopSequence,
10
10
  RealtimeTrainId,
11
11
  } from "mobility-toolbox-js/types";
12
- import { Map as OlMap } from "ol";
13
- import { getCenter } from "ol/extent";
14
- import { fromLonLat } from "ol/proj";
12
+ import { Feature, Map as OlMap } from "ol";
15
13
  import { memo } from "preact/compat";
16
- import { useEffect, useMemo, useState } from "preact/hooks";
14
+ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
17
15
 
18
16
  import BaseLayer from "../BaseLayer";
19
17
  import Copyright from "../Copyright";
20
18
  import GeolocationButton from "../GeolocationButton";
19
+ import Cancel from "../icons/Cancel";
20
+ import Menu from "../icons/Menu";
21
21
  import Map from "../Map";
22
22
  import { MobilityMapProps } from "../MobilityMap/MobilityMap";
23
23
  import NotificationLayer from "../NotificationLayer";
24
24
  import Overlay from "../Overlay";
25
25
  import RealtimeLayer from "../RealtimeLayer";
26
26
  import RouteSchedule from "../RouteSchedule";
27
+ import RvfExportMenu from "../RvfExportMenu";
28
+ import RvfExportMenuButton from "../RvfExportMenuButton";
29
+ import RvfFeatureDetails from "../RvfFeatureDetails";
30
+ // Notificationurl example: https://mobility-web-component-tmp.vercel.app/geops-mobility?notificationurl=https%3A%2F%2Fmoco.geops.io%2Fapi%2Fv1%2Fexport%2Fnotification%2F%3Fsso_config%3Dsob&geolocation=false&realtime=false&search=false&notificationat=2024-01-25T22%3A59%3A00Z
31
+ import RvfIconButton from "../RvfIconButton";
32
+ import Modal from "../RvfModal";
33
+ import RvfPoisLayer from "../RvfPoisLayer";
34
+ import RvfSharedMobilityLayerGroup from "../RvfSharedMobilityLayerGroup";
35
+ import RvfZoomButtons from "../RvfZoomButtons";
27
36
  import ScaleLine from "../ScaleLine";
28
37
  import Search from "../Search";
29
- import SingleClickListener from "../SingleClickListener/SingleClickListener";
38
+ import SingleClickListener from "../SingleClickListener";
30
39
  import Station from "../Station";
31
40
  import StationsLayer from "../StationsLayer";
32
41
  // @ts-expect-error bad type definition
33
42
  import tailwind from "../style.css";
43
+ import TopicMenu from "../TopicMenu";
44
+ import { RVF_EXTENT_3857 } from "../utils/constants";
34
45
  import { I18nContext } from "../utils/hooks/useI18n";
35
46
  import { MapContext } from "../utils/hooks/useMapContext";
47
+ import { RvfContext } from "../utils/hooks/useRvfContext";
36
48
  import useUpdatePermalink from "../utils/hooks/useUpdatePermalink";
37
49
  import i18n from "../utils/i18n";
38
50
  import MobilityEvent from "../utils/MobilityEvent";
39
51
  // @ts-expect-error bad type definition
40
52
  import style from "./index.css";
41
- // Notificationurl example: https://mobility-web-component-tmp.vercel.app/geops-mobility?notificationurl=https%3A%2F%2Fmoco.geops.io%2Fapi%2Fv1%2Fexport%2Fnotification%2F%3Fsso_config%3Dsob&geolocation=false&realtime=false&search=false&notificationat=2024-01-25T22%3A59%3A00Z
42
53
 
43
54
  export type RvfMobilityMapProps = {} & MobilityMapProps;
44
55
 
45
- const extent = [7.5, 47.7, 8.45, 48.4];
46
- const centerRvf = fromLonLat(getCenter(extent)).join(",");
56
+ const bbox = RVF_EXTENT_3857.join(",");
57
+
58
+ const baseLayerProps = {
59
+ mapLibreOptions: {
60
+ preserveDrawingBuffer: true,
61
+ },
62
+ };
47
63
 
48
64
  function RvfMobilityMap({
49
65
  apikey = "5cc87b12d7c5370001c1d655820abcc37dfd4d968d7bab5b2a74a935",
50
66
  baselayer = "de.rvf",
51
- center = centerRvf,
67
+ center = null,
68
+ extent = bbox,
52
69
  geolocation = "true",
53
70
  mapsurl = "https://maps.geops.io",
54
- maxzoom = null,
71
+ maxextent = bbox,
72
+ maxzoom = "20",
55
73
  minzoom = null,
56
74
  mots = null,
57
75
  notification = "true",
@@ -64,8 +82,9 @@ function RvfMobilityMap({
64
82
  search = "false",
65
83
  stopsurl = "https://api.geops.io/stops/v1/",
66
84
  tenant = null,
67
- zoom = "9.8",
85
+ zoom = null,
68
86
  }: RvfMobilityMapProps) {
87
+ const eventNodeRef = useRef<HTMLDivElement>();
69
88
  const [baseLayer, setBaseLayer] = useState<MaplibreLayer>();
70
89
  const [isFollowing, setIsFollowing] = useState(false);
71
90
  const [isTracking, setIsTracking] = useState(false);
@@ -76,10 +95,14 @@ function RvfMobilityMap({
76
95
  const [map, setMap] = useState<OlMap>();
77
96
  const [stationId, setStationId] = useState<RealtimeStationId>();
78
97
  const [trainId, setTrainId] = useState<RealtimeTrainId>();
98
+ const [isExportMenuOpen, setIsExportMenuOpen] = useState<boolean>(false);
99
+ const [selectedFeature, setSelectedFeature] = useState<Feature>();
100
+ const [selectedFeatures, setSelectedFeatures] = useState<Feature[]>();
101
+ const [isLayerTreeOpen, setIsLayerTreeOpen] = useState(false);
79
102
 
80
103
  // TODO: this should be removed. The parent application should be responsible to do this
81
104
  // or we should find something that fit more usecases
82
- const { x, y, z } = useUpdatePermalink(map, permalink === "true");
105
+ useUpdatePermalink(map, permalink === "true");
83
106
 
84
107
  const mapContextValue = useMemo(() => {
85
108
  return {
@@ -88,11 +111,13 @@ function RvfMobilityMap({
88
111
  baselayer,
89
112
  baseLayer,
90
113
  center,
114
+ extent,
91
115
  geolocation,
92
116
  isFollowing,
93
117
  isTracking,
94
118
  map,
95
119
  mapsurl,
120
+ maxextent,
96
121
  maxzoom,
97
122
  minzoom,
98
123
  mots,
@@ -127,11 +152,13 @@ function RvfMobilityMap({
127
152
  baselayer,
128
153
  baseLayer,
129
154
  center,
155
+ extent,
130
156
  geolocation,
131
157
  isFollowing,
132
158
  isTracking,
133
159
  map,
134
160
  mapsurl,
161
+ maxextent,
135
162
  maxzoom,
136
163
  minzoom,
137
164
  mots,
@@ -153,12 +180,14 @@ function RvfMobilityMap({
153
180
  ]);
154
181
 
155
182
  useEffect(() => {
156
- dispatchEvent(
183
+ eventNodeRef.current?.dispatchEvent(
157
184
  new MobilityEvent<RvfMobilityMapProps>("mwc:attribute", {
158
185
  baselayer,
159
- center: x && y ? `${x},${y}` : center,
186
+ center,
187
+ extent,
160
188
  geolocation,
161
189
  mapsurl,
190
+ maxextent,
162
191
  maxzoom,
163
192
  minzoom,
164
193
  mots,
@@ -170,7 +199,7 @@ function RvfMobilityMap({
170
199
  realtimeurl,
171
200
  search,
172
201
  tenant,
173
- zoom: z || zoom,
202
+ zoom,
174
203
  }),
175
204
  );
176
205
  }, [
@@ -190,53 +219,104 @@ function RvfMobilityMap({
190
219
  search,
191
220
  tenant,
192
221
  zoom,
193
- x,
194
- y,
195
- z,
222
+ extent,
223
+ maxextent,
196
224
  ]);
197
225
 
226
+ const rvfContextValue = useMemo(() => {
227
+ return {
228
+ isExportMenuOpen,
229
+ selectedFeature,
230
+ selectedFeatures,
231
+ setIsExportMenuOpen,
232
+ setSelectedFeature,
233
+ setSelectedFeatures,
234
+ };
235
+ }, [isExportMenuOpen, selectedFeature, selectedFeatures]);
236
+
198
237
  return (
199
238
  <I18nContext.Provider value={i18n}>
200
239
  <style>{tailwind}</style>
201
240
  <style>{style}</style>
202
241
  <MapContext.Provider value={mapContextValue}>
203
- <div className="relative size-full border font-sans @container/main">
204
- <div className="relative flex size-full flex-col @lg/main:flex-row-reverse">
205
- <Map className="relative flex-1 overflow-visible ">
206
- <BaseLayer />
207
- <SingleClickListener />
208
- {realtime === "true" && <RealtimeLayer />}
209
- {tenant && <StationsLayer />}
210
- {notification === "true" && <NotificationLayer />}
211
- <div className="absolute inset-x-2 bottom-2 z-10 flex items-end justify-between gap-2 text-[10px]">
212
- <ScaleLine className="bg-slate-50/70" />
213
- <Copyright className="bg-slate-50/70" />
214
- </div>
215
- <div className="absolute right-2 top-2 z-10 flex flex-col gap-2">
216
- {geolocation === "true" && <GeolocationButton />}
217
- </div>
218
- {search === "true" && (
219
- <div className="absolute left-2 right-12 top-2 z-10 flex max-h-[90%] min-w-64 max-w-96 flex-col">
220
- <Search />
242
+ <RvfContext.Provider value={rvfContextValue}>
243
+ <div
244
+ className="relative size-full border font-sans @container/main"
245
+ ref={eventNodeRef}
246
+ >
247
+ <div className="relative flex size-full flex-col @lg/main:flex-row-reverse">
248
+ <Map className="relative flex-1 overflow-visible ">
249
+ <BaseLayer {...baseLayerProps} isNotInLayerTree />
250
+ <SingleClickListener />
251
+
252
+ {realtime === "true" && <RealtimeLayer title="Realtime data" />}
253
+ {tenant && <StationsLayer />}
254
+ {notification === "true" && <NotificationLayer />}
255
+ {/* <RvfLnpLayer />
256
+ <RvfVerkaufStellenLayer />
257
+ <RvfTarifZonenLayer /> */}
258
+ <RvfPoisLayer title="POIs" />
259
+
260
+ <RvfSharedMobilityLayerGroup title="Shared Mobility" />
261
+
262
+ <div className="absolute left-2 top-2 z-10">
263
+ <RvfIconButton
264
+ onClick={() => {
265
+ return setIsLayerTreeOpen(!isLayerTreeOpen);
266
+ }}
267
+ selected={isLayerTreeOpen}
268
+ >
269
+ {isLayerTreeOpen ? <Cancel /> : <Menu />}
270
+ </RvfIconButton>
271
+ {isLayerTreeOpen && <TopicMenu map={map} />}
221
272
  </div>
273
+ <div className="absolute inset-x-2 bottom-2 z-10 flex items-end justify-between gap-2 text-[10px]">
274
+ <ScaleLine className="bg-slate-50/70" />
275
+ <Copyright className="bg-slate-50/70" />
276
+ </div>
277
+ <div className="absolute right-2 top-2 z-10 flex flex-col gap-2">
278
+ {geolocation === "true" && <GeolocationButton />}
279
+ </div>
280
+ {search === "true" && (
281
+ <div className="absolute left-2 right-12 top-2 z-10 flex max-h-[90%] min-w-64 max-w-96 flex-col">
282
+ <Search />
283
+ </div>
284
+ )}
285
+ <div className="absolute bottom-10 right-2 z-10 flex flex-col justify-between gap-2">
286
+ <RvfExportMenuButton />
287
+ <RvfZoomButtons />
288
+ </div>
289
+ </Map>
290
+
291
+ <Overlay
292
+ className={"z-50"}
293
+ ScrollableHandlerProps={{
294
+ style: { width: "calc(100% - 60px)" },
295
+ }}
296
+ >
297
+ {realtime === "true" && trainId && (
298
+ <RouteSchedule className="relative overflow-y-auto overflow-x-hidden" />
299
+ )}
300
+ {tenant && stationId && (
301
+ <Station className="relative overflow-y-auto overflow-x-hidden" />
302
+ )}
303
+ {selectedFeature && (
304
+ <RvfFeatureDetails className="relative overflow-y-auto overflow-x-hidden" />
305
+ )}
306
+ </Overlay>
307
+
308
+ {isExportMenuOpen && (
309
+ <Modal
310
+ onClose={() => {
311
+ setIsExportMenuOpen(false);
312
+ }}
313
+ >
314
+ <RvfExportMenu className="relative flex h-full flex-col overflow-y-auto overflow-x-hidden" />
315
+ </Modal>
222
316
  )}
223
- </Map>
224
-
225
- <Overlay
226
- className={"z-50"}
227
- ScrollableHandlerProps={{
228
- style: { width: "calc(100% - 60px)" },
229
- }}
230
- >
231
- {realtime === "true" && trainId && (
232
- <RouteSchedule className="relative overflow-y-auto overflow-x-hidden" />
233
- )}
234
- {tenant && stationId && (
235
- <Station className="relative overflow-y-auto overflow-x-hidden" />
236
- )}
237
- </Overlay>
317
+ </div>
238
318
  </div>
239
- </div>
319
+ </RvfContext.Provider>
240
320
  </MapContext.Provider>
241
321
  </I18nContext.Provider>
242
322
  );
@@ -1,13 +0,0 @@
1
- ::-webkit-scrollbar {
2
- width: 3px;
3
- height: 3px;
4
- }
5
-
6
- ::-webkit-scrollbar-thumb {
7
- background: lightgray;
8
- z-index: 5;
9
- }
10
-
11
- ::-webkit-scrollbar-track {
12
- background: transparent;
13
- }
@@ -0,0 +1,52 @@
1
+ import { JSX, PreactDOMAttributes } from "preact";
2
+ import { memo } from "preact/compat";
3
+ import { useEffect, useState } from "preact/hooks";
4
+
5
+ export type ModalProps = {
6
+ onClose: () => void;
7
+ } & JSX.HTMLAttributes<HTMLDialogElement> &
8
+ PreactDOMAttributes;
9
+
10
+ function RvfModal({ children, onClose, ...props }: ModalProps) {
11
+ const [node, setNode] = useState<HTMLDialogElement>();
12
+ let hasChildren = !!children;
13
+ if (Array.isArray(children)) {
14
+ hasChildren =
15
+ children?.length &&
16
+ (children || []).find((c) => {
17
+ return !!c;
18
+ });
19
+ }
20
+
21
+ useEffect(() => {
22
+ node?.addEventListener("close", onClose);
23
+ return () => {
24
+ node?.removeEventListener("close", onClose);
25
+ };
26
+ }, [node, onClose]);
27
+
28
+ if (!hasChildren) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <dialog
34
+ className={
35
+ "absolute inset-0 z-50 flex size-full items-center justify-center bg-transparent"
36
+ }
37
+ ref={(n) => {
38
+ setNode(n);
39
+ n?.focus();
40
+ }}
41
+ {...props}
42
+ >
43
+ <button
44
+ className="absolute inset-0 z-0 size-full bg-black/60"
45
+ onClick={onClose}
46
+ ></button>
47
+ <div className="z-10 h-3/4 w-1/2 bg-white">{children}</div>
48
+ </dialog>
49
+ );
50
+ }
51
+
52
+ export default memo(RvfModal);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfModal";
@@ -0,0 +1,39 @@
1
+ import { MaplibreStyleLayer } from "mobility-toolbox-js/ol";
2
+ import { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreStyleLayer";
3
+ import { memo } from "preact/compat";
4
+ import { useEffect, useMemo } from "preact/hooks";
5
+
6
+ import useMapContext from "../utils/hooks/useMapContext";
7
+
8
+ function RvfPoisLayer(props: MaplibreStyleLayerOptions) {
9
+ const { baseLayer, map } = useMapContext();
10
+
11
+ const layer = useMemo(() => {
12
+ if (!baseLayer) {
13
+ return null;
14
+ }
15
+ return new MaplibreStyleLayer({
16
+ layersFilter: ({ metadata }) => {
17
+ return metadata?.["mapset.filter"] === "mapset_poi";
18
+ },
19
+ maplibreLayer: baseLayer,
20
+ visible: false,
21
+ ...(props || {}),
22
+ });
23
+ }, [baseLayer, props]);
24
+
25
+ useEffect(() => {
26
+ if (!map || !layer) {
27
+ return;
28
+ }
29
+
30
+ map.addLayer(layer);
31
+ return () => {
32
+ map.removeLayer(layer);
33
+ };
34
+ }, [map, layer]);
35
+
36
+ return null; // <RegisterForSelectFeaturesOnClick />;
37
+ }
38
+
39
+ export default memo(RvfPoisLayer);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfPoisLayer";
@@ -0,0 +1,88 @@
1
+ import type { Options } from "ol/layer/Group";
2
+
3
+ import { Group, Vector } from "ol/layer";
4
+ import { memo } from "preact/compat";
5
+ import { useEffect, useMemo } from "preact/hooks";
6
+
7
+ import createMobiDataBwWfsLayer from "../utils/createMobiDataBwWfsLayer";
8
+ import useMapContext from "../utils/hooks/useMapContext";
9
+
10
+ function RvfSharedMobilityLayerGroup(props: Options & Record<string, unknown>) {
11
+ const { map } = useMapContext();
12
+
13
+ const group = useMemo(() => {
14
+ const sharingStationsBicycle = createMobiDataBwWfsLayer(
15
+ "sharing_stations_bicycle",
16
+ "red",
17
+ );
18
+ sharingStationsBicycle.set("title", "Stations Bicycle");
19
+
20
+ const sharingStationsCar = createMobiDataBwWfsLayer(
21
+ "sharing_stations_car",
22
+ "blue",
23
+ );
24
+ sharingStationsCar.set("title", "Stations Car");
25
+
26
+ const sharingStationsCargoBicycle = createMobiDataBwWfsLayer(
27
+ "sharing_stations_cargo_bicycle",
28
+ "green",
29
+ );
30
+ sharingStationsCargoBicycle.set("title", "Stations Cargo Bicycle");
31
+
32
+ const sharingStationsScooterStanding = createMobiDataBwWfsLayer(
33
+ "sharing_stations_scooters_standing",
34
+ "purple",
35
+ );
36
+ sharingStationsScooterStanding.set("title", "Stations Scooter");
37
+
38
+ const sharingVehicles = createMobiDataBwWfsLayer(
39
+ "sharing_vehicles",
40
+ "orange",
41
+ );
42
+ sharingVehicles.set("title", "Free Floating");
43
+ console.log("ici");
44
+ return new Group({
45
+ layers: [
46
+ sharingStationsBicycle,
47
+ sharingStationsCar,
48
+ sharingStationsCargoBicycle,
49
+ sharingStationsScooterStanding,
50
+ sharingVehicles,
51
+ ],
52
+ ...props,
53
+ });
54
+ }, [props]);
55
+
56
+ // Reload features every minute
57
+ useEffect(() => {
58
+ const interval = window.setInterval(() => {
59
+ group.getLayers().forEach((layer: Vector) => {
60
+ // @ts-expect-error - private property
61
+ layer.getSource().loadedExtentsRtree_.clear();
62
+ layer.getSource().clear(true);
63
+ layer.getSource().changed();
64
+ });
65
+ }, 60000);
66
+ return () => {
67
+ window.clearInterval(interval);
68
+ };
69
+ }, [group]);
70
+
71
+ useEffect(() => {
72
+ if (!map || !group) {
73
+ return;
74
+ }
75
+ map.on("moveend", () => {
76
+ console.log("ZOOM", map.getView().getZoom());
77
+ });
78
+
79
+ map.addLayer(group);
80
+
81
+ return () => {
82
+ map.removeLayer(group);
83
+ };
84
+ });
85
+
86
+ return null;
87
+ }
88
+ export default memo(RvfSharedMobilityLayerGroup);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfSharedMobilityLayerGroup";
@@ -0,0 +1,137 @@
1
+ import { Feature, MapBrowserEvent } from "ol";
2
+ import { GeoJSON } from "ol/format";
3
+ import { unByKey } from "ol/Observable";
4
+ import { toLonLat } from "ol/proj";
5
+ import { useCallback, useEffect } from "preact/hooks";
6
+
7
+ import useMapContext from "../utils/hooks/useMapContext";
8
+ import useRvfContext from "../utils/hooks/useRvfContext";
9
+ import MobilityEvent from "../utils/MobilityEvent";
10
+
11
+ const geojson = new GeoJSON();
12
+
13
+ function SingleClickListener() {
14
+ const {
15
+ map,
16
+ realtimeLayer,
17
+ setStationId,
18
+ setTrainId,
19
+ stationId,
20
+ stationsLayer,
21
+ tenant,
22
+ trainId,
23
+ } = useMapContext();
24
+ const { setSelectedFeature, setSelectedFeatures } = useRvfContext();
25
+
26
+ const onPointerMove = useCallback(
27
+ async (evt: MapBrowserEvent<PointerEvent>) => {
28
+ const [realtimeFeature] = evt.map.getFeaturesAtPixel(evt.pixel, {
29
+ layerFilter: (l) => {
30
+ return l === realtimeLayer;
31
+ },
32
+ });
33
+ realtimeLayer?.highlight(realtimeFeature as Feature);
34
+
35
+ const stationsFeatures = evt.map.getFeaturesAtPixel(evt.pixel, {
36
+ layerFilter: (l) => {
37
+ return l === stationsLayer;
38
+ },
39
+ });
40
+
41
+ const [stationFeature] = stationsFeatures.filter((feat) => {
42
+ return feat.get("tralis_network")?.includes(tenant);
43
+ });
44
+
45
+ evt.map.getTargetElement().style.cursor =
46
+ realtimeFeature || stationFeature ? "pointer" : "default";
47
+ },
48
+ [realtimeLayer, stationsLayer, tenant],
49
+ );
50
+
51
+ const onSingleClick = useCallback(
52
+ async (evt: MapBrowserEvent<PointerEvent>) => {
53
+ const [realtimeFeature] = evt.map.getFeaturesAtPixel(evt.pixel, {
54
+ layerFilter: (l) => {
55
+ return l === realtimeLayer;
56
+ },
57
+ });
58
+
59
+ const stationsFeatures = evt.map.getFeaturesAtPixel(evt.pixel, {
60
+ layerFilter: (l) => {
61
+ return l === stationsLayer;
62
+ },
63
+ });
64
+ const [stationFeature] = stationsFeatures.filter((feat) => {
65
+ return feat.get("tralis_network")?.includes(tenant);
66
+ });
67
+
68
+ const newStationId = stationFeature?.get("uid");
69
+
70
+ const newTrainId = realtimeFeature?.get("train_id");
71
+
72
+ if (newStationId && stationId !== newStationId) {
73
+ setStationId(newStationId);
74
+ setTrainId(null);
75
+ } else if (newTrainId && newTrainId !== trainId) {
76
+ setTrainId(realtimeFeature.get("train_id"));
77
+ setStationId(null);
78
+ } else {
79
+ setTrainId(null);
80
+ setStationId(null);
81
+ }
82
+
83
+ // Send all the features under the cursor
84
+ const features = evt.map.getFeaturesAtPixel(evt.pixel, {
85
+ layerFilter: (l) => {
86
+ return l.get("isQueryable");
87
+ },
88
+ }) as Feature[];
89
+ evt.map.getTargetElement().dispatchEvent(
90
+ new MobilityEvent("singleclick", {
91
+ ...evt,
92
+ features: geojson.writeFeaturesObject(features),
93
+ lonlat: toLonLat(evt.coordinate),
94
+ }),
95
+ );
96
+ // feature.get("form_factor"); //free float
97
+ // feature.get("num_vehicles_available"); // wfs livedata
98
+ // feature.get("provider_name"); // station sharing
99
+
100
+ if (newStationId || newTrainId || !features.length) {
101
+ setSelectedFeature(null);
102
+ setSelectedFeatures([]);
103
+ } else {
104
+ setSelectedFeatures(features);
105
+ setSelectedFeature(features[0]);
106
+ }
107
+ },
108
+ [
109
+ stationId,
110
+ trainId,
111
+ realtimeLayer,
112
+ stationsLayer,
113
+ tenant,
114
+ setStationId,
115
+ setTrainId,
116
+ setSelectedFeature,
117
+ setSelectedFeatures,
118
+ ],
119
+ );
120
+
121
+ useEffect(() => {
122
+ const key = map?.on("singleclick", onSingleClick);
123
+ return () => {
124
+ unByKey(key);
125
+ };
126
+ }, [map, onSingleClick]);
127
+
128
+ useEffect(() => {
129
+ const key = map?.on("pointermove", onPointerMove);
130
+ return () => {
131
+ unByKey(key);
132
+ };
133
+ }, [map, onPointerMove]);
134
+
135
+ return null;
136
+ }
137
+ export default SingleClickListener;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfSingleClickListener";