@geops/rvf-mobility-web-component 0.1.43 → 0.1.45
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/.yarnrc.yml +1 -0
- package/CHANGELOG.md +17 -0
- package/README.md +31 -3
- package/docutils.js +1 -1
- package/iframe.html +221 -11
- package/index.html +16 -5
- package/index.js +113 -108
- package/package.json +2 -2
- package/src/MobilityMap/MobilityMap.tsx +2 -2
- package/src/NotificationLayer/NotificationLayer.tsx +11 -189
- package/src/RouteStopTime/RouteStopTime.tsx +2 -2
- package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +4 -3
- package/src/RvfFeatureDetails/RvfLineNetworkDetails/RvfLineNetworkDetails.tsx +9 -3
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +68 -21
- package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +39 -26
- package/src/RvfTopics/RvfTopics.tsx +25 -1
- package/src/utils/constants.ts +7 -4
- package/src/utils/hooks/useMapContext.tsx +5 -0
- package/src/utils/sharingWFSUtils.ts +6 -0
- package/testNotification.json +103 -50641
- package/src/NotificationLayer/notificationUtils.ts +0 -437
- package/src/utils/createFreeFloatMobilityLayer.ts +0 -152
- package/src/utils/createMobiDataBwWfsLayer.ts +0 -155
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import { FeatureCollection } from "geojson";
|
|
2
|
-
import { Feature } from "ol";
|
|
3
|
-
import { getCenter } from "ol/extent";
|
|
4
|
-
import GeoJSON from "ol/format/GeoJSON";
|
|
5
|
-
import { toLonLat } from "ol/proj";
|
|
6
|
-
|
|
7
|
-
import addSourceAndLayers from "../utils/addSourceAndLayers";
|
|
8
|
-
|
|
9
|
-
const format = new GeoJSON();
|
|
10
|
-
|
|
11
|
-
export const getTime = (str) => {
|
|
12
|
-
return parseInt(str?.substr(0, 8).replace(/:/g, ""), 10);
|
|
13
|
-
};
|
|
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
|
-
};
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
* @param {Array.<Object>} notifications Raw notifications
|
|
101
|
-
* @param {Date} now The date to compare with the affected_time_intervals
|
|
102
|
-
* @returns {Array.<Object>}
|
|
103
|
-
*/
|
|
104
|
-
const getNotificationsWithStatus = (notifications, now) => {
|
|
105
|
-
return notifications
|
|
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.
|
|
114
|
-
|
|
115
|
-
const isPublished = isNotificationPublished(notification, now);
|
|
116
|
-
const isActive = isNotificationActive(notification, now);
|
|
117
|
-
const starts = getStartsString(notification, now);
|
|
118
|
-
|
|
119
|
-
const commonProperties = {
|
|
120
|
-
...notification.properties,
|
|
121
|
-
isActive,
|
|
122
|
-
isPublished,
|
|
123
|
-
starts,
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// We append all the common properties to all the features.
|
|
127
|
-
const features = notification.features.map((f) => {
|
|
128
|
-
return {
|
|
129
|
-
...f,
|
|
130
|
-
properties: { ...commonProperties, ...f.properties },
|
|
131
|
-
};
|
|
132
|
-
});
|
|
133
|
-
|
|
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,
|
|
154
|
-
isIconRefPoint: true,
|
|
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;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
...notification,
|
|
184
|
-
features: [...features, ...iconRefFeatures],
|
|
185
|
-
properties: commonProperties,
|
|
186
|
-
};
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
export const getCurrentGraph = (mapping: object, zoom: number) => {
|
|
191
|
-
const breakPoints = Object.keys(mapping).map((k) => {
|
|
192
|
-
return parseFloat(k);
|
|
193
|
-
});
|
|
194
|
-
const closest = breakPoints.reverse().find((bp) => {
|
|
195
|
-
return bp <= Math.floor(zoom) - 1;
|
|
196
|
-
}); // - 1 due to ol zoom !== mapbox zoom
|
|
197
|
-
return mapping[closest || Math.min(...breakPoints)];
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// export const getDisruptionActiveStyleLayers = (
|
|
201
|
-
// id = "notificationActive",
|
|
202
|
-
// graph,
|
|
203
|
-
// ) => {
|
|
204
|
-
// return [
|
|
205
|
-
// {
|
|
206
|
-
// filter: [
|
|
207
|
-
// "all",
|
|
208
|
-
// ["==", ["get", "isActive"], true],
|
|
209
|
-
// ["==", ["get", "graph"], graph],
|
|
210
|
-
// ["==", ["get", "disruption_type"], "DISRUPTION"],
|
|
211
|
-
// ],
|
|
212
|
-
// id: id,
|
|
213
|
-
// layout: { visibility: "visible" },
|
|
214
|
-
// metadata: {
|
|
215
|
-
// "general.filter": "notifications",
|
|
216
|
-
// },
|
|
217
|
-
// paint: {
|
|
218
|
-
// "line-color": "rgba(255,0,0,1)",
|
|
219
|
-
// "line-dasharray": [2, 2],
|
|
220
|
-
// "line-width": 5,
|
|
221
|
-
// },
|
|
222
|
-
// source: "notifications",
|
|
223
|
-
// type: "line",
|
|
224
|
-
// },
|
|
225
|
-
// ];
|
|
226
|
-
// };
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* This function add layers in the mapbox style to show notifications lines.
|
|
230
|
-
*/
|
|
231
|
-
const addNotificationsLayers = (
|
|
232
|
-
mapboxLayer: object,
|
|
233
|
-
sourceId: string,
|
|
234
|
-
sourceData: FeatureCollection,
|
|
235
|
-
beforeLayerId: string,
|
|
236
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
237
|
-
graph: number,
|
|
238
|
-
) => {
|
|
239
|
-
if (!mapboxLayer) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
addSourceAndLayers(
|
|
244
|
-
mapboxLayer,
|
|
245
|
-
sourceId,
|
|
246
|
-
sourceData,
|
|
247
|
-
[
|
|
248
|
-
// {
|
|
249
|
-
// filter: [
|
|
250
|
-
// "all",
|
|
251
|
-
// ["==", ["get", "isPreview"], true],
|
|
252
|
-
// ["==", ["get", "disruption_type"], "DISRUPTION"],
|
|
253
|
-
// ],
|
|
254
|
-
// id: "notificationsPreviewDisruption",
|
|
255
|
-
// layout: { visibility: "visible" },
|
|
256
|
-
// metadata: {
|
|
257
|
-
// "general.filter": "notifications",
|
|
258
|
-
// },
|
|
259
|
-
// paint: {
|
|
260
|
-
// "line-color": "rgba(255,255,0,0.2)",
|
|
261
|
-
// "line-width": 10,
|
|
262
|
-
// },
|
|
263
|
-
// source: "notifications",
|
|
264
|
-
// type: "line",
|
|
265
|
-
// },
|
|
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
|
-
// },
|
|
286
|
-
// {
|
|
287
|
-
// filter: [
|
|
288
|
-
// "all",
|
|
289
|
-
// ["==", ["get", "isPreview"], true],
|
|
290
|
-
// // ["==", ["get", "graph"], graph],
|
|
291
|
-
// ["==", ["get", "disruption_type"], "DEVIATION"],
|
|
292
|
-
// ],
|
|
293
|
-
// id: "notificationsPreviewDeviation",
|
|
294
|
-
// layout: { visibility: "visible" },
|
|
295
|
-
// metadata: {
|
|
296
|
-
// "general.filter": "notifications",
|
|
297
|
-
// },
|
|
298
|
-
// paint: {
|
|
299
|
-
// "line-color": "rgba(255,255,0,0.2)",
|
|
300
|
-
// "line-width": 10,
|
|
301
|
-
// },
|
|
302
|
-
// source: "notifications",
|
|
303
|
-
// type: "line",
|
|
304
|
-
// },
|
|
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
|
-
// },
|
|
340
|
-
|
|
341
|
-
// Display an icon
|
|
342
|
-
{
|
|
343
|
-
filter: [
|
|
344
|
-
"all",
|
|
345
|
-
["==", ["get", "isIconRefPoint"], true],
|
|
346
|
-
["==", ["get", "isPublished"], true],
|
|
347
|
-
["==", ["get", "isActive"], true],
|
|
348
|
-
],
|
|
349
|
-
id: "notificationsIconRefPointActive",
|
|
350
|
-
layout: {
|
|
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],
|
|
364
|
-
visibility: "visible",
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
metadata: {
|
|
368
|
-
"general.filter": "notifications",
|
|
369
|
-
},
|
|
370
|
-
minzoom: 11,
|
|
371
|
-
paint: {},
|
|
372
|
-
source: "notifications",
|
|
373
|
-
type: "symbol",
|
|
374
|
-
},
|
|
375
|
-
|
|
376
|
-
// Display a banner with the start date
|
|
377
|
-
{
|
|
378
|
-
filter: [
|
|
379
|
-
"all",
|
|
380
|
-
["==", ["get", "isIconRefPoint"], true],
|
|
381
|
-
["==", ["get", "isActive"], false],
|
|
382
|
-
["==", ["get", "isPublished"], true],
|
|
383
|
-
],
|
|
384
|
-
id: "notificationsIconRefPointNonActive",
|
|
385
|
-
layout: {
|
|
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
|
-
],
|
|
396
|
-
"icon-size": 0.15,
|
|
397
|
-
// "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0, 11, 0.15],
|
|
398
|
-
"text-field": ["get", "starts"],
|
|
399
|
-
"text-offset": [1.5, 0],
|
|
400
|
-
"text-size": 8,
|
|
401
|
-
visibility: "visible",
|
|
402
|
-
},
|
|
403
|
-
metadata: {
|
|
404
|
-
"general.filter": "notifications",
|
|
405
|
-
},
|
|
406
|
-
minzoom: 11,
|
|
407
|
-
paint: {},
|
|
408
|
-
source: "notifications",
|
|
409
|
-
type: "symbol",
|
|
410
|
-
},
|
|
411
|
-
],
|
|
412
|
-
beforeLayerId,
|
|
413
|
-
);
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const parsePreviewNotification = (mocoPreviewObject: {
|
|
417
|
-
graphs: object;
|
|
418
|
-
id: number;
|
|
419
|
-
}) => {
|
|
420
|
-
let properties = {};
|
|
421
|
-
const features = Object.keys(mocoPreviewObject.graphs).map((graph) => {
|
|
422
|
-
const feature = mocoPreviewObject.graphs[graph].features[0];
|
|
423
|
-
properties = mocoPreviewObject.graphs[graph].properties;
|
|
424
|
-
return { ...feature, properties: { ...feature.properties, graph } };
|
|
425
|
-
});
|
|
426
|
-
return {
|
|
427
|
-
features,
|
|
428
|
-
properties,
|
|
429
|
-
type: "FeatureCollection",
|
|
430
|
-
};
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
export {
|
|
434
|
-
addNotificationsLayers,
|
|
435
|
-
getNotificationsWithStatus,
|
|
436
|
-
parsePreviewNotification,
|
|
437
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { Feature, getUid } from "ol";
|
|
2
|
-
import { GeoJSON } from "ol/format";
|
|
3
|
-
import { Point } from "ol/geom";
|
|
4
|
-
import VectorLayer, { Options } from "ol/layer/Vector";
|
|
5
|
-
import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
|
|
6
|
-
import { Cluster, Vector } from "ol/source";
|
|
7
|
-
import { Circle, Fill, Stroke, Style, Text } from "ol/style";
|
|
8
|
-
|
|
9
|
-
import { LAYER_PROP_IS_EXPORTING } from "./constants";
|
|
10
|
-
import { iconStyleByFormFactor } from "./createMobiDataBwWfsLayer";
|
|
11
|
-
|
|
12
|
-
export const getCircleStyle = (color: string) => {
|
|
13
|
-
return new Style({
|
|
14
|
-
image: new Circle({
|
|
15
|
-
declutterMode: "declutter",
|
|
16
|
-
displacement: [-12, 12],
|
|
17
|
-
fill: new Fill({
|
|
18
|
-
color: "white",
|
|
19
|
-
}),
|
|
20
|
-
radius: 9,
|
|
21
|
-
stroke: new Stroke({
|
|
22
|
-
color: color,
|
|
23
|
-
width: 1,
|
|
24
|
-
}),
|
|
25
|
-
}),
|
|
26
|
-
text: new Text({
|
|
27
|
-
declutterMode: "declutter",
|
|
28
|
-
fill: new Fill({
|
|
29
|
-
color: color,
|
|
30
|
-
}),
|
|
31
|
-
font: "bolder 10px arial",
|
|
32
|
-
offsetX: -12,
|
|
33
|
-
offsetY: -11,
|
|
34
|
-
}),
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function createFreeFloatMobilityLayer(
|
|
39
|
-
name: string,
|
|
40
|
-
color: string,
|
|
41
|
-
formFactor: string,
|
|
42
|
-
layerOptions: Options = {
|
|
43
|
-
minZoom: 17.99,
|
|
44
|
-
},
|
|
45
|
-
): VectorLayer<Vector<Feature<Point>>> {
|
|
46
|
-
// Exemple how to get a specific form factor
|
|
47
|
-
//api.mobidata-bw.de/geoserver/MobiData-BW/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=MobiData-BW%3Asharing_vehicles&CQL_FILTER=form_factor%20%3D%20%27scooter%27&maxFeatures=20000&outputFormat=csv
|
|
48
|
-
const source = new Vector({
|
|
49
|
-
format: new GeoJSON(),
|
|
50
|
-
loader: function (extent, resolution, projection, success, failure) {
|
|
51
|
-
const url =
|
|
52
|
-
"https://api.mobidata-bw.de/geoserver/MobiData-BW/" +
|
|
53
|
-
name +
|
|
54
|
-
"/ows" +
|
|
55
|
-
"?service=WFS&" +
|
|
56
|
-
"version=1.1.0&request=GetFeature&typename=" +
|
|
57
|
-
"MobiData-BW:" +
|
|
58
|
-
name +
|
|
59
|
-
"&" +
|
|
60
|
-
"outputFormat=application/json" +
|
|
61
|
-
"bbox=" +
|
|
62
|
-
extent.join(",") +
|
|
63
|
-
",EPSG:3857";
|
|
64
|
-
const xhr = new XMLHttpRequest();
|
|
65
|
-
xhr.open("GET", url);
|
|
66
|
-
const onError = function () {
|
|
67
|
-
source.removeLoadedExtent(extent);
|
|
68
|
-
failure();
|
|
69
|
-
};
|
|
70
|
-
xhr.onerror = onError;
|
|
71
|
-
xhr.onload = function () {
|
|
72
|
-
if (xhr.status == 200) {
|
|
73
|
-
const features = source
|
|
74
|
-
.getFormat()
|
|
75
|
-
.readFeatures(xhr.responseText, {
|
|
76
|
-
dataProjection: "EPSG:4326",
|
|
77
|
-
featureProjection: "EPSG:3857",
|
|
78
|
-
})
|
|
79
|
-
?.filter((feature) => {
|
|
80
|
-
if (formFactor) {
|
|
81
|
-
return feature.get("form_factor") === formFactor;
|
|
82
|
-
}
|
|
83
|
-
return true;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
features?.forEach((feature) => {
|
|
87
|
-
// The WFS returns everytime a different id for the same vehicle, so we set the vehicle_id as id.
|
|
88
|
-
// and remove the previous feature with the same id.
|
|
89
|
-
const vehicleId = feature.get("vehicle_id");
|
|
90
|
-
if (vehicleId) {
|
|
91
|
-
feature.setId(vehicleId);
|
|
92
|
-
const featuresToRemove = source.getFeatures().filter((feat) => {
|
|
93
|
-
return feat.getId() === feature.getId();
|
|
94
|
-
});
|
|
95
|
-
source.removeFeatures(featuresToRemove);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
source.addFeatures(features);
|
|
99
|
-
success(features);
|
|
100
|
-
} else {
|
|
101
|
-
onError();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
xhr.send();
|
|
105
|
-
},
|
|
106
|
-
strategy: bboxStrategy,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const style = getCircleStyle(color);
|
|
110
|
-
|
|
111
|
-
const clusterSource = new Cluster({
|
|
112
|
-
distance: 40,
|
|
113
|
-
source: source,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const styleFunction = (feature) => {
|
|
117
|
-
if (layer.get(LAYER_PROP_IS_EXPORTING)) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
const circleStyle = style.clone();
|
|
121
|
-
|
|
122
|
-
const featuresCount = feature.get("features").length;
|
|
123
|
-
circleStyle.getText().setText(featuresCount);
|
|
124
|
-
|
|
125
|
-
const formFactor =
|
|
126
|
-
feature.get("features")?.[0].get?.("form_factor") || "scooter";
|
|
127
|
-
|
|
128
|
-
const styles = [iconStyleByFormFactor[formFactor], circleStyle];
|
|
129
|
-
|
|
130
|
-
styles.forEach((style) => {
|
|
131
|
-
style.setZIndex(parseInt(getUid(feature), 10));
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
return styles;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const layer = new VectorLayer({
|
|
138
|
-
// @ts-expect-error - custom properties
|
|
139
|
-
isQueryable: true,
|
|
140
|
-
source: clusterSource,
|
|
141
|
-
style: styleFunction,
|
|
142
|
-
...layerOptions,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
clusterSource.on("addfeature", function (event) {
|
|
146
|
-
event.feature?.setStyle(styleFunction);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
return layer;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export default createFreeFloatMobilityLayer;
|