@geops/rvf-mobility-web-component 0.1.41 → 0.1.43

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 (40) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/index.js +148 -272
  3. package/package.json +1 -1
  4. package/src/NotificationLayer/NotificationLayer.tsx +1 -0
  5. package/src/NotificationLayer/notificationUtils.ts +234 -160
  6. package/src/RvfFeatureDetails/RvfLineNetworkDetails/RvfLineNetworkDetails.tsx +172 -131
  7. package/src/RvfFeatureDetails/RvfNotificationDetails/RvfNotificationDetails.tsx +198 -44
  8. package/src/icons/disruption-type-banner.xcf +0 -0
  9. package/src/icons/sbb_add_stop.png +0 -0
  10. package/src/icons/sbb_add_stop.svg +6 -0
  11. package/src/icons/sbb_alternative.png +0 -0
  12. package/src/icons/sbb_alternative.svg +6 -0
  13. package/src/icons/sbb_cancellation.png +0 -0
  14. package/src/icons/sbb_cancellation.svg +7 -0
  15. package/src/icons/sbb_construction.png +0 -0
  16. package/src/icons/sbb_construction.svg +6 -0
  17. package/src/icons/sbb_construction_banner.png +0 -0
  18. package/src/icons/sbb_delay.png +0 -0
  19. package/src/icons/sbb_delay.svg +6 -0
  20. package/src/icons/sbb_disruption.png +0 -0
  21. package/src/icons/sbb_disruption.svg +6 -0
  22. package/src/icons/sbb_disruption_banner.png +0 -0
  23. package/src/icons/sbb_info.png +0 -0
  24. package/src/icons/sbb_info.svg +6 -0
  25. package/src/icons/sbb_missed_connection.png +0 -0
  26. package/src/icons/sbb_missed_connection.svg +6 -0
  27. package/src/icons/sbb_missed_connection_banner.png +0 -0
  28. package/src/icons/sbb_platform_change.png +0 -0
  29. package/src/icons/sbb_platform_change.svg +7 -0
  30. package/src/icons/sbb_replacementbus.png +0 -0
  31. package/src/icons/sbb_replacementbus.svg +6 -0
  32. package/src/icons/sbb_replacementbus_banner.png +0 -0
  33. package/src/icons/sbb_reroute.png +0 -0
  34. package/src/icons/sbb_reroute.svg +7 -0
  35. package/src/icons/warning-banner.xcf +0 -0
  36. package/src/utils/addSourceAndLayers.ts +51 -5
  37. package/src/utils/{getFeatureInformationTitle.ts → getFeatureInformationTitle.tsx} +14 -2
  38. package/src/utils/i18n.ts +6 -5
  39. package/src/utils/sharingGraphqlUtils.ts +18 -11
  40. package/src/utils/translations.ts +16 -0
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.41",
5
+ "version": "0.1.43",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
@@ -92,6 +92,7 @@ const useNotifications = (): FeatureCollection => {
92
92
  abortCtrl = new AbortController();
93
93
  const response = await fetch(url, { signal: abortCtrl.signal });
94
94
  const data = await response.json();
95
+
95
96
  const notifications = getNotificationsWithStatus(data, now);
96
97
  setNotifications(notifications);
97
98
  setShouldAddPreviewNotifications(true);
@@ -12,6 +12,89 @@ export const getTime = (str) => {
12
12
  return parseInt(str?.substr(0, 8).replace(/:/g, ""), 10);
13
13
  };
14
14
 
15
+ export const isNotificationNotOutOfDate = (notification, now) => {
16
+ // TODO: The backend should be responsible to returns only good notifications.
17
+ let notOutOfDate = notification.properties.affected_time_intervals.some(
18
+ (ati) => {
19
+ return now < new Date(ati.end);
20
+ },
21
+ );
22
+ if (!notOutOfDate) {
23
+ notOutOfDate = notification.properties.publications.some((publication) => {
24
+ return (
25
+ now >= new Date(publication.visible_from) &&
26
+ now <= new Date(publication.visible_until)
27
+ );
28
+ });
29
+ }
30
+ return notOutOfDate;
31
+ };
32
+ export const isNotificationPublished = (notification, now) => {
33
+ if (!notification?.properties?.publications?.length) {
34
+ // If there is no piblications date, use the time intervals
35
+ return isNotificationActive(notification, now);
36
+ }
37
+ return notification.properties.publications.some((publication) => {
38
+ return (
39
+ now >= new Date(publication.visible_from) &&
40
+ now <= new Date(publication.visible_until)
41
+ );
42
+ });
43
+ };
44
+
45
+ export const isNotificationActive = (notification, now: Date) => {
46
+ return notification.properties.affected_time_intervals.some(
47
+ (affectedTimeInterval) => {
48
+ const {
49
+ end,
50
+ start,
51
+ time_of_day_end: dayTimeEnd,
52
+ time_of_day_start: dayTimeStart,
53
+ } = affectedTimeInterval;
54
+ const nowTime = getTime(now.toTimeString());
55
+ const startTime = getTime(dayTimeStart);
56
+ const endTime = getTime(dayTimeEnd);
57
+ const inRange = new Date(start) <= now && now <= new Date(end);
58
+ return startTime && endTime
59
+ ? inRange && startTime <= nowTime && nowTime <= endTime
60
+ : inRange;
61
+ },
62
+ );
63
+ };
64
+
65
+ const getStartsString = (notification, now) => {
66
+ const next = notification.properties.affected_time_intervals.reduce(
67
+ (a, b) => {
68
+ const aEnd = new Date(a.end);
69
+ const aStart = new Date(a.start);
70
+ const bStart = new Date(b.start);
71
+ return now < aEnd && aStart < bStart ? a : b;
72
+ },
73
+ [],
74
+ );
75
+ const nextStartDate = new Date(next.start);
76
+ let starts;
77
+ if (
78
+ now.toDateString() === nextStartDate.toDateString() ||
79
+ now.getTime() - nextStartDate.getTime() > 0
80
+ ) {
81
+ if (next.time_of_day_start) {
82
+ starts = `ab ${next.time_of_day_start.substr(0, 5)}`;
83
+ } else {
84
+ starts = `ab ${nextStartDate.toLocaleTimeString(["de"], {
85
+ hour: "2-digit",
86
+ hour12: false,
87
+ minute: "2-digit",
88
+ })}`;
89
+ }
90
+ } else {
91
+ starts = `ab ${nextStartDate.toLocaleDateString(["de-DE"], {
92
+ day: "numeric",
93
+ month: "short",
94
+ })}`;
95
+ }
96
+ return starts;
97
+ };
15
98
  /**
16
99
  *
17
100
  * @param {Array.<Object>} notifications Raw notifications
@@ -20,124 +103,86 @@ export const getTime = (str) => {
20
103
  */
21
104
  const getNotificationsWithStatus = (notifications, now) => {
22
105
  return notifications
23
- .filter((notification) => {
24
- // TODO: The backend should be responsible to returns only good notifications.
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
- }
40
- return notOutOfDate;
41
- })
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
- });
49
- const isActive = n.properties.affected_time_intervals.some((ati) => {
50
- const {
51
- end,
52
- start,
53
- time_of_day_end: dayTimeEnd,
54
- time_of_day_start: dayTimeStart,
55
- } = ati;
56
- const nowTime = getTime(now.toTimeString());
57
- const startTime = getTime(dayTimeStart);
58
- const endTime = getTime(dayTimeEnd);
59
- const inRange = new Date(start) <= now && now <= new Date(end);
60
- return startTime && endTime
61
- ? inRange && startTime <= nowTime && nowTime <= endTime
62
- : inRange;
63
- });
64
-
65
- const next = n.properties.affected_time_intervals.reduce((a, b) => {
66
- const aEnd = new Date(a.end);
67
- const aStart = new Date(a.start);
68
- const bStart = new Date(b.start);
69
- return now < aEnd && aStart < bStart ? a : b;
70
- }, []);
71
- const nextStartDate = new Date(next.start);
72
- let starts;
73
- if (
74
- now.toDateString() === nextStartDate.toDateString() ||
75
- now.getTime() - nextStartDate.getTime() > 0
76
- ) {
77
- if (next.time_of_day_start) {
78
- starts = `ab ${next.time_of_day_start.substr(0, 5)}`;
79
- } else {
80
- starts = `ab ${nextStartDate.toLocaleTimeString(["de"], {
81
- hour: "2-digit",
82
- hour12: false,
83
- minute: "2-digit",
84
- })}`;
85
- }
86
- } else {
87
- starts = `ab ${nextStartDate.toLocaleDateString(["de-DE"], {
88
- day: "numeric",
89
- month: "short",
90
- })}`;
91
- }
106
+ .filter(isNotificationNotOutOfDate)
107
+ .map((notification) => {
108
+ // For information:
109
+ // A notification here is a FeatureCollection representing a publication of a situation,
110
+ // containing properties about the situation and a list of features representing affecetdLines and affectedStops of this publication.
111
+ // AffectedStops are not currently represented in the map.
112
+ // AffectedLines are represented as linestrings in the map(the display of line is currently disabled) and, if is_icon_ref=true, as an icon .
113
+ // The representation of the icon depends of the disruption_type property of the publication.
92
114
 
93
- let iconRefPoint;
94
- const iconRef = n.features.find((f) => {
95
- return f.properties.is_icon_ref;
96
- });
97
- if (iconRef) {
98
- const iconRefFeature = format.readFeature(iconRef, {
99
- dataProjection: "EPSG:4326",
100
- featureProjection: "EPSG:3857",
101
- }) as Feature;
102
- const center = getCenter(iconRefFeature.getGeometry().getExtent());
103
- iconRefPoint = iconRefFeature.getGeometry().getClosestPoint(center);
104
- }
115
+ const isPublished = isNotificationPublished(notification, now);
116
+ const isActive = isNotificationActive(notification, now);
117
+ const starts = getStartsString(notification, now);
105
118
 
106
- const properties = {
107
- ...n.properties,
108
- iconRefPoint,
119
+ const commonProperties = {
120
+ ...notification.properties,
109
121
  isActive,
110
122
  isPublished,
111
123
  starts,
112
124
  };
113
125
 
114
- const features = n.features.map((f) => {
126
+ // We append all the common properties to all the features.
127
+ const features = notification.features.map((f) => {
115
128
  return {
116
129
  ...f,
117
- properties: { ...f.properties, ...properties },
130
+ properties: { ...commonProperties, ...f.properties },
118
131
  };
119
132
  });
120
133
 
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",
134
+ // We search for is_icon_ref=true features (lines) and create a point feature for each of them.
135
+ const iconRefFeatures = notification.features
136
+ .filter((f) => {
137
+ return f.properties.is_icon_ref || f.geometry.type === "Point";
138
+ })
139
+ .map((affectedLine) => {
140
+ const affectedLineFeature = format.readFeature(affectedLine, {
141
+ dataProjection: "EPSG:4326",
142
+ featureProjection: "EPSG:3857",
143
+ }) as Feature;
144
+ const center = getCenter(
145
+ affectedLineFeature.getGeometry().getExtent(),
146
+ );
147
+ const iconRefPoint = affectedLineFeature
148
+ .getGeometry()
149
+ .getClosestPoint(center);
150
+
151
+ const iconRefFeatureProperties = {
152
+ ...commonProperties,
153
+ ...affectedLine.properties,
131
154
  isIconRefPoint: true,
132
- },
133
- type: "Feature",
155
+ };
156
+ if (!iconRefFeatureProperties.disruption_type) {
157
+ iconRefFeatureProperties.disruption_type = "OTHER";
158
+ }
159
+ // Set Banner image
160
+ if (iconRefFeatureProperties.disruption_type) {
161
+ iconRefFeatureProperties.disruption_type_banner =
162
+ iconRefFeatureProperties.disruption_type + "_BANNER";
163
+ }
164
+
165
+ if (iconRefPoint) {
166
+ const iconRefFeature = {
167
+ geometry: {
168
+ coordinates: toLonLat(iconRefPoint),
169
+ type: "Point",
170
+ },
171
+ id: Math.random() + "",
172
+ properties: iconRefFeatureProperties,
173
+ type: "Feature",
174
+ };
175
+ return iconRefFeature;
176
+ }
177
+ })
178
+ .filter((f) => {
179
+ return !!f;
134
180
  });
135
- }
136
181
 
137
182
  return {
138
- ...n,
139
- features,
140
- properties,
183
+ ...notification,
184
+ features: [...features, ...iconRefFeatures],
185
+ properties: commonProperties,
141
186
  };
142
187
  });
143
188
  };
@@ -188,6 +233,7 @@ const addNotificationsLayers = (
188
233
  sourceId: string,
189
234
  sourceData: FeatureCollection,
190
235
  beforeLayerId: string,
236
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
191
237
  graph: number,
192
238
  ) => {
193
239
  if (!mapboxLayer) {
@@ -217,26 +263,26 @@ const addNotificationsLayers = (
217
263
  // source: "notifications",
218
264
  // type: "line",
219
265
  // },
220
- {
221
- filter: [
222
- "all",
223
- ["==", ["get", "isActive"], true],
224
- ["==", ["get", "graph"], graph],
225
- ["==", ["get", "disruption_type"], "DISRUPTION"],
226
- ],
227
- id: "notificationsActive",
228
- layout: { visibility: "visible" },
229
- metadata: {
230
- "general.filter": "notifications",
231
- },
232
- paint: {
233
- "line-color": "rgba(255,0,0,1)",
234
- "line-dasharray": [2, 2],
235
- "line-width": 5,
236
- },
237
- source: "notifications",
238
- type: "line",
239
- },
266
+ // {
267
+ // filter: [
268
+ // "all",
269
+ // ["==", ["get", "isActive"], true],
270
+ // ["==", ["get", "graph"], graph],
271
+ // ["==", ["get", "disruption_type"], "DISRUPTION"],
272
+ // ],
273
+ // id: "notificationsActive",
274
+ // layout: { visibility: "visible" },
275
+ // metadata: {
276
+ // "general.filter": "notifications",
277
+ // },
278
+ // paint: {
279
+ // "line-color": "rgba(255,0,0,0.5)",
280
+ // "line-dasharray": [2, 2],
281
+ // "line-width": 5,
282
+ // },
283
+ // source: "notifications",
284
+ // type: "line",
285
+ // },
240
286
  // {
241
287
  // filter: [
242
288
  // "all",
@@ -256,61 +302,78 @@ const addNotificationsLayers = (
256
302
  // source: "notifications",
257
303
  // type: "line",
258
304
  // },
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
- },
305
+ // {
306
+ // filter: [
307
+ // "all",
308
+ // ["==", ["get", "isActive"], true],
309
+ // ["==", ["get", "disruption_type"], "DEVIATION"],
310
+ // ],
311
+ // id: "notificationsActiveDeviation",
312
+ // layout: { visibility: "visible" },
313
+ // paint: {
314
+ // "line-color": "#000000",
315
+ // "line-dasharray": [2, 2],
316
+ // "line-opacity": 0.5,
317
+ // "line-width": 5,
318
+ // },
319
+ // source: "notifications",
320
+ // type: "line",
321
+ // },
322
+ // {
323
+ // filter: [
324
+ // "all",
325
+ // ["==", ["get", "isIconRefPoint"], true],
326
+ // // ["==", ["get", "isActive"], true],
327
+ // ],
328
+ // id: "notificationsIconRefPointActive",
329
+ // layout: { visibility: "visible" },
330
+ // metadata: {
331
+ // "general.filter": "notifications",
332
+ // },
333
+ // paint: {
334
+ // "circle-color": "#ff0000",
335
+ // "circle-radius": 10,
336
+ // },
337
+ // source: "notifications",
338
+ // type: "circle",
339
+ // },
294
340
 
341
+ // Display an icon
295
342
  {
296
343
  filter: [
297
344
  "all",
298
345
  ["==", ["get", "isIconRefPoint"], true],
346
+ ["==", ["get", "isPublished"], true],
299
347
  ["==", ["get", "isActive"], true],
300
348
  ],
301
349
  id: "notificationsIconRefPointActive",
302
350
  layout: {
303
- "icon-image": "warning",
304
- "icon-size": 0.15,
351
+ "icon-allow-overlap": true,
352
+ // "icon-image": "warning"
353
+ "icon-image": [
354
+ "coalesce",
355
+ // ["image", "warning"],
356
+ ["image", ["get", "disruption_type"]],
357
+ // If no image with the name above exists, show the
358
+ // "rocket" image instead.
359
+ ["image", "warning"],
360
+ ],
361
+ "icon-size": 0.6,
362
+
363
+ // "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0, 11, 0.6],
305
364
  visibility: "visible",
306
365
  },
366
+
307
367
  metadata: {
308
368
  "general.filter": "notifications",
309
369
  },
370
+ minzoom: 11,
310
371
  paint: {},
311
372
  source: "notifications",
312
373
  type: "symbol",
313
374
  },
375
+
376
+ // Display a banner with the start date
314
377
  {
315
378
  filter: [
316
379
  "all",
@@ -320,8 +383,18 @@ const addNotificationsLayers = (
320
383
  ],
321
384
  id: "notificationsIconRefPointNonActive",
322
385
  layout: {
323
- "icon-image": "warningbanner",
386
+ "icon-allow-overlap": true,
387
+ // "icon-image": "warningBanner",
388
+ "icon-image": [
389
+ "coalesce",
390
+ // ["image", "warning"],
391
+ ["image", ["get", "disruption_type_banner"]],
392
+ // If no image with the name above exists, show the
393
+ // "rocket" image instead.
394
+ ["image", "warningBanner"],
395
+ ],
324
396
  "icon-size": 0.15,
397
+ // "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0, 11, 0.15],
325
398
  "text-field": ["get", "starts"],
326
399
  "text-offset": [1.5, 0],
327
400
  "text-size": 8,
@@ -330,6 +403,7 @@ const addNotificationsLayers = (
330
403
  metadata: {
331
404
  "general.filter": "notifications",
332
405
  },
406
+ minzoom: 11,
333
407
  paint: {},
334
408
  source: "notifications",
335
409
  type: "symbol",