@geops/rvf-mobility-web-component 0.1.8 → 0.1.10
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/.github/CODEOWNERS +37 -0
- package/.github/workflows/conventional-pr-title.yml +36 -0
- package/CHANGELOG.md +31 -0
- package/README.md +4 -2
- package/doc/package.json +5 -5
- package/doc/src/app/components/GeopsMobilityDoc.tsx +19 -0
- package/index.html +2 -2
- package/index.js +101 -49
- package/input.css +4 -0
- package/package.json +7 -7
- package/src/GeolocationButton/GeolocationButton.tsx +6 -18
- package/src/Map/Map.tsx +56 -12
- package/src/MobilityMap/MobilityMap.tsx +12 -0
- package/src/RvfButton/RvfButton.tsx +38 -0
- package/src/RvfButton/index.tsx +1 -0
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +52 -6
- package/src/RvfSharedMobilityLayer/RvfSharedMobilityLayer.tsx +147 -0
- package/src/RvfSharedMobilityLayer/index.tsx +1 -0
- package/src/RvfZoomButtons/RvfZoomButtons.tsx +66 -0
- package/src/RvfZoomButtons/index.tsx +1 -0
- package/src/icons/Geolocation/Geolocation.tsx +21 -0
- package/src/icons/Geolocation/index.tsx +1 -0
- package/src/icons/Minus/Minus.tsx +19 -0
- package/src/icons/Minus/index.tsx +1 -0
- package/src/icons/Minus/minus.svg +7 -0
- package/src/icons/Plus/Plus.tsx +19 -0
- package/src/icons/Plus/index.tsx +1 -0
- package/src/icons/Plus/plus.svg +7 -0
- package/src/index.tsx +2 -0
- package/tailwind.config.mjs +19 -8
package/input.css
CHANGED
package/package.json
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
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.
|
|
5
|
+
"version": "0.1.10",
|
|
6
6
|
"homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"maplibre-gl": "^4.7.1",
|
|
11
|
-
"mobility-toolbox-js": "3.0.0
|
|
12
|
-
"ol": "^10.3.
|
|
11
|
+
"mobility-toolbox-js": "3.0.0",
|
|
12
|
+
"ol": "^10.3.1",
|
|
13
13
|
"preact": "^10.25.1",
|
|
14
14
|
"preact-custom-element": "^4.3.0",
|
|
15
15
|
"react": "npm:@preact/compat@^18.3.1",
|
|
16
16
|
"react-dom": "npm:@preact/compat@^18.3.1",
|
|
17
|
-
"react-icons": "^5.
|
|
17
|
+
"react-icons": "^5.4.0",
|
|
18
18
|
"rosetta": "^1.1.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"eslint": "^9.16.0",
|
|
33
33
|
"eslint-config-prettier": "9.1.0",
|
|
34
34
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
35
|
-
"eslint-plugin-perfectionist": "^4.
|
|
35
|
+
"eslint-plugin-perfectionist": "^4.2.0",
|
|
36
36
|
"eslint-plugin-prettier": "^5.2.1",
|
|
37
37
|
"eslint-plugin-react": "^7.37.2",
|
|
38
38
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"jest-preset-preact": "^4.1.1",
|
|
47
47
|
"next": "15.0.3",
|
|
48
48
|
"preact-render-to-string": "^6.5.11",
|
|
49
|
-
"prettier": "^3.4.
|
|
49
|
+
"prettier": "^3.4.2",
|
|
50
50
|
"standard-version": "^9.5.0",
|
|
51
|
-
"tailwindcss": "^3.4.
|
|
51
|
+
"tailwindcss": "^3.4.16",
|
|
52
52
|
"ts-jest": "^29.2.5",
|
|
53
53
|
"typescript": "^5.7.2",
|
|
54
54
|
"typescript-eslint": "^8.17.0"
|
|
@@ -5,6 +5,8 @@ import { unByKey } from "ol/Observable";
|
|
|
5
5
|
import { fromLonLat } from "ol/proj";
|
|
6
6
|
import { useEffect, useMemo } from "preact/hooks";
|
|
7
7
|
|
|
8
|
+
import GeoIcon from "../icons/Geolocation";
|
|
9
|
+
import RvfButton from "../RvfButton";
|
|
8
10
|
import useMapContext from "../utils/hooks/useMapContext";
|
|
9
11
|
|
|
10
12
|
export type GeolocationButtonProps = JSX.HTMLAttributes<HTMLButtonElement> &
|
|
@@ -53,28 +55,14 @@ function GeolocationButton({ ...props }: GeolocationButtonProps) {
|
|
|
53
55
|
}, [geolocation, isTracking]);
|
|
54
56
|
|
|
55
57
|
return (
|
|
56
|
-
<
|
|
57
|
-
className="
|
|
58
|
+
<RvfButton
|
|
59
|
+
className={isTracking ? "animate-pulse" : ""}
|
|
60
|
+
Icon={GeoIcon}
|
|
58
61
|
onClick={() => {
|
|
59
62
|
setIsTracking(!isTracking);
|
|
60
63
|
}}
|
|
61
|
-
type="button"
|
|
62
64
|
{...props}
|
|
63
|
-
|
|
64
|
-
<svg
|
|
65
|
-
className={isTracking ? "animate-pulse" : ""}
|
|
66
|
-
fill="currentColor"
|
|
67
|
-
focusable="false"
|
|
68
|
-
height="1.5em"
|
|
69
|
-
stroke="currentColor"
|
|
70
|
-
strokeWidth="0"
|
|
71
|
-
viewBox="0 0 512 512"
|
|
72
|
-
width="1.5em"
|
|
73
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
74
|
-
>
|
|
75
|
-
<path d="M256 56c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m0-48C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 168c-44.183 0-80 35.817-80 80s35.817 80 80 80 80-35.817 80-80-35.817-80-80-80z" />
|
|
76
|
-
</svg>
|
|
77
|
-
</button>
|
|
65
|
+
/>
|
|
78
66
|
);
|
|
79
67
|
}
|
|
80
68
|
|
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,50 @@ export type RealtimeMapProps = JSX.HTMLAttributes<HTMLDivElement> &
|
|
|
12
13
|
|
|
13
14
|
function Map({ children, ...props }: RealtimeMapProps) {
|
|
14
15
|
const mapRef = useRef();
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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({
|
|
56
|
+
newMap = new OlMap({
|
|
57
|
+
controls: [],
|
|
58
|
+
target: mapRef.current,
|
|
59
|
+
});
|
|
28
60
|
setMap(newMap);
|
|
29
61
|
}
|
|
30
62
|
|
|
@@ -35,7 +67,19 @@ function Map({ children, ...props }: RealtimeMapProps) {
|
|
|
35
67
|
}, [setMap]);
|
|
36
68
|
|
|
37
69
|
useEffect(() => {
|
|
38
|
-
if (!map) {
|
|
70
|
+
if (!map || !extent) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const bbox = extent.split(",").map((c) => {
|
|
74
|
+
return parseFloat(c);
|
|
75
|
+
});
|
|
76
|
+
if (bbox) {
|
|
77
|
+
map.getView().fit(bbox);
|
|
78
|
+
}
|
|
79
|
+
}, [map, extent]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!map || !center) {
|
|
39
83
|
return;
|
|
40
84
|
}
|
|
41
85
|
const [x, y] = center.split(",").map((c) => {
|
|
@@ -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,
|
|
@@ -102,11 +106,13 @@ function MobilityMap({
|
|
|
102
106
|
baselayer,
|
|
103
107
|
baseLayer,
|
|
104
108
|
center,
|
|
109
|
+
extent,
|
|
105
110
|
geolocation,
|
|
106
111
|
isFollowing,
|
|
107
112
|
isTracking,
|
|
108
113
|
map,
|
|
109
114
|
mapsurl,
|
|
115
|
+
maxextent,
|
|
110
116
|
maxzoom,
|
|
111
117
|
minzoom,
|
|
112
118
|
mots,
|
|
@@ -141,11 +147,13 @@ function MobilityMap({
|
|
|
141
147
|
baselayer,
|
|
142
148
|
baseLayer,
|
|
143
149
|
center,
|
|
150
|
+
extent,
|
|
144
151
|
geolocation,
|
|
145
152
|
isFollowing,
|
|
146
153
|
isTracking,
|
|
147
154
|
map,
|
|
148
155
|
mapsurl,
|
|
156
|
+
maxextent,
|
|
149
157
|
maxzoom,
|
|
150
158
|
minzoom,
|
|
151
159
|
mots,
|
|
@@ -171,8 +179,10 @@ function MobilityMap({
|
|
|
171
179
|
new MobilityEvent<MobilityMapProps>("mwc:attribute", {
|
|
172
180
|
baselayer,
|
|
173
181
|
center: x && y ? `${x},${y}` : center,
|
|
182
|
+
extent,
|
|
174
183
|
geolocation,
|
|
175
184
|
mapsurl,
|
|
185
|
+
maxextent,
|
|
176
186
|
maxzoom,
|
|
177
187
|
minzoom,
|
|
178
188
|
mots,
|
|
@@ -190,8 +200,10 @@ function MobilityMap({
|
|
|
190
200
|
}, [
|
|
191
201
|
baselayer,
|
|
192
202
|
center,
|
|
203
|
+
extent,
|
|
193
204
|
geolocation,
|
|
194
205
|
mapsurl,
|
|
206
|
+
maxextent,
|
|
195
207
|
maxzoom,
|
|
196
208
|
minzoom,
|
|
197
209
|
mots,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { JSX, PreactDOMAttributes } from "preact";
|
|
2
|
+
|
|
3
|
+
import { memo, SVGProps } from "preact/compat";
|
|
4
|
+
|
|
5
|
+
export type RvfButtonProps = ButtonProps &
|
|
6
|
+
JSX.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
7
|
+
PreactDOMAttributes;
|
|
8
|
+
|
|
9
|
+
interface ButtonProps {
|
|
10
|
+
Icon?: (props: SVGProps<SVGSVGElement>) => preact.JSX.Element;
|
|
11
|
+
theme?: "primary" | "secondary";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function RvfButton({
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
disabled,
|
|
18
|
+
Icon,
|
|
19
|
+
onClick,
|
|
20
|
+
theme,
|
|
21
|
+
}: RvfButtonProps) {
|
|
22
|
+
const baseClasses =
|
|
23
|
+
"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";
|
|
24
|
+
const themeClasses =
|
|
25
|
+
theme === "primary"
|
|
26
|
+
? "border-red bg-red text-white"
|
|
27
|
+
: "border-grey bg-white text-grey";
|
|
28
|
+
|
|
29
|
+
const classes = `${baseClasses} ${themeClasses} ${className || ""}`;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button className={classes} disabled={disabled} onClick={onClick}>
|
|
33
|
+
{children || <Icon height={"100%"} width={"100%"} />}
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default memo(RvfButton);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfButton";
|
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
RealtimeTrainId,
|
|
11
11
|
} from "mobility-toolbox-js/types";
|
|
12
12
|
import { Map as OlMap } from "ol";
|
|
13
|
-
import {
|
|
14
|
-
import { fromLonLat } from "ol/proj";
|
|
13
|
+
import { transformExtent } from "ol/proj";
|
|
15
14
|
import { memo } from "preact/compat";
|
|
16
15
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
17
16
|
|
|
@@ -24,6 +23,8 @@ import NotificationLayer from "../NotificationLayer";
|
|
|
24
23
|
import Overlay from "../Overlay";
|
|
25
24
|
import RealtimeLayer from "../RealtimeLayer";
|
|
26
25
|
import RouteSchedule from "../RouteSchedule";
|
|
26
|
+
import RvfSharedMobilityLayer from "../RvfSharedMobilityLayer";
|
|
27
|
+
import RvfZoomButtons from "../RvfZoomButtons";
|
|
27
28
|
import ScaleLine from "../ScaleLine";
|
|
28
29
|
import Search from "../Search";
|
|
29
30
|
import SingleClickListener from "../SingleClickListener/SingleClickListener";
|
|
@@ -42,15 +43,22 @@ import style from "./index.css";
|
|
|
42
43
|
|
|
43
44
|
export type RvfMobilityMapProps = {} & MobilityMapProps;
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
-
const
|
|
46
|
+
const rvfExtent = [7.5, 47.7, 8.45, 48.4];
|
|
47
|
+
const rvfExtentTransformed = transformExtent(
|
|
48
|
+
rvfExtent,
|
|
49
|
+
"EPSG:4326",
|
|
50
|
+
"EPSG:3857",
|
|
51
|
+
);
|
|
52
|
+
const bbox = rvfExtentTransformed.join(",");
|
|
47
53
|
|
|
48
54
|
function RvfMobilityMap({
|
|
49
55
|
apikey = "5cc87b12d7c5370001c1d655820abcc37dfd4d968d7bab5b2a74a935",
|
|
50
56
|
baselayer = "de.rvf",
|
|
51
|
-
center =
|
|
57
|
+
center = null,
|
|
58
|
+
extent = bbox,
|
|
52
59
|
geolocation = "true",
|
|
53
60
|
mapsurl = "https://maps.geops.io",
|
|
61
|
+
maxextent = bbox,
|
|
54
62
|
maxzoom = null,
|
|
55
63
|
minzoom = null,
|
|
56
64
|
mots = null,
|
|
@@ -64,7 +72,7 @@ function RvfMobilityMap({
|
|
|
64
72
|
search = "false",
|
|
65
73
|
stopsurl = "https://api.geops.io/stops/v1/",
|
|
66
74
|
tenant = null,
|
|
67
|
-
zoom =
|
|
75
|
+
zoom = null,
|
|
68
76
|
}: RvfMobilityMapProps) {
|
|
69
77
|
const [baseLayer, setBaseLayer] = useState<MaplibreLayer>();
|
|
70
78
|
const [isFollowing, setIsFollowing] = useState(false);
|
|
@@ -88,11 +96,13 @@ function RvfMobilityMap({
|
|
|
88
96
|
baselayer,
|
|
89
97
|
baseLayer,
|
|
90
98
|
center,
|
|
99
|
+
extent,
|
|
91
100
|
geolocation,
|
|
92
101
|
isFollowing,
|
|
93
102
|
isTracking,
|
|
94
103
|
map,
|
|
95
104
|
mapsurl,
|
|
105
|
+
maxextent,
|
|
96
106
|
maxzoom,
|
|
97
107
|
minzoom,
|
|
98
108
|
mots,
|
|
@@ -127,11 +137,13 @@ function RvfMobilityMap({
|
|
|
127
137
|
baselayer,
|
|
128
138
|
baseLayer,
|
|
129
139
|
center,
|
|
140
|
+
extent,
|
|
130
141
|
geolocation,
|
|
131
142
|
isFollowing,
|
|
132
143
|
isTracking,
|
|
133
144
|
map,
|
|
134
145
|
mapsurl,
|
|
146
|
+
maxextent,
|
|
135
147
|
maxzoom,
|
|
136
148
|
minzoom,
|
|
137
149
|
mots,
|
|
@@ -157,8 +169,10 @@ function RvfMobilityMap({
|
|
|
157
169
|
new MobilityEvent<RvfMobilityMapProps>("mwc:attribute", {
|
|
158
170
|
baselayer,
|
|
159
171
|
center: x && y ? `${x},${y}` : center,
|
|
172
|
+
extent,
|
|
160
173
|
geolocation,
|
|
161
174
|
mapsurl,
|
|
175
|
+
maxextent,
|
|
162
176
|
maxzoom,
|
|
163
177
|
minzoom,
|
|
164
178
|
mots,
|
|
@@ -193,6 +207,8 @@ function RvfMobilityMap({
|
|
|
193
207
|
x,
|
|
194
208
|
y,
|
|
195
209
|
z,
|
|
210
|
+
extent,
|
|
211
|
+
maxextent,
|
|
196
212
|
]);
|
|
197
213
|
|
|
198
214
|
return (
|
|
@@ -208,6 +224,33 @@ function RvfMobilityMap({
|
|
|
208
224
|
{realtime === "true" && <RealtimeLayer />}
|
|
209
225
|
{tenant && <StationsLayer />}
|
|
210
226
|
{notification === "true" && <NotificationLayer />}
|
|
227
|
+
<RvfSharedMobilityLayer
|
|
228
|
+
color="red"
|
|
229
|
+
name="MobiData-BW:sharing_stations_bicycle"
|
|
230
|
+
url="https://api.mobidata-bw.de/geoserver/MobiData-BW/sharing_stations_bicycle/ows"
|
|
231
|
+
/>
|
|
232
|
+
<RvfSharedMobilityLayer
|
|
233
|
+
color="blue"
|
|
234
|
+
name="MobiData-BW:sharing_stations_cargo_bicycle"
|
|
235
|
+
url="https://api.mobidata-bw.de/geoserver/MobiData-BW/sharing_stations_cargo_bicycle/ows"
|
|
236
|
+
/>
|
|
237
|
+
<RvfSharedMobilityLayer
|
|
238
|
+
color="green"
|
|
239
|
+
name="MobiData-BW:sharing_stations_car"
|
|
240
|
+
url="https://api.mobidata-bw.de/geoserver/MobiData-BW/sharing_stations_car/ows"
|
|
241
|
+
/>
|
|
242
|
+
<RvfSharedMobilityLayer
|
|
243
|
+
color="orange"
|
|
244
|
+
name="MobiData-BW:sharing_stations_scooters_standing"
|
|
245
|
+
url="https://api.mobidata-bw.de/geoserver/MobiData-BW/sharing_stations_scooters_standing/ows"
|
|
246
|
+
/>
|
|
247
|
+
<RvfSharedMobilityLayer
|
|
248
|
+
color="orange"
|
|
249
|
+
minZoom={14.999}
|
|
250
|
+
name="MobiData-BW:sharing_vehicles"
|
|
251
|
+
url="https://api.mobidata-bw.de/geoserver/MobiData-BW/sharing_vehicles/ows"
|
|
252
|
+
/>
|
|
253
|
+
|
|
211
254
|
<div className="absolute inset-x-2 bottom-2 z-10 flex items-end justify-between gap-2 text-[10px]">
|
|
212
255
|
<ScaleLine className="bg-slate-50/70" />
|
|
213
256
|
<Copyright className="bg-slate-50/70" />
|
|
@@ -220,6 +263,9 @@ function RvfMobilityMap({
|
|
|
220
263
|
<Search />
|
|
221
264
|
</div>
|
|
222
265
|
)}
|
|
266
|
+
<div className="absolute bottom-10 right-2 z-10 flex flex-col justify-between gap-2">
|
|
267
|
+
<RvfZoomButtons />
|
|
268
|
+
</div>
|
|
223
269
|
</Map>
|
|
224
270
|
|
|
225
271
|
<Overlay
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getUid } from "ol";
|
|
2
|
+
import { GeoJSON } from "ol/format";
|
|
3
|
+
import VectorLayer from "ol/layer/Vector";
|
|
4
|
+
import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
|
|
5
|
+
import { Vector } from "ol/source";
|
|
6
|
+
import { Circle, Fill, Stroke, Style, Text } from "ol/style";
|
|
7
|
+
import { useEffect, useMemo } from "preact/hooks";
|
|
8
|
+
|
|
9
|
+
import useMapContext from "../utils/hooks/useMapContext";
|
|
10
|
+
|
|
11
|
+
export default function RvfSharedMobilityLayer({
|
|
12
|
+
color,
|
|
13
|
+
minZoom = 16.999,
|
|
14
|
+
name,
|
|
15
|
+
url,
|
|
16
|
+
}: {
|
|
17
|
+
color: string;
|
|
18
|
+
minZoom?: number;
|
|
19
|
+
name: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}) {
|
|
22
|
+
const { map } = useMapContext();
|
|
23
|
+
|
|
24
|
+
const layer = useMemo(() => {
|
|
25
|
+
if (!name || !url) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const source = new Vector({
|
|
29
|
+
format: new GeoJSON(),
|
|
30
|
+
strategy: bboxStrategy,
|
|
31
|
+
url: function (extent) {
|
|
32
|
+
return (
|
|
33
|
+
url +
|
|
34
|
+
"?service=WFS&" +
|
|
35
|
+
"version=1.1.0&request=GetFeature&typename=" +
|
|
36
|
+
name +
|
|
37
|
+
"&" +
|
|
38
|
+
"outputFormat=application/json&srsname=EPSG:3857&" +
|
|
39
|
+
"bbox=" +
|
|
40
|
+
extent.join(",") +
|
|
41
|
+
",EPSG:3857"
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const style = new Style({
|
|
46
|
+
image: new Circle({
|
|
47
|
+
declutterMode: "declutter",
|
|
48
|
+
fill: new Fill({
|
|
49
|
+
color: "white",
|
|
50
|
+
}),
|
|
51
|
+
radius: 10,
|
|
52
|
+
stroke: new Stroke({
|
|
53
|
+
color: color,
|
|
54
|
+
width: 2,
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
text: new Text({
|
|
58
|
+
declutterMode: "declutter",
|
|
59
|
+
|
|
60
|
+
fill: new Fill({
|
|
61
|
+
color: color,
|
|
62
|
+
}),
|
|
63
|
+
font: "bold 12px inherit",
|
|
64
|
+
// text: ["to-string", ["get", "num_vehicles_available"]],
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const styleFunction = (feature) => {
|
|
69
|
+
const clone = style.clone();
|
|
70
|
+
clone
|
|
71
|
+
.getText()
|
|
72
|
+
.setText(feature.get("num_vehicles_available")?.toString());
|
|
73
|
+
const isFreeFloat = !feature.get("num_vehicles_available")?.toString();
|
|
74
|
+
if (isFreeFloat) {
|
|
75
|
+
(clone.getImage() as Circle).setRadius(6);
|
|
76
|
+
}
|
|
77
|
+
clone.setZIndex(parseInt(getUid(feature), 10));
|
|
78
|
+
return clone;
|
|
79
|
+
};
|
|
80
|
+
const layer = new VectorLayer({
|
|
81
|
+
// declutter: true,
|
|
82
|
+
minZoom: minZoom,
|
|
83
|
+
source: source,
|
|
84
|
+
// style: (feature) => {
|
|
85
|
+
// console.log("feature", feature);
|
|
86
|
+
// style
|
|
87
|
+
// .getText()
|
|
88
|
+
// .setText(feature.get("num_vehicles_available").toString());
|
|
89
|
+
// return style;
|
|
90
|
+
// },
|
|
91
|
+
|
|
92
|
+
// style: {
|
|
93
|
+
// "circle-fill-color": "white",
|
|
94
|
+
// "circle-radius": 15,
|
|
95
|
+
// "circle-stroke-color": color,
|
|
96
|
+
// "circle-stroke-width": 2,
|
|
97
|
+
// // "fill-color": "rgba(100,100,100,0.25)",
|
|
98
|
+
// // "stroke-color": "white",
|
|
99
|
+
// // "stroke-width": 0.75,
|
|
100
|
+
// // "text-background-fill-color": "black",
|
|
101
|
+
// "text-fill-color": color,
|
|
102
|
+
// "text-font": "bold 12px sans-serif",
|
|
103
|
+
// "text-stroke-color": color,
|
|
104
|
+
// "text-value": ["to-string", ["get", "num_vehicles_available"]],
|
|
105
|
+
// },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
source.on("addfeature", function (event) {
|
|
109
|
+
const feature = event.feature;
|
|
110
|
+
feature.setStyle(styleFunction);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return layer;
|
|
114
|
+
}, [color, minZoom, name, url]);
|
|
115
|
+
|
|
116
|
+
// Reload features every minutes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const interval = window.setInterval(() => {
|
|
119
|
+
// @ts-expect-error - private property
|
|
120
|
+
layer.getSource().loadedExtentsRtree_.clear();
|
|
121
|
+
// vectorSource.clear(true);
|
|
122
|
+
layer.getSource().changed();
|
|
123
|
+
}, 60000);
|
|
124
|
+
return () => {
|
|
125
|
+
window.clearInterval(interval);
|
|
126
|
+
};
|
|
127
|
+
}, [layer]);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!map || !layer) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
map.on("moveend", () => {
|
|
134
|
+
console.log("ZOOM", map.getView().getZoom());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// map.addLayer(layer);
|
|
138
|
+
layer.setMap(map);
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
// map.removeLayer(layer);
|
|
142
|
+
layer.setMap(null);
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfSharedMobilityLayer";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { memo } from "preact/compat";
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
import Minus from "../icons/Minus";
|
|
5
|
+
import Plus from "../icons/Plus";
|
|
6
|
+
import RvfButton from "../RvfButton";
|
|
7
|
+
import useMapContext from "../utils/hooks/useMapContext";
|
|
8
|
+
|
|
9
|
+
function RvfZoomButtons() {
|
|
10
|
+
const { map } = useMapContext();
|
|
11
|
+
const [isZoomInDisabled, setIsZoomInDisabled] = useState(false);
|
|
12
|
+
const [isZoomOutDisabled, setIsZoomOutDisabled] = useState(false);
|
|
13
|
+
|
|
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);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
view.setZoom(zoom + 1);
|
|
26
|
+
|
|
27
|
+
if (!minzoom || view.getZoom() > Number(minzoom)) {
|
|
28
|
+
setIsZoomOutDisabled(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleZoomOut = () => {
|
|
33
|
+
const view = map.getView();
|
|
34
|
+
const zoom = view.getZoom();
|
|
35
|
+
const maxzoom = view.getMaxZoom();
|
|
36
|
+
const minzoom = view.getMinZoom();
|
|
37
|
+
|
|
38
|
+
if (minzoom && zoom === Number(minzoom)) {
|
|
39
|
+
setIsZoomOutDisabled(true);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
view.setZoom(zoom - 1);
|
|
44
|
+
|
|
45
|
+
if (!maxzoom || view.getZoom() < Number(maxzoom)) {
|
|
46
|
+
setIsZoomInDisabled(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<RvfButton
|
|
53
|
+
disabled={isZoomInDisabled}
|
|
54
|
+
Icon={Plus}
|
|
55
|
+
onClick={handleZoomIn}
|
|
56
|
+
/>
|
|
57
|
+
<RvfButton
|
|
58
|
+
disabled={isZoomOutDisabled}
|
|
59
|
+
Icon={Minus}
|
|
60
|
+
onClick={handleZoomOut}
|
|
61
|
+
/>
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default memo(RvfZoomButtons);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfZoomButtons";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SVGProps } from "preact/compat";
|
|
2
|
+
|
|
3
|
+
function Geolocation(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
focusable="false"
|
|
8
|
+
height="1.5em"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
strokeWidth="0"
|
|
11
|
+
viewBox="0 0 512 512"
|
|
12
|
+
width="1.5em"
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<path d="M256 56c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m0-48C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 168c-44.183 0-80 35.817-80 80s35.817 80 80 80 80-35.817 80-80-35.817-80-80-80z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default Geolocation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Geolocation";
|