@geops/rvf-mobility-web-component 0.1.24-beta.0 → 0.1.25
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 +15 -0
- package/index.html +4 -1
- package/index.js +94 -94
- package/package.json +1 -1
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +16 -4
- package/src/RvfSelectedFeatureHighlightLayer/RvfSelectedFeatureHighlightLayer.tsx +0 -1
- package/src/RvfSharedMobilityLayerGroup2/RvfSharedMobilityLayerGroup2.tsx +1 -1
- package/src/utils/constants.ts +8 -1
- package/src/utils/fullTrajectoryStyle.ts +57 -0
- package/src/utils/getBgColor.ts +16 -1
- package/src/utils/getDelayColor.test.ts +9 -7
- package/src/utils/getDelayColor.ts +7 -7
- package/src/utils/getDelayColorForVehicle.test.ts +16 -9
- package/src/utils/getDelayColorForVehicle.ts +2 -2
- package/src/utils/getMainColorForVehicle.ts +6 -5
- package/src/utils/getRadius.ts +39 -0
- package/src/utils/getTextColor.ts +10 -0
- package/src/utils/realtimeRVFStyle.ts +452 -0
- package/src/utils/sharingGraphqlUtils.ts +0 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { createCanvas } from "mobility-toolbox-js/ol";
|
|
2
|
+
import {
|
|
3
|
+
AnyCanvasContext,
|
|
4
|
+
RealtimeStyleFunction,
|
|
5
|
+
RealtimeStyleOptions,
|
|
6
|
+
RealtimeTrajectory,
|
|
7
|
+
StyleCache,
|
|
8
|
+
ViewState,
|
|
9
|
+
} from "mobility-toolbox-js/types";
|
|
10
|
+
|
|
11
|
+
const cacheDelayBg: StyleCache = {};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Draw circle delay background
|
|
15
|
+
*
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export const getDelayBgCanvas = (
|
|
19
|
+
origin: number,
|
|
20
|
+
radius: number,
|
|
21
|
+
color: string,
|
|
22
|
+
) => {
|
|
23
|
+
const key = `${origin}, ${radius}, ${color}`;
|
|
24
|
+
if (!cacheDelayBg[key]) {
|
|
25
|
+
const canvas = createCanvas(origin * 2, origin * 2);
|
|
26
|
+
if (canvas) {
|
|
27
|
+
const ctx = canvas.getContext("2d") as AnyCanvasContext;
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
ctx.beginPath();
|
|
32
|
+
ctx.arc(origin, origin, radius, 0, 2 * Math.PI, false);
|
|
33
|
+
ctx.fillStyle = color;
|
|
34
|
+
ctx.filter = "blur(1px)";
|
|
35
|
+
ctx.fill();
|
|
36
|
+
cacheDelayBg[key] = canvas;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return cacheDelayBg[key];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const cacheDelayText: StyleCache = {};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Draw delay text
|
|
46
|
+
*
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
export const getDelayTextCanvas = (
|
|
50
|
+
text: string,
|
|
51
|
+
fontSize: number,
|
|
52
|
+
font: string,
|
|
53
|
+
delayColor: string,
|
|
54
|
+
delayOutlineColor = "#000",
|
|
55
|
+
pixelRatio = 1,
|
|
56
|
+
) => {
|
|
57
|
+
const key = `${text}, ${font}, ${delayColor}, ${delayOutlineColor}, ${pixelRatio}`;
|
|
58
|
+
if (!cacheDelayText[key]) {
|
|
59
|
+
const canvas = createCanvas(
|
|
60
|
+
Math.ceil(text.length * fontSize),
|
|
61
|
+
Math.ceil(fontSize + 8 * pixelRatio),
|
|
62
|
+
);
|
|
63
|
+
if (canvas) {
|
|
64
|
+
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
65
|
+
if (!ctx) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
ctx.font = font;
|
|
69
|
+
ctx.textAlign = "left";
|
|
70
|
+
ctx.textBaseline = "middle";
|
|
71
|
+
ctx.font = font;
|
|
72
|
+
ctx.fillStyle = delayColor;
|
|
73
|
+
ctx.strokeStyle = delayOutlineColor;
|
|
74
|
+
ctx.lineWidth = 1.5 * pixelRatio;
|
|
75
|
+
ctx.strokeText(text, 0, fontSize);
|
|
76
|
+
ctx.fillText(text, 0, fontSize);
|
|
77
|
+
cacheDelayText[key] = canvas;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return cacheDelayText[key];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const cacheCircle: StyleCache = {};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Draw colored circle with black border
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
export const getCircleCanvas = (
|
|
91
|
+
origin: number,
|
|
92
|
+
radius: number,
|
|
93
|
+
color: string,
|
|
94
|
+
hasStroke: boolean,
|
|
95
|
+
hasDash: boolean,
|
|
96
|
+
pixelRatio: number,
|
|
97
|
+
) => {
|
|
98
|
+
const key = `${origin}, ${radius}, ${color}, ${hasStroke}, ${hasDash}, ${pixelRatio}`;
|
|
99
|
+
if (!cacheCircle[key]) {
|
|
100
|
+
const canvas = createCanvas(origin * 2, origin * 2);
|
|
101
|
+
if (canvas) {
|
|
102
|
+
const ctx = canvas.getContext("2d") as AnyCanvasContext;
|
|
103
|
+
if (!ctx) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
ctx.fillStyle = color;
|
|
107
|
+
|
|
108
|
+
if (hasStroke) {
|
|
109
|
+
ctx.lineWidth = 1 * pixelRatio;
|
|
110
|
+
ctx.strokeStyle = "#000000";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ctx.beginPath();
|
|
114
|
+
ctx.arc(origin, origin, radius, 0, 2 * Math.PI, false);
|
|
115
|
+
ctx.fill();
|
|
116
|
+
|
|
117
|
+
if (hasDash) {
|
|
118
|
+
ctx.setLineDash([5, 3]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (hasStroke) {
|
|
122
|
+
ctx.stroke();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cacheCircle[key] = canvas;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return cacheCircle[key];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const cacheText: StyleCache = {};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Draw text in the circle
|
|
135
|
+
*
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
export const getTextCanvas = (
|
|
139
|
+
text: string,
|
|
140
|
+
origin: number,
|
|
141
|
+
textSize: number,
|
|
142
|
+
fillColor: string,
|
|
143
|
+
strokeColor: string,
|
|
144
|
+
hasStroke: boolean,
|
|
145
|
+
pixelRatio: number,
|
|
146
|
+
getTextFont: (fontSize: number, text?: string) => string,
|
|
147
|
+
) => {
|
|
148
|
+
const key = `${text}, ${origin}, ${textSize}, ${fillColor},${strokeColor}, ${hasStroke}, ${pixelRatio}`;
|
|
149
|
+
if (!cacheText[key]) {
|
|
150
|
+
const canvas = createCanvas(origin * 2, origin * 2);
|
|
151
|
+
if (canvas) {
|
|
152
|
+
const ctx = canvas.getContext("2d") as AnyCanvasContext;
|
|
153
|
+
if (!ctx) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Draw a stroke to the text only if a provider provides realtime but we don't use it.
|
|
158
|
+
if (hasStroke) {
|
|
159
|
+
ctx.save();
|
|
160
|
+
ctx.textBaseline = "middle";
|
|
161
|
+
ctx.textAlign = "center";
|
|
162
|
+
ctx.font = getTextFont(textSize + 2, text);
|
|
163
|
+
ctx.strokeStyle = strokeColor;
|
|
164
|
+
ctx.strokeText(text, origin, origin);
|
|
165
|
+
ctx.restore();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Draw a text
|
|
169
|
+
ctx.textBaseline = "middle";
|
|
170
|
+
ctx.textAlign = "center";
|
|
171
|
+
ctx.fillStyle = fillColor;
|
|
172
|
+
ctx.font = getTextFont(textSize, text);
|
|
173
|
+
ctx.strokeStyle = strokeColor;
|
|
174
|
+
ctx.strokeText(text, origin, origin);
|
|
175
|
+
ctx.fillText(text, origin, origin);
|
|
176
|
+
|
|
177
|
+
cacheText[key] = canvas;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return cacheText[key];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const cache: StyleCache = {};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A tracker style that take in account the delay.
|
|
187
|
+
*
|
|
188
|
+
* @param {RealtimeTrajectory} trajectory The trajectory to render.
|
|
189
|
+
* @param {ViewState} viewState The view state of the map.
|
|
190
|
+
* @param {RealtimeStyleOptions} options Some options to change the rendering
|
|
191
|
+
* @return a canvas
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
const realtimeRVFStyle: RealtimeStyleFunction = (
|
|
195
|
+
trajectory: RealtimeTrajectory,
|
|
196
|
+
viewState: ViewState,
|
|
197
|
+
options: RealtimeStyleOptions,
|
|
198
|
+
) => {
|
|
199
|
+
const {
|
|
200
|
+
delayDisplay = 300000,
|
|
201
|
+
delayOutlineColor = "#000",
|
|
202
|
+
getBgColor = () => {
|
|
203
|
+
return "#000";
|
|
204
|
+
},
|
|
205
|
+
getDelayColor = () => {
|
|
206
|
+
return "#000";
|
|
207
|
+
},
|
|
208
|
+
getDelayFont = (fontSize: number) => {
|
|
209
|
+
return `bold ${fontSize}px arial, sans-serif`;
|
|
210
|
+
},
|
|
211
|
+
getDelayText = () => {
|
|
212
|
+
return null;
|
|
213
|
+
},
|
|
214
|
+
getMaxRadiusForStrokeAndDelay = () => {
|
|
215
|
+
return 7;
|
|
216
|
+
},
|
|
217
|
+
getMaxRadiusForText = () => {
|
|
218
|
+
return 10;
|
|
219
|
+
},
|
|
220
|
+
getRadius = () => {
|
|
221
|
+
return 0;
|
|
222
|
+
},
|
|
223
|
+
getText = (text?: string) => {
|
|
224
|
+
return text;
|
|
225
|
+
},
|
|
226
|
+
getTextColor = () => {
|
|
227
|
+
return "#000";
|
|
228
|
+
},
|
|
229
|
+
getTextFont = (fontSize: number) => {
|
|
230
|
+
return `bold ${fontSize}px arial, sans-serif`;
|
|
231
|
+
},
|
|
232
|
+
getTextSize = () => {
|
|
233
|
+
return 14;
|
|
234
|
+
},
|
|
235
|
+
hoverVehicleId,
|
|
236
|
+
selectedVehicleId,
|
|
237
|
+
useDelayStyle,
|
|
238
|
+
} = options;
|
|
239
|
+
|
|
240
|
+
const { pixelRatio = 1, zoom } = viewState;
|
|
241
|
+
let { type } = trajectory.properties;
|
|
242
|
+
const {
|
|
243
|
+
delay,
|
|
244
|
+
line,
|
|
245
|
+
operator_provides_realtime_journey: operatorProvidesRealtime,
|
|
246
|
+
state,
|
|
247
|
+
train_id: id,
|
|
248
|
+
} = trajectory.properties;
|
|
249
|
+
let { color, name, text_color: textColor } = line || {};
|
|
250
|
+
|
|
251
|
+
name = getText(name);
|
|
252
|
+
|
|
253
|
+
const cancelled = state === "JOURNEY_CANCELLED";
|
|
254
|
+
|
|
255
|
+
if (!type) {
|
|
256
|
+
type = "Rail";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!name) {
|
|
260
|
+
name = "I";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// if (!textColor) {
|
|
264
|
+
textColor = getTextColor(type); //"#000000";
|
|
265
|
+
// }
|
|
266
|
+
color = getBgColor(type, line);
|
|
267
|
+
|
|
268
|
+
if (color && !color.startsWith("#")) {
|
|
269
|
+
color = `#${color}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!textColor.startsWith("#")) {
|
|
273
|
+
textColor = `#${textColor}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const z = Math.min(Math.floor(zoom || 1), 16);
|
|
277
|
+
const hover = !!(hoverVehicleId && hoverVehicleId === id);
|
|
278
|
+
const selected = !!(selectedVehicleId && selectedVehicleId === id);
|
|
279
|
+
|
|
280
|
+
// Calcul the radius of the circle
|
|
281
|
+
let radius = getRadius(type, z) * pixelRatio;
|
|
282
|
+
const isDisplayStrokeAndDelay =
|
|
283
|
+
radius >= getMaxRadiusForStrokeAndDelay() * pixelRatio;
|
|
284
|
+
|
|
285
|
+
if (hover || selected) {
|
|
286
|
+
radius = isDisplayStrokeAndDelay
|
|
287
|
+
? radius + 5 * pixelRatio
|
|
288
|
+
: 14 * pixelRatio;
|
|
289
|
+
}
|
|
290
|
+
const isDisplayText = radius > getMaxRadiusForText() * pixelRatio;
|
|
291
|
+
|
|
292
|
+
// Optimize the cache key, very important in high zoom level
|
|
293
|
+
let key = `${radius}${hover || selected}`;
|
|
294
|
+
|
|
295
|
+
if (useDelayStyle) {
|
|
296
|
+
key += `${operatorProvidesRealtime}${delay}${cancelled}`;
|
|
297
|
+
} else {
|
|
298
|
+
key += `${color || type}`;
|
|
299
|
+
|
|
300
|
+
if (isDisplayStrokeAndDelay) {
|
|
301
|
+
key += `${cancelled}${delay}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (isDisplayText) {
|
|
306
|
+
key += `${name}${textColor}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!cache[key]) {
|
|
310
|
+
if (radius === 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const margin = 1 * pixelRatio;
|
|
315
|
+
const radiusDelay = radius + 2 * pixelRatio;
|
|
316
|
+
const markerSize = radius * 2;
|
|
317
|
+
const size = radiusDelay * 2 + margin * 2;
|
|
318
|
+
const origin = size / 2;
|
|
319
|
+
|
|
320
|
+
// Draw circle delay background
|
|
321
|
+
let delayBg = null;
|
|
322
|
+
if (isDisplayStrokeAndDelay && delay !== null) {
|
|
323
|
+
delayBg = getDelayBgCanvas(
|
|
324
|
+
origin,
|
|
325
|
+
radiusDelay,
|
|
326
|
+
getDelayColor(delay, cancelled),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Show delay if feature is hovered or if delay is above 5mins.
|
|
331
|
+
let delayText = null;
|
|
332
|
+
let fontSize = 0;
|
|
333
|
+
if (
|
|
334
|
+
isDisplayStrokeAndDelay &&
|
|
335
|
+
(hover || (delay || 0) >= delayDisplay || cancelled)
|
|
336
|
+
) {
|
|
337
|
+
// Draw delay text
|
|
338
|
+
fontSize =
|
|
339
|
+
Math.max(
|
|
340
|
+
cancelled ? 19 : 14,
|
|
341
|
+
Math.min(cancelled ? 19 : 17, radius * 1.2),
|
|
342
|
+
) * pixelRatio;
|
|
343
|
+
const text = getDelayText(delay, cancelled);
|
|
344
|
+
|
|
345
|
+
if (text) {
|
|
346
|
+
delayText = getDelayTextCanvas(
|
|
347
|
+
text,
|
|
348
|
+
fontSize,
|
|
349
|
+
getDelayFont(fontSize, text),
|
|
350
|
+
getDelayColor(delay, cancelled, true),
|
|
351
|
+
delayOutlineColor,
|
|
352
|
+
pixelRatio,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Draw colored circle with black border
|
|
358
|
+
let circleFillColor;
|
|
359
|
+
if (useDelayStyle) {
|
|
360
|
+
circleFillColor = getDelayColor(delay, cancelled);
|
|
361
|
+
} else {
|
|
362
|
+
circleFillColor = color || getBgColor(type, trajectory);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const hasStroke = isDisplayStrokeAndDelay || hover || selected;
|
|
366
|
+
|
|
367
|
+
const hasDash =
|
|
368
|
+
!!isDisplayStrokeAndDelay &&
|
|
369
|
+
!!useDelayStyle &&
|
|
370
|
+
delay === null &&
|
|
371
|
+
operatorProvidesRealtime === "yes";
|
|
372
|
+
|
|
373
|
+
const circle = getCircleCanvas(
|
|
374
|
+
origin,
|
|
375
|
+
radius,
|
|
376
|
+
circleFillColor,
|
|
377
|
+
hasStroke,
|
|
378
|
+
hasDash,
|
|
379
|
+
pixelRatio,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Create the canvas
|
|
383
|
+
const width = size + (delayText?.width || 0) * 2;
|
|
384
|
+
const height = size;
|
|
385
|
+
const canvas = createCanvas(width, height);
|
|
386
|
+
if (canvas) {
|
|
387
|
+
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
388
|
+
if (!ctx) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// The renderTrajectories will center the image on the vehicle positions.
|
|
393
|
+
const originX = delayText?.width || 0;
|
|
394
|
+
|
|
395
|
+
if (delayBg) {
|
|
396
|
+
ctx.drawImage(delayBg, originX, 0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (circle) {
|
|
400
|
+
ctx.drawImage(circle, originX, 0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Draw text in the circle
|
|
404
|
+
let circleText = null;
|
|
405
|
+
if (isDisplayText) {
|
|
406
|
+
const fontSize2 = Math.max(radius, 10);
|
|
407
|
+
const textSize = getTextSize(
|
|
408
|
+
ctx,
|
|
409
|
+
markerSize,
|
|
410
|
+
name,
|
|
411
|
+
fontSize2,
|
|
412
|
+
getTextFont,
|
|
413
|
+
);
|
|
414
|
+
const textColor2 = !useDelayStyle
|
|
415
|
+
? textColor || getTextColor(type)
|
|
416
|
+
: "#000000";
|
|
417
|
+
const hasStroke2 =
|
|
418
|
+
!!useDelayStyle &&
|
|
419
|
+
delay === null &&
|
|
420
|
+
operatorProvidesRealtime === "yes";
|
|
421
|
+
|
|
422
|
+
circleText = getTextCanvas(
|
|
423
|
+
name,
|
|
424
|
+
origin,
|
|
425
|
+
textSize,
|
|
426
|
+
textColor2,
|
|
427
|
+
circleFillColor,
|
|
428
|
+
hasStroke2,
|
|
429
|
+
pixelRatio,
|
|
430
|
+
getTextFont,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (circleText) {
|
|
435
|
+
ctx.drawImage(circleText, originX, 0);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (delayText) {
|
|
439
|
+
ctx.drawImage(
|
|
440
|
+
delayText,
|
|
441
|
+
originX + Math.ceil(origin + radiusDelay) + margin,
|
|
442
|
+
Math.ceil(origin - fontSize),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
cache[key] = canvas;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return cache[key];
|
|
451
|
+
};
|
|
452
|
+
export default realtimeRVFStyle;
|
|
@@ -285,7 +285,6 @@ export const fetchSharingStations = async (
|
|
|
285
285
|
);
|
|
286
286
|
const featureCollection: FeatureCollection<Point, SharingStation> = {
|
|
287
287
|
features: stations.map((station: SharingStation) => {
|
|
288
|
-
console.log("station", station);
|
|
289
288
|
return {
|
|
290
289
|
geometry: {
|
|
291
290
|
coordinates: [station.lon, station.lat],
|