@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/package.json CHANGED
@@ -2,7 +2,7 @@
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.32",
5
+ "version": "0.1.34",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
@@ -1,3 +1,4 @@
1
+ import { FeatureCollection } from "geojson";
1
2
  import { MaplibreStyleLayer } from "mobility-toolbox-js/ol";
2
3
  import { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreStyleLayer";
3
4
  import { useEffect, useMemo, useState } from "preact/hooks";
@@ -6,6 +7,7 @@ import useMapContext from "../utils/hooks/useMapContext";
6
7
  import useZoom from "../utils/hooks/useZoom";
7
8
  import {
8
9
  addNotificationsLayers,
10
+ getCurrentGraph,
9
11
  getNotificationsWithStatus,
10
12
  parsePreviewNotification,
11
13
  } from "./notificationUtils";
@@ -16,28 +18,18 @@ interface Metadata {
16
18
  graphs?: Graphs;
17
19
  }
18
20
 
19
- const useNotifications = () => {
20
- const {
21
- baselayer,
22
- baseLayer,
23
- notificationat,
24
- notificationbeforelayerid,
25
- notificationurl,
26
- } = useMapContext();
27
- const zoom = useZoom();
28
- const [notifications, setNotifications] = useState([]);
29
- const [previewNotification, setPreviewNotification] = useState(null);
30
- const [shouldAddPreviewNotifications, setShouldAddPreviewNotifications] =
31
- useState<boolean>(true);
21
+ const useStyleName = (): string => {
22
+ const { baselayer } = useMapContext();
23
+ return baselayer;
24
+ };
32
25
 
33
- const [style, setStyle] = useState<string>();
26
+ const useStyleMetadata = (): Metadata => {
27
+ const { baseLayer } = useMapContext();
34
28
  const [styleMetadata, setStyleMetadata] = useState<Metadata>();
35
-
36
29
  useEffect(() => {
37
30
  if (!baseLayer) {
38
31
  return;
39
32
  }
40
- setStyle(baselayer);
41
33
  if (!baseLayer.loaded) {
42
34
  // @ts-expect-error bad type definition
43
35
  baseLayer.once("load", () => {
@@ -46,24 +38,36 @@ const useNotifications = () => {
46
38
  } else {
47
39
  setStyleMetadata(baseLayer.mapLibreMap?.getStyle()?.metadata);
48
40
  }
49
- }, [baseLayer, baselayer]);
41
+ }, [baseLayer]);
42
+ return styleMetadata;
43
+ };
44
+
45
+ const useGraphMapping = (
46
+ defaultMapping: Record<number, string> = { 1: "osm" },
47
+ ): Record<number, string> => {
48
+ const styleMetadata = useStyleMetadata();
49
+ const graphMapping = useMemo(() => {
50
+ return styleMetadata?.graphs || defaultMapping;
51
+ }, [defaultMapping, styleMetadata?.graphs]);
52
+ return graphMapping;
53
+ };
54
+
55
+ const useNotifications = (): FeatureCollection => {
56
+ const { notificationat, notificationurl } = useMapContext();
57
+ const [notifications, setNotifications] = useState([]);
58
+ const [previewNotification, setPreviewNotification] = useState(null);
59
+ const [shouldAddPreviewNotifications, setShouldAddPreviewNotifications] =
60
+ useState<boolean>(true);
61
+
62
+ const styleName = useStyleName();
63
+ const graphMapping = useGraphMapping();
50
64
 
51
65
  const now = useMemo(() => {
52
66
  return notificationat ? new Date(notificationat) : new Date();
53
67
  }, [notificationat]);
54
68
 
55
- const graphMapping = useMemo(() => {
56
- return styleMetadata?.graphs || { 1: "osm" };
57
- }, [styleMetadata]);
58
-
59
69
  const graphsString = useMemo(() => {
60
- return [
61
- ...new Set(
62
- Object.keys(graphMapping || []).map((key) => {
63
- return graphMapping[key];
64
- }),
65
- ),
66
- ].join(",");
70
+ return [...new Set(Object.values(graphMapping))].join(",");
67
71
  }, [graphMapping]);
68
72
 
69
73
  useEffect(() => {
@@ -105,12 +109,12 @@ const useNotifications = () => {
105
109
  useEffect(() => {
106
110
  // Merge notifications with the previewNotification
107
111
  const newNotifications = [...notifications];
108
- if (shouldAddPreviewNotifications && previewNotification?.[style]) {
109
- const parsedPreviewNotification = parsePreviewNotification(
110
- previewNotification?.[style],
111
- );
112
+ const notifByStyle = previewNotification?.[styleName];
113
+
114
+ if (shouldAddPreviewNotifications && notifByStyle) {
115
+ const parsedPreviewNotification = parsePreviewNotification(notifByStyle);
112
116
  const index = newNotifications.findIndex((n) => {
113
- return n.properties.id === previewNotification[style].id;
117
+ return n.properties.id === notifByStyle.id;
114
118
  });
115
119
 
116
120
  if (index > -1) {
@@ -126,42 +130,47 @@ const useNotifications = () => {
126
130
  previewNotification,
127
131
  notifications,
128
132
  shouldAddPreviewNotifications,
129
- style,
133
+ styleName,
130
134
  now,
131
135
  ]);
132
136
 
133
- useEffect(() => {
134
- // Add the notifications to the map
135
- if (styleMetadata && notifications?.length) {
136
- addNotificationsLayers(
137
- baseLayer,
138
- notifications,
139
- notificationbeforelayerid,
140
- zoom,
141
- graphMapping,
142
- );
137
+ if (previewNotification?.id) {
138
+ const index = notifications.findIndex((n) => {
139
+ return n.properties.id === previewNotification.id;
140
+ });
141
+ if (index > -1) {
142
+ notifications[index].isPreview = true;
143
+ notifications[index].features.forEach((feature) => {
144
+ feature.properties.isPreview = true;
145
+ });
143
146
  }
144
- }, [
145
- notifications,
146
- notificationbeforelayerid,
147
- styleMetadata,
148
- zoom,
149
- graphMapping,
150
- baseLayer,
151
- ]);
152
-
153
- return notifications;
147
+ }
148
+
149
+ const features =
150
+ notifications
151
+ ?.map((notification) => {
152
+ return notification.features;
153
+ })
154
+ .flat() || [];
155
+
156
+ return {
157
+ features: features,
158
+ type: "FeatureCollection",
159
+ };
154
160
  };
155
161
 
156
162
  export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
157
- useNotifications();
158
- const { baseLayer, map } = useMapContext();
163
+ const notifications = useNotifications();
164
+ const { baseLayer, map, notificationbeforelayerid } = useMapContext();
165
+ const graphMapping = useGraphMapping();
166
+ const zoom = useZoom();
159
167
 
160
168
  const layer = useMemo(() => {
161
169
  if (!baseLayer) {
162
170
  return null;
163
171
  }
164
172
  return new MaplibreStyleLayer({
173
+ beforeId: notificationbeforelayerid,
165
174
  isQueryable: true,
166
175
  layersFilter: ({ metadata }) => {
167
176
  return metadata?.["general.filter"] === "notifications";
@@ -169,7 +178,25 @@ export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
169
178
  maplibreLayer: baseLayer,
170
179
  ...(props || {}),
171
180
  });
172
- }, [baseLayer, props]);
181
+ }, [baseLayer, notificationbeforelayerid, props]);
182
+
183
+ // Update data and style layers
184
+ useEffect(() => {
185
+ if (!baseLayer || !graphMapping) {
186
+ return null;
187
+ }
188
+ const graph = getCurrentGraph(graphMapping, zoom);
189
+ if (!graph) {
190
+ return null;
191
+ }
192
+ addNotificationsLayers(
193
+ baseLayer,
194
+ "notifications",
195
+ notifications,
196
+ notificationbeforelayerid,
197
+ graph,
198
+ );
199
+ }, [baseLayer, graphMapping, notificationbeforelayerid, notifications, zoom]);
173
200
 
174
201
  useEffect(() => {
175
202
  if (!map || !layer) {
@@ -2,6 +2,7 @@ import { FeatureCollection } from "geojson";
2
2
  import { Feature } from "ol";
3
3
  import { getCenter } from "ol/extent";
4
4
  import GeoJSON from "ol/format/GeoJSON";
5
+ import { toLonLat } from "ol/proj";
5
6
 
6
7
  import addSourceAndLayers from "../utils/addSourceAndLayers";
7
8
 
@@ -19,14 +20,32 @@ export const getTime = (str) => {
19
20
  */
20
21
  const getNotificationsWithStatus = (notifications, now) => {
21
22
  return notifications
22
- .filter((n) => {
23
+ .filter((notification) => {
23
24
  // TODO: The backend should be responsible to returns only good notifications.
24
- const notOutOfDate = n.properties.affected_time_intervals.some((ati) => {
25
- return now < new Date(ati.end);
26
- });
25
+ let notOutOfDate = notification.properties.affected_time_intervals.some(
26
+ (ati) => {
27
+ return now < new Date(ati.end);
28
+ },
29
+ );
30
+ if (!notOutOfDate) {
31
+ notOutOfDate = notification.properties.publications.some(
32
+ (publication) => {
33
+ return (
34
+ now >= new Date(publication.visible_from) &&
35
+ now <= new Date(publication.visible_until)
36
+ );
37
+ },
38
+ );
39
+ }
27
40
  return notOutOfDate;
28
41
  })
29
42
  .map((n) => {
43
+ const isPublished = n.properties.publications.some((publication) => {
44
+ return (
45
+ now >= new Date(publication.visible_from) &&
46
+ now <= new Date(publication.visible_until)
47
+ );
48
+ });
30
49
  const isActive = n.properties.affected_time_intervals.some((ati) => {
31
50
  const {
32
51
  end,
@@ -88,6 +107,7 @@ const getNotificationsWithStatus = (notifications, now) => {
88
107
  ...n.properties,
89
108
  iconRefPoint,
90
109
  isActive,
110
+ isPublished,
91
111
  starts,
92
112
  };
93
113
 
@@ -98,6 +118,22 @@ const getNotificationsWithStatus = (notifications, now) => {
98
118
  };
99
119
  });
100
120
 
121
+ if (iconRefPoint) {
122
+ features.push({
123
+ geometry: {
124
+ coordinates: toLonLat(iconRefPoint),
125
+ type: "Point",
126
+ },
127
+ id: Math.random() + "",
128
+ properties: {
129
+ ...properties,
130
+ disruption_type: "whatever",
131
+ isIconRefPoint: true,
132
+ },
133
+ type: "Feature",
134
+ });
135
+ }
136
+
101
137
  return {
102
138
  ...n,
103
139
  features,
@@ -106,7 +142,7 @@ const getNotificationsWithStatus = (notifications, now) => {
106
142
  });
107
143
  };
108
144
 
109
- const getCurrentGraph = (mapping: object, zoom: number) => {
145
+ export const getCurrentGraph = (mapping: object, zoom: number) => {
110
146
  const breakPoints = Object.keys(mapping).map((k) => {
111
147
  return parseFloat(k);
112
148
  });
@@ -116,41 +152,76 @@ const getCurrentGraph = (mapping: object, zoom: number) => {
116
152
  return mapping[closest || Math.min(...breakPoints)];
117
153
  };
118
154
 
155
+ // export const getDisruptionActiveStyleLayers = (
156
+ // id = "notificationActive",
157
+ // graph,
158
+ // ) => {
159
+ // return [
160
+ // {
161
+ // filter: [
162
+ // "all",
163
+ // ["==", ["get", "isActive"], true],
164
+ // ["==", ["get", "graph"], graph],
165
+ // ["==", ["get", "disruption_type"], "DISRUPTION"],
166
+ // ],
167
+ // id: id,
168
+ // layout: { visibility: "visible" },
169
+ // metadata: {
170
+ // "general.filter": "notifications",
171
+ // },
172
+ // paint: {
173
+ // "line-color": "rgba(255,0,0,1)",
174
+ // "line-dasharray": [2, 2],
175
+ // "line-width": 5,
176
+ // },
177
+ // source: "notifications",
178
+ // type: "line",
179
+ // },
180
+ // ];
181
+ // };
182
+
119
183
  /**
120
184
  * This function add layers in the mapbox style to show notifications lines.
121
185
  */
122
186
  const addNotificationsLayers = (
123
187
  mapboxLayer: object,
124
- notifications: FeatureCollection[],
188
+ sourceId: string,
189
+ sourceData: FeatureCollection,
125
190
  beforeLayerId: string,
126
- zoom: number,
127
- graphMapping: object,
191
+ graph: number,
128
192
  ) => {
129
193
  if (!mapboxLayer) {
130
194
  return;
131
195
  }
132
- const features = notifications
133
- .map((n) => {
134
- return n.features;
135
- })
136
- .flat();
196
+
137
197
  addSourceAndLayers(
138
198
  mapboxLayer,
139
- "notifications",
140
- {
141
- data: {
142
- features,
143
- type: "FeatureCollection",
144
- },
145
- generateId: true,
146
- type: "geojson",
147
- },
199
+ sourceId,
200
+ sourceData,
148
201
  [
202
+ // {
203
+ // filter: [
204
+ // "all",
205
+ // ["==", ["get", "isPreview"], true],
206
+ // ["==", ["get", "disruption_type"], "DISRUPTION"],
207
+ // ],
208
+ // id: "notificationsPreviewDisruption",
209
+ // layout: { visibility: "visible" },
210
+ // metadata: {
211
+ // "general.filter": "notifications",
212
+ // },
213
+ // paint: {
214
+ // "line-color": "rgba(255,255,0,0.2)",
215
+ // "line-width": 10,
216
+ // },
217
+ // source: "notifications",
218
+ // type: "line",
219
+ // },
149
220
  {
150
221
  filter: [
151
222
  "all",
152
223
  ["==", ["get", "isActive"], true],
153
- ["==", ["get", "graph"], getCurrentGraph(graphMapping, zoom)],
224
+ ["==", ["get", "graph"], graph],
154
225
  ["==", ["get", "disruption_type"], "DISRUPTION"],
155
226
  ],
156
227
  id: "notificationsActive",
@@ -166,6 +237,103 @@ const addNotificationsLayers = (
166
237
  source: "notifications",
167
238
  type: "line",
168
239
  },
240
+ // {
241
+ // filter: [
242
+ // "all",
243
+ // ["==", ["get", "isPreview"], true],
244
+ // // ["==", ["get", "graph"], graph],
245
+ // ["==", ["get", "disruption_type"], "DEVIATION"],
246
+ // ],
247
+ // id: "notificationsPreviewDeviation",
248
+ // layout: { visibility: "visible" },
249
+ // metadata: {
250
+ // "general.filter": "notifications",
251
+ // },
252
+ // paint: {
253
+ // "line-color": "rgba(255,255,0,0.2)",
254
+ // "line-width": 10,
255
+ // },
256
+ // source: "notifications",
257
+ // type: "line",
258
+ // },
259
+ {
260
+ filter: [
261
+ "all",
262
+ ["==", ["get", "isActive"], true],
263
+ ["==", ["get", "disruption_type"], "DEVIATION"],
264
+ ],
265
+ id: "notificationsActiveDeviation",
266
+ layout: { visibility: "visible" },
267
+ paint: {
268
+ "line-color": "#000000",
269
+ "line-dasharray": [2, 2],
270
+ "line-opacity": 0.5,
271
+ "line-width": 5,
272
+ },
273
+ source: "notifications",
274
+ type: "line",
275
+ },
276
+ {
277
+ filter: [
278
+ "all",
279
+ ["==", ["get", "isIconRefPoint"], true],
280
+ // ["==", ["get", "isActive"], true],
281
+ ],
282
+ id: "notificationsIconRefPointActive",
283
+ layout: { visibility: "visible" },
284
+ metadata: {
285
+ "general.filter": "notifications",
286
+ },
287
+ paint: {
288
+ "circle-color": "#ff0000",
289
+ "circle-radius": 10,
290
+ },
291
+ source: "notifications",
292
+ type: "circle",
293
+ },
294
+
295
+ {
296
+ filter: [
297
+ "all",
298
+ ["==", ["get", "isIconRefPoint"], true],
299
+ ["==", ["get", "isActive"], true],
300
+ ],
301
+ id: "notificationsIconRefPointActive",
302
+ layout: {
303
+ "icon-image": "warning",
304
+ "icon-size": 0.15,
305
+ visibility: "visible",
306
+ },
307
+ metadata: {
308
+ "general.filter": "notifications",
309
+ },
310
+ paint: {},
311
+ source: "notifications",
312
+ type: "symbol",
313
+ },
314
+ {
315
+ filter: [
316
+ "all",
317
+ ["==", ["get", "isIconRefPoint"], true],
318
+ ["==", ["get", "isActive"], false],
319
+ ["==", ["get", "isPublished"], true],
320
+ ],
321
+ id: "notificationsIconRefPointNonActive",
322
+ layout: {
323
+ "icon-image": "warningbanner",
324
+ "icon-size": 0.15,
325
+ "text-field": ["get", "starts"],
326
+ "text-offset": [1.5, 0],
327
+ "text-size": 8,
328
+ visibility: "visible",
329
+ },
330
+ metadata: {
331
+ "general.filter": "notifications",
332
+ },
333
+ paint: {},
334
+ source: "notifications",
335
+ type: "symbol",
336
+ },
169
337
  ],
170
338
  beforeLayerId,
171
339
  );
@@ -34,7 +34,7 @@ function RouteIcon({
34
34
  departure?.line ||
35
35
  stopSequence?.line ||
36
36
  trajectory?.properties?.line;
37
- const type = stopSequence?.type || trajectory?.type;
37
+ const type = lineToUse?.type || stopSequence?.type || trajectory?.type;
38
38
  const backgroundColor = getMainColorForVehicle(
39
39
  line || departure || stopSequence || trajectory,
40
40
  );
@@ -46,7 +46,10 @@ function RouteIcon({
46
46
 
47
47
  const fontSize = fontSizesByNbLetters[text.length] || 12;
48
48
  const font = getTextFontForVehicle(fontSize, text);
49
- const hasRealtime = stopSequence?.has_realtime_journey;
49
+
50
+ // RealtimeIcon only for stopsequence for now
51
+ const hasRealtime = stopSequence?.has_realtime_journey === true;
52
+ const showNoRealtimeIcon = !!stopSequence;
50
53
  const isCancelled = stopSequence?.stations[0]?.state === "JOURNEY_CANCELLED";
51
54
 
52
55
  if (borderColor === backgroundColor) {
@@ -67,7 +70,7 @@ function RouteIcon({
67
70
  >
68
71
  {children || text}
69
72
 
70
- {!isCancelled && !hasRealtime && (
73
+ {showNoRealtimeIcon && !isCancelled && !hasRealtime && (
71
74
  <NoRealtime className={"absolute -left-2 -top-2"} />
72
75
  )}
73
76
  </span>
@@ -6,11 +6,16 @@ import useMapContext from "../utils/hooks/useMapContext";
6
6
  import useRouteStop from "../utils/hooks/useRouteStop";
7
7
 
8
8
  export type RouteStopProgressProps = {
9
+ lineColor?: string;
9
10
  svgProps?: JSX.HTMLAttributes<SVGElement> & PreactDOMAttributes;
10
11
  } & JSX.HTMLAttributes<HTMLDivElement> &
11
12
  PreactDOMAttributes;
12
13
 
13
- function RouteStopProgress({ svgProps, ...props }: RouteStopProgressProps) {
14
+ function RouteStopProgress({
15
+ lineColor,
16
+ svgProps,
17
+ ...props
18
+ }: RouteStopProgressProps) {
14
19
  const { stopSequence } = useMapContext();
15
20
  const { invertColor, status } = useRouteStop();
16
21
  const { isBoarding, isFirst, isLast, isLeft, isPassed, progress } = status;
@@ -19,16 +24,16 @@ function RouteStopProgress({ svgProps, ...props }: RouteStopProgressProps) {
19
24
  const yDone = `${progress}%`;
20
25
 
21
26
  const greyColor = "rgb(156, 163, 175)";
22
- const lineColor = getMainColorForVehicle(stopSequence);
27
+ const lineColorr = lineColor || getMainColorForVehicle(stopSequence);
23
28
 
24
- let colorScheme = isPassed ? greyColor : lineColor;
25
- let invertColorScheme = isPassed ? lineColor : greyColor;
29
+ let colorScheme = isPassed ? greyColor : lineColorr;
30
+ let invertColorScheme = isPassed ? lineColorr : greyColor;
26
31
  let progressDoneColor = greyColor;
27
32
 
28
33
  if (invertColor) {
29
- colorScheme = isPassed ? lineColor : greyColor;
30
- invertColorScheme = isPassed ? greyColor : lineColor;
31
- progressDoneColor = lineColor;
34
+ colorScheme = isPassed ? lineColorr : greyColor;
35
+ invertColorScheme = isPassed ? greyColor : lineColorr;
36
+ progressDoneColor = lineColorr;
32
37
  }
33
38
 
34
39
  const circleColor =