@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
package/src/Map/Map.tsx CHANGED
@@ -1,9 +1,10 @@
1
- import { Map as OlMap } from "ol";
1
+ import { Map as OlMap, View } from "ol";
2
+ import { unByKey } from "ol/Observable";
2
3
  // @ts-expect-error bad type definition
3
4
  import olStyle from "ol/ol.css";
4
5
  import { JSX, PreactDOMAttributes } from "preact";
5
6
  import { memo } from "preact/compat";
6
- import { useEffect, useRef } from "preact/hooks";
7
+ import { useEffect, useMemo, useRef } from "preact/hooks";
7
8
 
8
9
  import useMapContext from "../utils/hooks/useMapContext";
9
10
 
@@ -12,19 +13,51 @@ export type RealtimeMapProps = JSX.HTMLAttributes<HTMLDivElement> &
12
13
 
13
14
  function Map({ children, ...props }: RealtimeMapProps) {
14
15
  const mapRef = useRef();
15
- const {
16
- center = "831634,5933959",
17
- map,
18
- maxzoom,
19
- minzoom,
20
- setMap,
21
- zoom = "13",
22
- } = useMapContext();
16
+ const { center, extent, map, maxextent, maxzoom, minzoom, setMap, zoom } =
17
+ useMapContext();
18
+
19
+ const view = useMemo(() => {
20
+ if (!maxextent) {
21
+ return;
22
+ }
23
+ const bbox = maxextent.split(",").map((c) => {
24
+ return parseFloat(c);
25
+ });
26
+ return new View({
27
+ constrainOnlyCenter: false, // allow to have the same value as extent and max extent
28
+ extent: bbox,
29
+ showFullExtent: true,
30
+ });
31
+ }, [maxextent]);
32
+
33
+ useEffect(() => {
34
+ if (!map || !view) {
35
+ return;
36
+ }
37
+ const key = map.on("change:view", (evt) => {
38
+ const oldView = evt.oldValue;
39
+ if (oldView) {
40
+ view.setMinZoom(oldView.getMinZoom());
41
+ view.setMaxZoom(oldView.getMaxZoom());
42
+ view.setCenter(oldView.getCenter());
43
+ view.setZoom(oldView.getZoom());
44
+ }
45
+ });
46
+ map.setView(view);
47
+
48
+ return () => {
49
+ unByKey(key);
50
+ };
51
+ }, [map, view]);
23
52
 
24
53
  useEffect(() => {
25
54
  let newMap: OlMap;
26
55
  if (mapRef.current) {
27
- newMap = new OlMap({ controls: [], target: mapRef.current });
56
+ newMap = new OlMap({
57
+ controls: [],
58
+ // pixelRatio: 3,
59
+ target: mapRef.current,
60
+ });
28
61
  setMap(newMap);
29
62
  }
30
63
 
@@ -35,7 +68,19 @@ function Map({ children, ...props }: RealtimeMapProps) {
35
68
  }, [setMap]);
36
69
 
37
70
  useEffect(() => {
38
- if (!map) {
71
+ if (!map || !extent) {
72
+ return;
73
+ }
74
+ const bbox = extent.split(",").map((c) => {
75
+ return parseFloat(c);
76
+ });
77
+ if (bbox) {
78
+ map.getView().fit(bbox);
79
+ }
80
+ }, [map, extent]);
81
+
82
+ useEffect(() => {
83
+ if (!map || !center) {
39
84
  return;
40
85
  }
41
86
  const [x, y] = center.split(",").map((c) => {
@@ -11,7 +11,7 @@ import {
11
11
  } from "mobility-toolbox-js/types";
12
12
  import { Map as OlMap } from "ol";
13
13
  import { memo } from "preact/compat";
14
- import { useEffect, useMemo, useState } from "preact/hooks";
14
+ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
15
15
 
16
16
  import BaseLayer from "../BaseLayer";
17
17
  import Copyright from "../Copyright";
@@ -41,8 +41,10 @@ export interface MobilityMapProps {
41
41
  apikey?: string;
42
42
  baselayer?: string;
43
43
  center?: string;
44
+ extent?: string;
44
45
  geolocation?: string;
45
46
  mapsurl?: string;
47
+ maxextent?: string;
46
48
  maxzoom?: string;
47
49
  minzoom?: string;
48
50
  mots?: string;
@@ -63,8 +65,10 @@ function MobilityMap({
63
65
  apikey = null,
64
66
  baselayer = "travic_v2",
65
67
  center = "831634,5933959",
68
+ extent = null,
66
69
  geolocation = "true",
67
70
  mapsurl = "https://maps.geops.io",
71
+ maxextent = null,
68
72
  maxzoom = null,
69
73
  minzoom = null,
70
74
  mots = null,
@@ -80,6 +84,7 @@ function MobilityMap({
80
84
  tenant = null,
81
85
  zoom = "13",
82
86
  }: MobilityMapProps) {
87
+ const eventNodeRef = useRef<HTMLDivElement>();
83
88
  const [baseLayer, setBaseLayer] = useState<MaplibreLayer>();
84
89
  const [isFollowing, setIsFollowing] = useState(false);
85
90
  const [isTracking, setIsTracking] = useState(false);
@@ -93,7 +98,7 @@ function MobilityMap({
93
98
 
94
99
  // TODO: this should be removed. The parent application should be responsible to do this
95
100
  // or we should find something that fit more usecases
96
- const { x, y, z } = useUpdatePermalink(map, permalink === "true");
101
+ useUpdatePermalink(map, permalink === "true");
97
102
 
98
103
  const mapContextValue = useMemo(() => {
99
104
  return {
@@ -102,11 +107,13 @@ function MobilityMap({
102
107
  baselayer,
103
108
  baseLayer,
104
109
  center,
110
+ extent,
105
111
  geolocation,
106
112
  isFollowing,
107
113
  isTracking,
108
114
  map,
109
115
  mapsurl,
116
+ maxextent,
110
117
  maxzoom,
111
118
  minzoom,
112
119
  mots,
@@ -141,11 +148,13 @@ function MobilityMap({
141
148
  baselayer,
142
149
  baseLayer,
143
150
  center,
151
+ extent,
144
152
  geolocation,
145
153
  isFollowing,
146
154
  isTracking,
147
155
  map,
148
156
  mapsurl,
157
+ maxextent,
149
158
  maxzoom,
150
159
  minzoom,
151
160
  mots,
@@ -167,12 +176,14 @@ function MobilityMap({
167
176
  ]);
168
177
 
169
178
  useEffect(() => {
170
- dispatchEvent(
179
+ eventNodeRef.current?.dispatchEvent(
171
180
  new MobilityEvent<MobilityMapProps>("mwc:attribute", {
172
181
  baselayer,
173
- center: x && y ? `${x},${y}` : center,
182
+ center,
183
+ extent,
174
184
  geolocation,
175
185
  mapsurl,
186
+ maxextent,
176
187
  maxzoom,
177
188
  minzoom,
178
189
  mots,
@@ -184,14 +195,16 @@ function MobilityMap({
184
195
  realtimeurl,
185
196
  search,
186
197
  tenant,
187
- zoom: z || zoom,
198
+ zoom,
188
199
  }),
189
200
  );
190
201
  }, [
191
202
  baselayer,
192
203
  center,
204
+ extent,
193
205
  geolocation,
194
206
  mapsurl,
207
+ maxextent,
195
208
  maxzoom,
196
209
  minzoom,
197
210
  mots,
@@ -204,9 +217,6 @@ function MobilityMap({
204
217
  search,
205
218
  tenant,
206
219
  zoom,
207
- x,
208
- y,
209
- z,
210
220
  ]);
211
221
 
212
222
  return (
@@ -214,7 +224,10 @@ function MobilityMap({
214
224
  <style>{tailwind}</style>
215
225
  <style>{style}</style>
216
226
  <MapContext.Provider value={mapContextValue}>
217
- <div className="relative size-full border font-sans @container/main">
227
+ <div
228
+ className="relative size-full border font-sans @container/main"
229
+ ref={eventNodeRef}
230
+ >
218
231
  <div className="relative flex size-full flex-col @lg/main:flex-row-reverse">
219
232
  <Map className="relative flex-1 overflow-visible ">
220
233
  <BaseLayer />
@@ -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
- }
@@ -16,7 +16,7 @@ import useMapContext from "../utils/hooks/useMapContext";
16
16
 
17
17
  const TRACKING_ZOOM = 16;
18
18
 
19
- export type RealtimeLayerProps = RealtimeLayerOptions;
19
+ export type RealtimeLayerProps = RealtimeLayerOptions & Record<string, unknown>;
20
20
 
21
21
  function RealtimeLayer(props: RealtimeLayerProps) {
22
22
  const {
@@ -0,0 +1,45 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import { memo, useMemo } from "preact/compat";
4
+
5
+ export type RvfButtonProps = {
6
+ selected?: boolean;
7
+ theme?: "primary" | "secondary";
8
+ } & JSX.ButtonHTMLAttributes<HTMLButtonElement> &
9
+ PreactDOMAttributes;
10
+
11
+ const baseClasses =
12
+ "flex h-8 md:h-9 lg:h-10 px-5 py-1.75 max-h-button items-center justify-center rounded-full border";
13
+
14
+ export const themes = {
15
+ primary: {
16
+ classes:
17
+ "border-red bg-red text-white disabled:bg-lightgrey disabled:border-lightgrey hover:bg-darkred hover:border-darkred active:bg-lightred active:border-lightred",
18
+ selectedClasses: "bg-darkred border-darkred",
19
+ },
20
+ secondary: {
21
+ classes:
22
+ "border border-current bg-white text-grey hover:text-red disabled:text-lightgrey active:text-lightred",
23
+ selectedClasses: "text-red",
24
+ },
25
+ };
26
+
27
+ function RvfButton({
28
+ children,
29
+ className,
30
+ selected = false,
31
+ theme = "secondary",
32
+ ...props
33
+ }: RvfButtonProps) {
34
+ const classes = useMemo(() => {
35
+ return `${baseClasses} ${themes[theme].classes} ${selected ? themes[theme].selectedClasses : ""} ${className || ""}`;
36
+ }, [className, selected, theme]);
37
+
38
+ return (
39
+ <button className={classes} {...props}>
40
+ {children}
41
+ </button>
42
+ );
43
+ }
44
+
45
+ export default memo(RvfButton);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfButton";
@@ -0,0 +1,95 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import { memo, useId, useState } from "preact/compat";
4
+
5
+ import Cancel from "../icons/Cancel";
6
+ import RvfButton from "../RvfButton";
7
+ import RvfIconButton from "../RvfIconButton";
8
+ import exportPdf from "../utils/exportPdf";
9
+ import useMapContext from "../utils/hooks/useMapContext";
10
+ import useRvfContext from "../utils/hooks/useRvfContext";
11
+
12
+ export type RvfExportMenuButtonProps = JSX.HTMLAttributes<HTMLDivElement> &
13
+ PreactDOMAttributes;
14
+
15
+ const formats = ["A4", "A1", "A3", "A0"];
16
+
17
+ function RvfExportMenu({ ...props }: RvfExportMenuButtonProps) {
18
+ const { setIsExportMenuOpen } = useRvfContext();
19
+ const { map } = useMapContext();
20
+ const [useMaxExtent, setUseMaxExtent] = useState(false);
21
+ const [format, setFormat] = useState<string>(formats[0]);
22
+ const checkboxId = useId();
23
+ const selectId = useId();
24
+ const [isExporting, setIsExporting] = useState(false);
25
+ const [isExportingError, setIsExportingError] = useState(false);
26
+ return (
27
+ <div {...props}>
28
+ <div className={"flex h-full flex-col gap-2 p-2"}>
29
+ {/* <!-- Header --> */}
30
+ <div className={"flex flex-row items-center justify-between gap-2"}>
31
+ <h1>Export </h1>
32
+ <RvfIconButton
33
+ onClick={() => {
34
+ setIsExportMenuOpen(false);
35
+ }}
36
+ >
37
+ <Cancel />
38
+ </RvfIconButton>
39
+ </div>
40
+ {/* <!-- Content --> */}
41
+ <div className="flex flex-1 flex-col gap-2">
42
+ <div className={"flex gap-2"}>
43
+ <input
44
+ checked={useMaxExtent}
45
+ id={checkboxId}
46
+ onChange={() => {
47
+ setUseMaxExtent(!useMaxExtent);
48
+ }}
49
+ type="checkbox"
50
+ />
51
+ <label htmlFor={checkboxId}>Ganze Region exportieren</label>
52
+ </div>
53
+ <div className={"flex gap-2"}>
54
+ <label htmlFor={selectId}>Format:</label>
55
+ <select
56
+ className={"w-24"}
57
+ id={selectId}
58
+ onChange={(evt) => {
59
+ setFormat((evt.target as HTMLSelectElement).value);
60
+ }}
61
+ value={format}
62
+ >
63
+ {formats.map((format) => {
64
+ return <option key={format}>{format}</option>;
65
+ })}
66
+ </select>
67
+ </div>
68
+ </div>
69
+ {/* <!-- Footer --> */}
70
+ <div>
71
+ <RvfButton
72
+ disabled={isExporting}
73
+ onClick={async () => {
74
+ setIsExportingError(false);
75
+ setIsExporting(true);
76
+ const result = await exportPdf(map, { format }, { useMaxExtent });
77
+ setTimeout(() => {
78
+ setIsExporting(false);
79
+ setIsExportingError(!result);
80
+ }, 1000);
81
+ }}
82
+ >
83
+ {isExporting
84
+ ? "Exporting..."
85
+ : isExportingError
86
+ ? "Error"
87
+ : "Download"}
88
+ </RvfButton>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export default memo(RvfExportMenu);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfExportMenu";
@@ -0,0 +1,27 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import { memo } from "preact/compat";
4
+ import { useCallback } from "preact/hooks";
5
+
6
+ import Download from "../icons/Download";
7
+ import RvfIconButton from "../RvfIconButton";
8
+ import useRvfContext from "../utils/hooks/useRvfContext";
9
+
10
+ export type RvfExportMenuButtonProps = JSX.HTMLAttributes<HTMLButtonElement> &
11
+ PreactDOMAttributes;
12
+
13
+ function RvfExportMenuButton({ ...props }: RvfExportMenuButtonProps) {
14
+ const { isExportMenuOpen, setIsExportMenuOpen } = useRvfContext();
15
+
16
+ const onClick = useCallback(() => {
17
+ setIsExportMenuOpen(!isExportMenuOpen);
18
+ }, [isExportMenuOpen, setIsExportMenuOpen]);
19
+
20
+ return (
21
+ <RvfIconButton {...props} onClick={onClick} selected={isExportMenuOpen}>
22
+ <Download />
23
+ </RvfIconButton>
24
+ );
25
+ }
26
+
27
+ export default memo(RvfExportMenuButton);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfExportMenuButton";
@@ -0,0 +1,29 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import { memo } from "preact/compat";
4
+
5
+ import useRvfContext from "../utils/hooks/useRvfContext";
6
+
7
+ export type RvfFeatureDetailsProps = JSX.HTMLAttributes<HTMLDivElement> &
8
+ PreactDOMAttributes;
9
+
10
+ function RvfFeatureDetails(props: RvfFeatureDetailsProps) {
11
+ const { selectedFeature } = useRvfContext();
12
+
13
+ return (
14
+ <div {...props}>
15
+ {Object.entries(selectedFeature.getProperties()).map(([key, value]) => {
16
+ return (
17
+ <div className="flex gap-2" key={key}>
18
+ <span>
19
+ <b>{key}:</b>
20
+ </span>
21
+ <div>{value}</div>
22
+ </div>
23
+ );
24
+ })}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default memo(RvfFeatureDetails);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfFeatureDetails";
@@ -0,0 +1,35 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import { memo, SVGProps, useMemo } from "preact/compat";
4
+
5
+ import { RvfButtonProps, themes } from "../RvfButton/RvfButton";
6
+
7
+ export type RvfIconButtonProps = {
8
+ Icon?: (props: SVGProps<SVGSVGElement>) => preact.JSX.Element;
9
+ } & JSX.ButtonHTMLAttributes<HTMLButtonElement> &
10
+ PreactDOMAttributes &
11
+ RvfButtonProps;
12
+
13
+ const baseClasses =
14
+ "flex h-8 w-8 md:h-9 md:w-9 lg:w-10 lg:h-10 p-1.75 max-w-button max-h-button items-center justify-center rounded-full border";
15
+
16
+ function RvfIconButton({
17
+ children,
18
+ className,
19
+ Icon,
20
+ selected = false,
21
+ theme = "secondary",
22
+ ...props
23
+ }: RvfIconButtonProps) {
24
+ const classes = useMemo(() => {
25
+ return `${baseClasses} ${themes[theme].classes} ${selected ? themes[theme].selectedClasses : ""} ${className || ""}`;
26
+ }, [className, selected, theme]);
27
+
28
+ return (
29
+ <button className={classes} {...props}>
30
+ {children || <Icon height={"100%"} width={"100%"} />}
31
+ </button>
32
+ );
33
+ }
34
+
35
+ export default memo(RvfIconButton);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfIconButton";