@geops/rvf-mobility-web-component 0.1.10 → 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 (66) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/docutils.js +198 -0
  3. package/index.html +48 -217
  4. package/index.js +627 -87
  5. package/input.css +11 -1
  6. package/jest-setup.js +3 -2
  7. package/package.json +4 -3
  8. package/scripts/dev.mjs +1 -1
  9. package/search.html +38 -69
  10. package/src/GeolocationButton/GeolocationButton.tsx +6 -5
  11. package/src/LayerTree/LayerTree.tsx +44 -0
  12. package/src/LayerTree/TreeItem/TreeItem.tsx +145 -0
  13. package/src/LayerTree/TreeItem/index.tsx +1 -0
  14. package/src/LayerTree/TreeItemContainer/TreeItemContainer.tsx +16 -0
  15. package/src/LayerTree/TreeItemContainer/index.tsx +1 -0
  16. package/src/LayerTree/index.tsx +1 -0
  17. package/src/LayerTree/layersTreeContext.ts +4 -0
  18. package/src/LayerTree/layersTreeReducer.ts +156 -0
  19. package/src/Map/Map.tsx +1 -0
  20. package/src/MobilityMap/MobilityMap.tsx +10 -9
  21. package/src/MobilityMap/index.css +0 -13
  22. package/src/RealtimeLayer/RealtimeLayer.tsx +1 -1
  23. package/src/RvfButton/RvfButton.tsx +28 -21
  24. package/src/RvfExportMenu/RvfExportMenu.tsx +95 -0
  25. package/src/RvfExportMenu/index.tsx +1 -0
  26. package/src/RvfExportMenuButton/RvfExportMenuButton.tsx +27 -0
  27. package/src/RvfExportMenuButton/index.tsx +1 -0
  28. package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +29 -0
  29. package/src/RvfFeatureDetails/index.tsx +1 -0
  30. package/src/RvfIconButton/RvfIconButton.tsx +35 -0
  31. package/src/RvfIconButton/index.tsx +1 -0
  32. package/src/RvfMobilityMap/RvfMobilityMap.tsx +117 -83
  33. package/src/RvfMobilityMap/index.css +0 -13
  34. package/src/RvfModal/RvfModal.tsx +52 -0
  35. package/src/RvfModal/index.tsx +1 -0
  36. package/src/RvfPoisLayer/RvfPoisLayer.tsx +39 -0
  37. package/src/RvfPoisLayer/index.tsx +1 -0
  38. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +88 -0
  39. package/src/RvfSharedMobilityLayerGroup/index.tsx +1 -0
  40. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +137 -0
  41. package/src/RvfSingleClickListener/index.tsx +1 -0
  42. package/src/RvfZoomButtons/RvfZoomButtons.tsx +36 -29
  43. package/src/Search/Search.tsx +11 -9
  44. package/src/SingleClickListener/index.tsx +1 -1
  45. package/src/StationsLayer/StationsLayer.tsx +0 -1
  46. package/src/StopsSearch/StopsSearch.tsx +38 -6
  47. package/src/TopicMenu/TopicMenu.tsx +143 -0
  48. package/src/TopicMenu/index.tsx +1 -0
  49. package/src/icons/Cancel/Cancel.tsx +21 -0
  50. package/src/icons/Cancel/cancel.svg +7 -0
  51. package/src/icons/Cancel/index.tsx +1 -0
  52. package/src/icons/Download/Download.tsx +20 -0
  53. package/src/icons/Download/download.svg +15 -0
  54. package/src/icons/Download/index.tsx +1 -0
  55. package/src/icons/Elevator/Elevator.tsx +1 -1
  56. package/src/icons/Menu/Menu.tsx +32 -0
  57. package/src/icons/Menu/index.tsx +1 -0
  58. package/src/icons/Menu/menu.svg +9 -0
  59. package/src/utils/constants.ts +9 -0
  60. package/src/utils/createMobiDataBwWfsLayer.ts +120 -0
  61. package/src/utils/exportPdf.ts +677 -0
  62. package/src/utils/hooks/useRvfContext.tsx +37 -0
  63. package/src/utils/hooks/useUpdatePermalink.tsx +2 -9
  64. package/tailwind.config.mjs +41 -0
  65. package/src/RvfSharedMobilityLayer/RvfSharedMobilityLayer.tsx +0 -147
  66. package/src/RvfSharedMobilityLayer/index.tsx +0 -1
