@geops/rvf-mobility-web-component 0.1.32 → 0.1.34
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/CHANGELOG.md +22 -0
- package/index.html +4 -1
- package/index.js +186 -181
- package/package.json +1 -1
- package/src/NotificationLayer/NotificationLayer.tsx +84 -57
- package/src/NotificationLayer/notificationUtils.ts +191 -23
- package/src/RouteIcon/RouteIcon.tsx +6 -3
- package/src/RouteStopProgress/RouteStopProgress.tsx +12 -7
- package/src/RvfFeatureDetails/RvfLineNetworkDetails/RvfLineNetworkDetails.tsx +158 -23
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +1 -1
- package/src/icons/warning-banner.png +0 -0
- package/src/icons/warning.png +0 -0
- package/src/utils/addSourceAndLayers.ts +24 -2
- package/src/utils/getFeatureInformationTitle.ts +3 -2
- package/src/utils/hooks/useUpdatePermalink.tsx +18 -7
- package/src/utils/hooks/useZoom.tsx +14 -2
|
@@ -1,9 +1,35 @@
|
|
|
1
|
+
import { RealtimeLine } from "mobility-toolbox-js/types";
|
|
1
2
|
import { Feature } from "ol";
|
|
2
3
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
3
4
|
|
|
5
|
+
import ArrowDown from "../../icons/ArrowDown";
|
|
6
|
+
import ArrowUp from "../../icons/ArrowUp";
|
|
7
|
+
import RouteIcon from "../../RouteIcon";
|
|
8
|
+
import RouteStopProgress from "../../RouteStopProgress";
|
|
4
9
|
import useMapContext from "../../utils/hooks/useMapContext";
|
|
10
|
+
import { RouteStopContext } from "../../utils/hooks/useRouteStop";
|
|
5
11
|
|
|
6
12
|
let cacheLineInfosById = null;
|
|
13
|
+
let cacheStopInfosById = null;
|
|
14
|
+
|
|
15
|
+
interface LineInfo {
|
|
16
|
+
color: string;
|
|
17
|
+
external_id: string;
|
|
18
|
+
id: string;
|
|
19
|
+
long_name: string;
|
|
20
|
+
mot: string;
|
|
21
|
+
operator_name: string;
|
|
22
|
+
short_name: string;
|
|
23
|
+
text_color: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StopInfo {
|
|
27
|
+
external_id: string;
|
|
28
|
+
importance: number;
|
|
29
|
+
long_name: string;
|
|
30
|
+
short_name: string;
|
|
31
|
+
visibility_level: number;
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
function RvfLineNetworkDetails({
|
|
9
35
|
feature,
|
|
@@ -13,7 +39,11 @@ function RvfLineNetworkDetails({
|
|
|
13
39
|
features: Feature[];
|
|
14
40
|
}) {
|
|
15
41
|
const { apikey, mapsurl } = useMapContext();
|
|
16
|
-
const [lineInfos, setLineInfos] = useState(null);
|
|
42
|
+
const [lineInfos, setLineInfos] = useState<LineInfo[]>(null);
|
|
43
|
+
const [stopInfos, setStopInfos] = useState<StopInfo[]>(null);
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
+
const [stopInfosOpenId, setStopInfosOpenId] = useState<string>(null);
|
|
46
|
+
|
|
17
47
|
useEffect(() => {
|
|
18
48
|
const fetchInfos = async () => {
|
|
19
49
|
if (!cacheLineInfosById) {
|
|
@@ -22,15 +52,16 @@ function RvfLineNetworkDetails({
|
|
|
22
52
|
);
|
|
23
53
|
const data = await response.json();
|
|
24
54
|
cacheLineInfosById = data["geops.lnp.lines"];
|
|
55
|
+
cacheStopInfosById = data["geops.lnp.stops"];
|
|
25
56
|
}
|
|
26
57
|
setLineInfos(cacheLineInfosById);
|
|
58
|
+
setStopInfos(cacheStopInfosById);
|
|
27
59
|
};
|
|
28
60
|
fetchInfos();
|
|
29
61
|
}, [apikey, mapsurl]);
|
|
30
62
|
|
|
31
|
-
const
|
|
63
|
+
const lineInfosByOperator: Record<string, LineInfo[]> = useMemo(() => {
|
|
32
64
|
const byOperators = {};
|
|
33
|
-
|
|
34
65
|
[
|
|
35
66
|
...new Set(
|
|
36
67
|
features.map((f) => {
|
|
@@ -46,51 +77,155 @@ function RvfLineNetworkDetails({
|
|
|
46
77
|
if (!byOperators[operatorName]) {
|
|
47
78
|
byOperators[operatorName] = [];
|
|
48
79
|
}
|
|
80
|
+
lineInfos[id].id = id;
|
|
49
81
|
byOperators[operatorName].push(lineInfos[id]);
|
|
50
82
|
});
|
|
51
83
|
|
|
52
84
|
return byOperators;
|
|
53
85
|
}, [features, lineInfos]);
|
|
54
86
|
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
|
+
const stopInfoIdsByLineId: Record<string, string[]> = useMemo(() => {
|
|
89
|
+
const byLineId = {};
|
|
90
|
+
features.forEach((f) => {
|
|
91
|
+
const lineId = f.get("original_line_id");
|
|
92
|
+
if (lineId && !byLineId[lineId]) {
|
|
93
|
+
try {
|
|
94
|
+
byLineId[lineId] = JSON.parse(f.get("stop_ids"));
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.log(e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return byLineId;
|
|
102
|
+
}, [features]);
|
|
103
|
+
|
|
55
104
|
if (!feature || !lineInfos) {
|
|
56
105
|
return null;
|
|
57
106
|
}
|
|
58
107
|
|
|
59
108
|
return (
|
|
60
109
|
<div className="flex flex-col gap-4">
|
|
61
|
-
{Object.entries(
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
<div
|
|
65
|
-
|
|
66
|
-
|
|
110
|
+
{Object.entries(lineInfosByOperator).map(([operatorName, linesInfos]) => {
|
|
111
|
+
return (
|
|
112
|
+
<div className={"flex flex-col gap-2"} key={operatorName}>
|
|
113
|
+
<div>{operatorName}</div>
|
|
114
|
+
{linesInfos
|
|
115
|
+
.sort((a, b) => {
|
|
116
|
+
return a.short_name?.localeCompare(b.short_name);
|
|
117
|
+
})
|
|
118
|
+
.map((lineInfo) => {
|
|
67
119
|
const {
|
|
120
|
+
color: backgroundColor,
|
|
121
|
+
id,
|
|
68
122
|
// color,
|
|
69
123
|
// external_id,
|
|
70
|
-
long_name
|
|
124
|
+
long_name,
|
|
125
|
+
mot,
|
|
71
126
|
short_name: shortName,
|
|
72
|
-
|
|
127
|
+
text_color: textColor,
|
|
73
128
|
} = lineInfo;
|
|
129
|
+
let longName = long_name;
|
|
130
|
+
|
|
131
|
+
let stops = null;
|
|
132
|
+
//stopInfoIdsByLineId?.[id] || null;
|
|
133
|
+
if (!stops?.length) {
|
|
134
|
+
stops = null;
|
|
135
|
+
}
|
|
136
|
+
console.log("stops", stops, longName);
|
|
137
|
+
if (!longName && stops) {
|
|
138
|
+
const names = stops.map((stopId) => {
|
|
139
|
+
return stopInfos[stopId].short_name;
|
|
140
|
+
});
|
|
141
|
+
console.log("stops", names);
|
|
142
|
+
|
|
143
|
+
longName = [
|
|
144
|
+
...new Set([names[0], names[names.length - 1]]),
|
|
145
|
+
].join(" - ");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build a line object
|
|
149
|
+
const line: { type: string } & RealtimeLine = {
|
|
150
|
+
color: null,
|
|
151
|
+
id: null,
|
|
152
|
+
name: shortName,
|
|
153
|
+
stroke: null,
|
|
154
|
+
text_color: null,
|
|
155
|
+
type: mot,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (textColor) {
|
|
159
|
+
line.text_color = "#" + textColor;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (backgroundColor) {
|
|
163
|
+
line.color = "#" + backgroundColor;
|
|
164
|
+
}
|
|
74
165
|
|
|
75
166
|
return (
|
|
76
|
-
<div
|
|
77
|
-
<div
|
|
167
|
+
<div key={shortName}>
|
|
168
|
+
<div
|
|
169
|
+
className={
|
|
170
|
+
"flex w-full items-center justify-between gap-2"
|
|
171
|
+
}
|
|
172
|
+
// onClick={() => {
|
|
173
|
+
// setStopInfosOpenId(stopInfosOpenId === id ? null : id);
|
|
174
|
+
// }}
|
|
175
|
+
>
|
|
176
|
+
<div>
|
|
177
|
+
<RouteIcon line={line}></RouteIcon>
|
|
178
|
+
</div>
|
|
179
|
+
{!!longName && (
|
|
180
|
+
<div className={"flex-1 text-left"}>{longName}</div>
|
|
181
|
+
)}
|
|
182
|
+
{!!stops && (
|
|
183
|
+
<button className={"shrink-0"}>
|
|
184
|
+
{stopInfosOpenId === id ? <ArrowUp /> : <ArrowDown />}
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
{!!stops && (
|
|
78
189
|
<div
|
|
79
|
-
className={
|
|
80
|
-
"rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
|
|
81
|
-
}
|
|
190
|
+
className={`${stopInfosOpenId === id ? "" : "hidden"}`}
|
|
82
191
|
>
|
|
83
|
-
{
|
|
192
|
+
{stops?.map((stopId, index, arr) => {
|
|
193
|
+
const stop = stopInfos[stopId];
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className={"flex items-center gap-2"}
|
|
197
|
+
key={stopId}
|
|
198
|
+
>
|
|
199
|
+
<RouteStopContext.Provider
|
|
200
|
+
value={{
|
|
201
|
+
index,
|
|
202
|
+
status: {
|
|
203
|
+
isFirst: !index,
|
|
204
|
+
isLast: index === arr.length - 1,
|
|
205
|
+
isLeft: false,
|
|
206
|
+
isPassed: false,
|
|
207
|
+
progress: !index ? 50 : 0,
|
|
208
|
+
},
|
|
209
|
+
stop,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<RouteStopProgress
|
|
213
|
+
className="relative flex size-8 shrink-0 items-center justify-center"
|
|
214
|
+
lineColor={line.color}
|
|
215
|
+
/>
|
|
216
|
+
<div>{stop.short_name}</div>
|
|
217
|
+
</RouteStopContext.Provider>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
84
221
|
</div>
|
|
85
|
-
|
|
86
|
-
<div>{longName}</div>
|
|
222
|
+
)}
|
|
87
223
|
</div>
|
|
88
224
|
);
|
|
89
225
|
})}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
94
229
|
</div>
|
|
95
230
|
);
|
|
96
231
|
}
|
|
@@ -246,7 +246,7 @@ function RvfMobilityMap({
|
|
|
246
246
|
|
|
247
247
|
// TODO: this should be removed. The parent application should be responsible to do this
|
|
248
248
|
// or we should find something that fit more usecases
|
|
249
|
-
useUpdatePermalink(map, permalink === "true");
|
|
249
|
+
useUpdatePermalink(map, permalink === "true", eventNodeRef);
|
|
250
250
|
|
|
251
251
|
const mapContextValue = useMemo(() => {
|
|
252
252
|
return {
|
|
Binary file
|
|
Binary file
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import warningbanner from "../icons/warning-banner.png";
|
|
2
|
+
import warning from "../icons/warning.png";
|
|
3
|
+
|
|
4
|
+
const icons = {
|
|
5
|
+
warning: warning,
|
|
6
|
+
warningbanner: warningbanner,
|
|
7
|
+
};
|
|
8
|
+
|
|
1
9
|
// Add a source and styleLayer using the id in parameter.
|
|
2
10
|
const addSourceAndLayers = (
|
|
3
11
|
mapboxLayer,
|
|
@@ -20,13 +28,27 @@ const addSourceAndLayers = (
|
|
|
20
28
|
}
|
|
21
29
|
const { mbMap } = mapboxLayer;
|
|
22
30
|
|
|
31
|
+
Object.keys(icons).forEach((icon) => {
|
|
32
|
+
if (!mbMap.getImage(icon)) {
|
|
33
|
+
mbMap.loadImage(icons[icon]).then((image) => {
|
|
34
|
+
if (!mbMap.getImage(icon)) {
|
|
35
|
+
mbMap.addImage(icon, image.data);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
23
41
|
// Update source
|
|
24
42
|
if (sourceId && sourceData) {
|
|
25
43
|
const source = mbMap.getSource(sourceId);
|
|
26
44
|
if (source) {
|
|
27
|
-
source.setData(sourceData
|
|
45
|
+
source.setData(sourceData);
|
|
28
46
|
} else {
|
|
29
|
-
mbMap.addSource(sourceId,
|
|
47
|
+
mbMap.addSource(sourceId, {
|
|
48
|
+
data: sourceData,
|
|
49
|
+
generateId: true,
|
|
50
|
+
type: "geojson",
|
|
51
|
+
});
|
|
30
52
|
}
|
|
31
53
|
}
|
|
32
54
|
|
|
@@ -15,9 +15,10 @@ const getFeatureInformationTitle = (feature: Feature) => {
|
|
|
15
15
|
category,
|
|
16
16
|
disruption_type: disruptionType,
|
|
17
17
|
feed_id: feedId,
|
|
18
|
-
|
|
18
|
+
line_id: lineId,
|
|
19
19
|
tickets,
|
|
20
20
|
} = selectedFeature.getProperties();
|
|
21
|
+
console.log(selectedFeature.getProperties());
|
|
21
22
|
|
|
22
23
|
if (disruptionType) {
|
|
23
24
|
return "MOCO Meldung";
|
|
@@ -29,7 +30,7 @@ const getFeatureInformationTitle = (feature: Feature) => {
|
|
|
29
30
|
return TITLE_BY_FEED_ID[feedId] || defaultTitle;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
if (
|
|
33
|
+
if (lineId) {
|
|
33
34
|
return RVF_LAYERS_TITLES.liniennetz;
|
|
34
35
|
}
|
|
35
36
|
if (tickets) {
|
|
@@ -2,17 +2,22 @@ import debounce from "lodash.debounce";
|
|
|
2
2
|
import { Map } from "ol";
|
|
3
3
|
import { EventsKey } from "ol/events";
|
|
4
4
|
import { unByKey } from "ol/Observable";
|
|
5
|
-
import { useEffect } from "preact/hooks";
|
|
5
|
+
import { MutableRef, useEffect } from "preact/hooks";
|
|
6
6
|
|
|
7
7
|
import { LAYER_PROP_IS_EXPORTING } from "../constants";
|
|
8
8
|
import getLayersAsFlatArray from "../getLayersAsFlatArray";
|
|
9
9
|
import getPermalinkParameters from "../getPermalinkParameters";
|
|
10
|
+
import MobilityEvent from "../MobilityEvent";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* This hook only update parameters in the url, it does not apply the url parameters.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
const useUpdatePermalink = (
|
|
16
|
+
const useUpdatePermalink = (
|
|
17
|
+
map: Map,
|
|
18
|
+
permalink: boolean,
|
|
19
|
+
eventNodeRef: MutableRef<HTMLDivElement>,
|
|
20
|
+
) => {
|
|
16
21
|
useEffect(() => {
|
|
17
22
|
let moveEndKey: EventsKey;
|
|
18
23
|
let loadEndKey: EventsKey;
|
|
@@ -23,17 +28,17 @@ const useUpdatePermalink = (map: Map, permalink: boolean) => {
|
|
|
23
28
|
|
|
24
29
|
// Update x,y,z in URL on moveend
|
|
25
30
|
moveEndKey = map?.on("moveend", (evt) => {
|
|
26
|
-
updatePermalinkDebounced(evt.map);
|
|
31
|
+
updatePermalinkDebounced(evt.map, eventNodeRef);
|
|
27
32
|
});
|
|
28
33
|
|
|
29
34
|
// Update layers in URL on change:visible event
|
|
30
35
|
loadEndKey = map.once("loadend", (evt) => {
|
|
31
|
-
updatePermalinkDebounced(evt.map);
|
|
36
|
+
updatePermalinkDebounced(evt.map, eventNodeRef);
|
|
32
37
|
changeVisibleKeys = getLayersAsFlatArray(
|
|
33
38
|
evt.map.getLayers().getArray(),
|
|
34
39
|
).map((layer) => {
|
|
35
40
|
return layer.on("change:visible", () => {
|
|
36
|
-
updatePermalinkDebounced(evt.map);
|
|
41
|
+
updatePermalinkDebounced(evt.map, eventNodeRef);
|
|
37
42
|
});
|
|
38
43
|
});
|
|
39
44
|
});
|
|
@@ -44,11 +49,14 @@ const useUpdatePermalink = (map: Map, permalink: boolean) => {
|
|
|
44
49
|
unByKey(loadEndKey);
|
|
45
50
|
unByKey(changeVisibleKeys);
|
|
46
51
|
};
|
|
47
|
-
}, [map, permalink]);
|
|
52
|
+
}, [map, permalink, eventNodeRef]);
|
|
48
53
|
return null;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
const updatePermalink = (
|
|
56
|
+
const updatePermalink = (
|
|
57
|
+
map: Map,
|
|
58
|
+
eventNodeRef: MutableRef<HTMLDivElement>,
|
|
59
|
+
) => {
|
|
52
60
|
// No update when exporting
|
|
53
61
|
if (map.get(LAYER_PROP_IS_EXPORTING)) {
|
|
54
62
|
return;
|
|
@@ -57,6 +65,9 @@ const updatePermalink = (map: Map) => {
|
|
|
57
65
|
const urlParams = getPermalinkParameters(map, currentUrlParams);
|
|
58
66
|
urlParams.set("permalink", "true");
|
|
59
67
|
window.history.replaceState(null, null, `?${urlParams.toString()}`);
|
|
68
|
+
eventNodeRef?.current?.dispatchEvent(
|
|
69
|
+
new MobilityEvent<string>("mwc:permalink", window.location.href),
|
|
70
|
+
);
|
|
60
71
|
};
|
|
61
72
|
|
|
62
73
|
const updatePermalinkDebounced = debounce(updatePermalink, 1000);
|
|
@@ -15,15 +15,27 @@ const useZoom = () => {
|
|
|
15
15
|
if (view) {
|
|
16
16
|
setZoom(view.getZoom());
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
const onZoomChange = (evt) => {
|
|
19
19
|
clearTimeout(timeout);
|
|
20
20
|
timeout = setTimeout(() => {
|
|
21
|
-
return setZoom(
|
|
21
|
+
return setZoom(evt.target.getZoom());
|
|
22
22
|
}, 150);
|
|
23
|
+
};
|
|
24
|
+
let zoomListener = view.on("change:resolution", onZoomChange);
|
|
25
|
+
|
|
26
|
+
const key = map.on("change:view", () => {
|
|
27
|
+
const view = map.getView();
|
|
28
|
+
unByKey(zoomListener);
|
|
29
|
+
if (view) {
|
|
30
|
+
setZoom(view.getZoom());
|
|
31
|
+
}
|
|
32
|
+
zoomListener = view.on("change:resolution", onZoomChange);
|
|
23
33
|
});
|
|
34
|
+
|
|
24
35
|
return () => {
|
|
25
36
|
clearTimeout(timeout);
|
|
26
37
|
unByKey(zoomListener);
|
|
38
|
+
unByKey(key);
|
|
27
39
|
};
|
|
28
40
|
}, [map]);
|
|
29
41
|
return zoom;
|