@@ -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";
@@ -1,9 +1,10 @@
1
+ import { unByKey } from "ol/Observable";
1
2
  import { memo } from "preact/compat";
2
- import { useState } from "preact/hooks";
3
+ import { useCallback, useEffect, useState } from "preact/hooks";
3
4
 
4
5
  import Minus from "../icons/Minus";
5
6
  import Plus from "../icons/Plus";
6
- import RvfButton from "../RvfButton";
7
+ import RvfIconButton from "../RvfIconButton";
7
8
  import useMapContext from "../utils/hooks/useMapContext";
8
9
 
9
10
  function RvfZoomButtons() {
@@ -11,50 +12,56 @@ function RvfZoomButtons() {
11
12
  const [isZoomInDisabled, setIsZoomInDisabled] = useState(false);
12
13
  const [isZoomOutDisabled, setIsZoomOutDisabled] = useState(false);
13
14
 
14
- const handleZoomIn = () => {
15
- const view = map.getView();
16
- const zoom = view.getZoom();
17
- const maxzoom = view.getMaxZoom();
18
- const minzoom = view.getMinZoom();
19
-
20
- if (maxzoom && zoom === Number(maxzoom)) {
21
- setIsZoomInDisabled(true);
15
+ const handleZoomIn = useCallback(() => {
16
+ if (!map?.getView()) {
22
17
  return;
23
18
  }
24
-
19
+ const view = map.getView();
20
+ const zoom = view.getZoom();
25
21
  view.setZoom(zoom + 1);
22
+ }, [map]);
26
23
 
27
- if (!minzoom || view.getZoom() > Number(minzoom)) {
28
- setIsZoomOutDisabled(false);
24
+ const handleZoomOut = useCallback(() => {
25
+ if (!map?.getView()) {
26
+ return;
29
27
  }
30
- };
31
-
32
- const handleZoomOut = () => {
33
28
  const view = map.getView();
34
29
  const zoom = view.getZoom();
35
- const maxzoom = view.getMaxZoom();
36
- const minzoom = view.getMinZoom();
30
+ view.setZoom(zoom - 1);
31
+ }, [map]);
37
32
 
38
- if (minzoom && zoom === Number(minzoom)) {
39
- setIsZoomOutDisabled(true);
40
- return;
41
- }
33
+ useEffect(() => {
34
+ const key = map?.on("moveend", () => {
35
+ const view = map.getView();
36
+ const zoom = view.getZoom();
37
+ const maxzoom = view.getMaxZoom();
38
+ const minzoom = view.getMinZoom();
42
39
 
43
- view.setZoom(zoom - 1);
40
+ if (maxzoom && zoom === Number(maxzoom)) {
41
+ setIsZoomInDisabled(true);
42
+ } else {
43
+ setIsZoomInDisabled(false);
44
+ }
44
45
 
45
- if (!maxzoom || view.getZoom() < Number(maxzoom)) {
46
- setIsZoomInDisabled(false);
47
- }
48
- };
46
+ if (minzoom && zoom === Number(minzoom)) {
47
+ setIsZoomOutDisabled(true);
48
+ } else {
49
+ setIsZoomOutDisabled(false);
50
+ }
51
+ });
52
+ return () => {
53
+ unByKey(key);
54
+ };
55
+ }, [map]);
49
56
 
50
57
  return (
51
58
  <>
52
- <RvfButton
59
+ <RvfIconButton
53
60
  disabled={isZoomInDisabled}
54
61
  Icon={Plus}
55
62
  onClick={handleZoomIn}
56
63
  />
57
- <RvfButton
64
+ <RvfIconButton
58
65
  disabled={isZoomOutDisabled}
59
66
  Icon={Minus}
60
67
  onClick={handleZoomOut}
@@ -1,3 +1,6 @@
1
+ import { memo } from "preact/compat";
2
+ import { useCallback } from "preact/hooks";
3
+
1
4
  import StopsSearch from "../StopsSearch";
2
5
  import centerOnStation from "../utils/centerOnStation";
3
6
  import useMapContext from "../utils/hooks/useMapContext";
@@ -5,14 +8,13 @@ import useMapContext from "../utils/hooks/useMapContext";
5
8
  function Search() {
6
9
  const { apikey, map, stopsurl } = useMapContext();
7
10
 
8
- return (
9
- <StopsSearch
10
- apikey={apikey}
11
- onselect={(selected) => {
12
- return centerOnStation(selected, map);
13
- }}
14
- url={stopsurl}
15
- />
11
+ const onSelect = useCallback(
12
+ (selected) => {
13
+ return centerOnStation(selected, map);
14
+ },
15
+ [map],
16
16
  );
17
+
18
+ return <StopsSearch apikey={apikey} onselect={onSelect} url={stopsurl} />;
17
19
  }
18
- export default Search;
20
+ export default memo(Search);
@@ -1 +1 @@
1
- export { default } from "./SingleClickListener";
1
+ export { default } from "../RvfSingleClickListener";
@@ -31,7 +31,6 @@ function StationsLayer(props: MaplibreStyleLayerOptions) {
31
31
 
32
32
  return () => {
33
33
  map.removeLayer(layer);
34
- layer.detachFromMap();
35
34
  };
36
35
  }, [map, setStationsLayer, layer]);
37
36
 
@@ -18,7 +18,7 @@ import tailwind from "../style.css";
18
18
  import i18n from "../utils/i18n";
19
19
  import MobilityEvent from "../utils/MobilityEvent";
20
20
 
21
- export type SearchProps = {
21
+ export type MobilityStopsSearchProps = {
22
22
  apikey: string;
23
23
  bbox?: string;
24
24
  countrycode?: string;
@@ -58,13 +58,45 @@ function StopsSearch({
58
58
  prefagencies,
59
59
  reflocation,
60
60
  url = "https://api.geops.io/stops/v1/",
61
- }: SearchProps) {
61
+ }: MobilityStopsSearchProps) {
62
62
  const { t } = i18n;
63
63
  const [query, setQuery] = useState("");
64
64
  const [selectedStation, setSelectedStation] = useState<StationFeature>();
65
65
  const [results, setResults] = useState<StopsResponse["features"]>(undefined);
66
66
  const myRef = useRef<HTMLDivElement>();
67
67
 
68
+ useEffect(() => {
69
+ myRef.current?.dispatchEvent(
70
+ new MobilityEvent<MobilityStopsSearchProps>("mwc:attribute", {
71
+ apikey,
72
+ bbox,
73
+ countrycode,
74
+ event,
75
+ field,
76
+ limit,
77
+ mots,
78
+ onselect,
79
+ params,
80
+ prefagencies,
81
+ reflocation,
82
+ url,
83
+ }),
84
+ );
85
+ }, [
86
+ apikey,
87
+ bbox,
88
+ countrycode,
89
+ event,
90
+ field,
91
+ limit,
92
+ mots,
93
+ onselect,
94
+ params,
95
+ prefagencies,
96
+ reflocation,
97
+ url,
98
+ ]);
99
+
68
100
  const api: StopsAPI = useMemo(() => {
69
101
  return new StopsAPI({ apiKey: apikey, url: url });
70
102
  }, [apikey, url]);
@@ -78,9 +110,9 @@ function StopsSearch({
78
110
  bubbles: true,
79
111
  },
80
112
  );
81
- if (myRef.current) {
82
- myRef.current.dispatchEvent(customEvt);
83
- }
113
+
114
+ myRef.current?.dispatchEvent(customEvt);
115
+
84
116
  if (onselect && typeof onselect === "function") {
85
117
  onselect(station);
86
118
  }
@@ -208,7 +240,7 @@ function StopsSearch({
208
240
  </div>
209
241
  </div>
210
242
 
211
- <div className="mt-[-4px] flex grow overflow-auto rounded-md rounded-t-none bg-white shadow">
243
+ <div className="-mt-4px flex grow overflow-auto rounded-md rounded-t-none bg-white shadow">
212
244
  {results && results.length === 0 && (
213
245
  <div
214
246
  className={