mobility-toolbox-js 1.6.0-beta.4 → 1.6.0-beta.8

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.
@@ -12,19 +12,23 @@ const trackerRadiusMapping = {
12
12
  };
13
13
 
14
14
  /**
15
+ * Trajserv value: 'Tram', 'Subway / Metro / S-Bahn', 'Train', 'Bus', 'Ferry', 'Cable Car', 'Gondola', 'Funicular', 'Long distance bus', 'Rail',
16
+ * New endpoint use Rail instead of Train.
17
+ * New tracker values: null, "tram", "subway", "rail", "bus", "ferry", "cablecar", "gondola", "funicular", "coach".
18
+ *
15
19
  * @ignore
16
20
  */
17
21
  export const types = [
18
- 'Tram',
19
- 'Subway / Metro / S-Bahn',
20
- 'Train',
21
- 'Bus',
22
- 'Ferry',
23
- 'Cable Car',
24
- 'Gondola',
25
- 'Funicular',
26
- 'Long distance bus',
27
- 'Rail', // New endpoint use Rail instead of Train.
22
+ /^Tram/i,
23
+ /^Subway( \/ Metro \/ S-Bahn)?/i,
24
+ /^Train/i,
25
+ /^Bus/i,
26
+ /^Ferry/i,
27
+ /^Cable ?Car/i,
28
+ /^Gondola/i,
29
+ /^Funicular/i,
30
+ /^(Long distance bus|coach)/i,
31
+ /^Rail/i, // New endpoint use Rail instead of Train.
28
32
  ];
29
33
 
30
34
  /**
@@ -89,10 +93,9 @@ export const timeSteps = [
89
93
  /**
90
94
  * @ignore
91
95
  */
92
- const getTypeIndex = (type) => {
96
+ export const getTypeIndex = (type) => {
93
97
  if (typeof type === 'string') {
94
- const matched = types.find((t) => new RegExp(type).test(t));
95
- return types.indexOf(matched);
98
+ return types.findIndex((t) => t.test(type));
96
99
  }
97
100
  return type;
98
101
  };
@@ -0,0 +1,38 @@
1
+ import { getTypeIndex } from './trackerConfig';
2
+
3
+ describe('trackerConfig', () => {
4
+ describe('#getTypeIndex()', () => {
5
+ test("retrurn the type is it's not a string", () => {
6
+ const obj = { foo: 'foo' };
7
+ expect(getTypeIndex(obj)).toBe(obj);
8
+ expect(getTypeIndex(0)).toBe(0);
9
+ expect(getTypeIndex(null)).toBe(null);
10
+ expect(getTypeIndex(undefined)).toBe(undefined);
11
+ });
12
+
13
+ test('find good index for old trajserv values', () => {
14
+ expect(getTypeIndex('Tram')).toBe(0);
15
+ expect(getTypeIndex('Subway / Metro / S-Bahn')).toBe(1);
16
+ expect(getTypeIndex('Train')).toBe(2);
17
+ expect(getTypeIndex('Bus')).toBe(3);
18
+ expect(getTypeIndex('Ferry')).toBe(4);
19
+ expect(getTypeIndex('Cable Car')).toBe(5);
20
+ expect(getTypeIndex('Gondola')).toBe(6);
21
+ expect(getTypeIndex('Funicular')).toBe(7);
22
+ expect(getTypeIndex('Long distance bus')).toBe(8);
23
+ expect(getTypeIndex('Rail')).toBe(9);
24
+ });
25
+
26
+ test('find good index for new tracker values', () => {
27
+ expect(getTypeIndex('tram')).toBe(0);
28
+ expect(getTypeIndex('subway')).toBe(1);
29
+ expect(getTypeIndex('bus')).toBe(3);
30
+ expect(getTypeIndex('ferry')).toBe(4);
31
+ expect(getTypeIndex('cablecar')).toBe(5);
32
+ expect(getTypeIndex('gondola')).toBe(6);
33
+ expect(getTypeIndex('funicular')).toBe(7);
34
+ expect(getTypeIndex('coach')).toBe(8);
35
+ expect(getTypeIndex('rail')).toBe(9);
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Return a filter functions based on some parameters of a vehicle.
3
+ *
4
+ * @param {string|Array<string>} line - A list of vehicle's name to filter. Names can be separated by a comma. Ex: 'S1,S2,S3'
5
+ * @param {string|Array<string} route - A list of vehicle's route (contained in routeIdentifier property) to filter. Indentifiers can be separated by a comma. Ex: 'id1,id2,id3'
6
+ * @param {string|Array<string} operator A list of vehicle's operator to filter. Operators can be separated by a comma. Ex: 'SBB,DB'
7
+ * @param {Regexp} regexLine - A regex aplly of vehcile's name.
8
+ * @private
9
+ */
10
+ const createFilters = (line, route, operator, regexLine) => {
11
+ const filterList = [];
12
+
13
+ if (!line && !route && !operator && !regexLine) {
14
+ return null;
15
+ }
16
+
17
+ if (regexLine) {
18
+ const regexLineList =
19
+ typeof regexLine === 'string' ? [regexLine] : regexLine;
20
+ const lineFilter = (item) => {
21
+ const name = item.name || (item.line && item.line.name) || '';
22
+ if (!name) {
23
+ return false;
24
+ }
25
+ return regexLineList.some((regexStr) =>
26
+ new RegExp(regexStr, 'i').test(name),
27
+ );
28
+ };
29
+ filterList.push(lineFilter);
30
+ }
31
+
32
+ if (line) {
33
+ const lineFiltersList = typeof line === 'string' ? line.split(',') : line;
34
+ const lineList = lineFiltersList.map((l) =>
35
+ l.replace(/\s+/g, '').toUpperCase(),
36
+ );
37
+ const lineFilter = (item) => {
38
+ const name = (
39
+ item.name ||
40
+ (item.line && item.line.name) ||
41
+ ''
42
+ ).toUpperCase();
43
+ if (!name) {
44
+ return false;
45
+ }
46
+ return lineList.includes(name);
47
+ };
48
+ filterList.push(lineFilter);
49
+ }
50
+
51
+ if (route) {
52
+ const routes = typeof route === 'string' ? route.split(',') : route;
53
+ const routeList = routes.map((item) => parseInt(item, 10));
54
+ const routeFilter = (item) => {
55
+ const routeId = parseInt(item.routeIdentifier.split('.')[0], 10);
56
+ return routeList.includes(routeId);
57
+ };
58
+ filterList.push(routeFilter);
59
+ }
60
+
61
+ if (operator) {
62
+ const operatorList = typeof operator === 'string' ? [operator] : operator;
63
+ const operatorFilter = (item) =>
64
+ operatorList.some((op) => new RegExp(op, 'i').test(item.operator));
65
+ filterList.push(operatorFilter);
66
+ }
67
+
68
+ if (!filterList.length) {
69
+ return null;
70
+ }
71
+
72
+ return (t) => {
73
+ for (let i = 0; i < filterList.length; i += 1) {
74
+ if (!filterList[i](t)) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ };
80
+ };
81
+
82
+ export default createFilters;
@@ -0,0 +1,89 @@
1
+ import createTrackerFilters from './createTrackerFilters';
2
+
3
+ const u1 = {
4
+ routeIdentifier: '001.000827.004:7',
5
+ operator: 'FoO',
6
+ line: {
7
+ name: 'U1',
8
+ },
9
+ };
10
+ const ireta = {
11
+ routeIdentifier: '0022.000827.004:7',
12
+ operator: 'BAR',
13
+ line: {
14
+ name: 'IRETA',
15
+ },
16
+ };
17
+ const arb = {
18
+ routeIdentifier: '00333.000827.004:7',
19
+ operator: 'qux',
20
+ line: {
21
+ name: 'ARB',
22
+ },
23
+ };
24
+
25
+ const trajectories = [u1, ireta, arb];
26
+
27
+ describe('#createTrackerFilter()', () => {
28
+ test('returns null', () => {
29
+ const filterFunc = createTrackerFilters();
30
+ expect(filterFunc).toBe(null);
31
+ });
32
+
33
+ describe('using line', () => {
34
+ test('as string', () => {
35
+ const filterFunc = createTrackerFilters('u1,foo');
36
+ expect(trajectories.filter(filterFunc)).toEqual([u1]);
37
+ });
38
+
39
+ test('as array of string', () => {
40
+ const filterFunc = createTrackerFilters(['u1', 'foo', 'IRETA']);
41
+ expect(trajectories.filter(filterFunc)).toEqual([u1, ireta]);
42
+ });
43
+ });
44
+
45
+ describe('using route identifier', () => {
46
+ test('as string', () => {
47
+ const filterFunc = createTrackerFilters(null, '1,foo');
48
+ expect(trajectories.filter(filterFunc)).toEqual([u1]);
49
+ });
50
+
51
+ test('as array of string', () => {
52
+ const filterFunc = createTrackerFilters(null, ['22', 'foo', '1']);
53
+ expect(trajectories.filter(filterFunc)).toEqual([u1, ireta]);
54
+ });
55
+ });
56
+
57
+ describe('using operator', () => {
58
+ test('as string', () => {
59
+ const filterFunc = createTrackerFilters(null, null, 'foo');
60
+ expect(trajectories.filter(filterFunc)).toEqual([u1]);
61
+ });
62
+
63
+ test('as array of string', () => {
64
+ const filterFunc = createTrackerFilters(null, null, ['bar', 'foo', '1']);
65
+ expect(trajectories.filter(filterFunc)).toEqual([u1, ireta]);
66
+ });
67
+ });
68
+
69
+ describe('using regexLine', () => {
70
+ test('as string', () => {
71
+ const filterFunc = createTrackerFilters(
72
+ null,
73
+ null,
74
+ null,
75
+ '^(S|R$|RE|PE|D|IRE|RB|TER)',
76
+ );
77
+ expect(trajectories.filter(filterFunc)).toEqual([ireta]);
78
+ });
79
+
80
+ test('as array of string', () => {
81
+ const filterFunc = createTrackerFilters(null, null, null, [
82
+ '^IR',
83
+ '^ARB$',
84
+ 'foo',
85
+ ]);
86
+ expect(trajectories.filter(filterFunc)).toEqual([ireta, arb]);
87
+ });
88
+ });
89
+ });
@@ -1,3 +1,4 @@
1
1
  export { default as getMapboxStyleUrl } from './getMapboxStyleUrl';
2
2
  export { default as getMapboxMapCopyrights } from './getMapboxMapCopyrights';
3
3
  export { default as removeDuplicate } from './removeDuplicate';
4
+ export { default as trackerStyle } from './trackerStyle';
@@ -0,0 +1,201 @@
1
+ import {
2
+ getRadius,
3
+ getBgColor,
4
+ getDelayColor,
5
+ getDelayText,
6
+ getTextColor,
7
+ getTextSize,
8
+ } from '../trackerConfig';
9
+
10
+ const styleCache = {};
11
+
12
+ const style = (trajectory, viewState, trackerLayer) => {
13
+ const {
14
+ hoverVehicleId,
15
+ selectedVehicleId,
16
+ useDelayStyle,
17
+ delayOutlineColor,
18
+ delayDisplay,
19
+ } = trackerLayer;
20
+
21
+ const {
22
+ zoom,
23
+ pixelRatio,
24
+ operator_provides_realtime_journey: operatorProvidesRealtime,
25
+ } = viewState;
26
+ let { line, type } = trajectory;
27
+ const { id, delay, cancelled = false } = trajectory;
28
+
29
+ if (!type) {
30
+ type = 'Rail';
31
+ }
32
+
33
+ if (!line) {
34
+ line = {};
35
+ }
36
+
37
+ let { name, text_color: textColor, color } = line;
38
+
39
+ if (!name) {
40
+ name = 'I';
41
+ }
42
+
43
+ if (!textColor) {
44
+ textColor = '#000000';
45
+ }
46
+
47
+ if (color && color[0] !== '#') {
48
+ color = `#${color}`;
49
+ }
50
+
51
+ if (textColor[0] !== '#') {
52
+ textColor = `#${textColor}`;
53
+ }
54
+
55
+ const z = Math.min(Math.floor(zoom || 1), 16);
56
+ const hover = hoverVehicleId === id;
57
+ const selected = selectedVehicleId === id;
58
+
59
+ // Calcul the radius of the circle
60
+ let radius = getRadius(type, z) * pixelRatio;
61
+ const isDisplayStrokeAndDelay = radius >= 7 * pixelRatio;
62
+ if (hover || selected) {
63
+ radius = isDisplayStrokeAndDelay
64
+ ? radius + 5 * pixelRatio
65
+ : 14 * pixelRatio;
66
+ }
67
+ const mustDrawText = radius > 10 * pixelRatio;
68
+
69
+ // Optimize the cache key, very important in high zoom level
70
+ let key = `${z}${type}${color}${hover}${selected}${cancelled}${delay}`;
71
+
72
+ if (useDelayStyle) {
73
+ key += `${operatorProvidesRealtime}`;
74
+ }
75
+
76
+ if (mustDrawText) {
77
+ key += `${name}${textColor}`;
78
+ }
79
+
80
+ if (!styleCache[key]) {
81
+ if (radius === 0) {
82
+ styleCache[key] = null;
83
+ return null;
84
+ }
85
+
86
+ const margin = 1 * pixelRatio;
87
+ const radiusDelay = radius + 2;
88
+ const markerSize = radius * 2;
89
+
90
+ const canvas = document.createElement('canvas');
91
+ // add space for delay information
92
+ canvas.width = radiusDelay * 2 + margin * 2 + 100 * pixelRatio;
93
+ canvas.height = radiusDelay * 2 + margin * 2 + 100 * pixelRatio;
94
+ const ctx = canvas.getContext('2d');
95
+ const origin = canvas.width / 2;
96
+
97
+ if (isDisplayStrokeAndDelay && delay !== null) {
98
+ // Draw circle delay background
99
+ ctx.save();
100
+ ctx.beginPath();
101
+ ctx.arc(origin, origin, radiusDelay, 0, 2 * Math.PI, false);
102
+ ctx.fillStyle = getDelayColor(delay, cancelled);
103
+ ctx.filter = 'blur(1px)';
104
+ ctx.fill();
105
+ ctx.restore();
106
+ }
107
+
108
+ // Show delay if feature is hovered or if delay is above 5mins.
109
+ if (
110
+ isDisplayStrokeAndDelay &&
111
+ (hover || delay >= delayDisplay || cancelled)
112
+ ) {
113
+ // Draw delay text
114
+ ctx.save();
115
+ ctx.textAlign = 'left';
116
+ ctx.textBaseline = 'middle';
117
+ ctx.font = `bold ${Math.max(
118
+ cancelled ? 19 : 14,
119
+ Math.min(cancelled ? 19 : 17, radius * 1.2),
120
+ )}px arial, sans-serif`;
121
+ ctx.fillStyle = getDelayColor(delay, cancelled, true);
122
+
123
+ ctx.strokeStyle = delayOutlineColor;
124
+ ctx.lineWidth = 1.5 * pixelRatio;
125
+ const delayText = getDelayText(delay, cancelled);
126
+ ctx.strokeText(delayText, origin + radiusDelay + margin, origin);
127
+ ctx.fillText(delayText, origin + radiusDelay + margin, origin);
128
+ ctx.restore();
129
+ }
130
+
131
+ // Draw colored circle with black border
132
+ let circleFillColor;
133
+ if (useDelayStyle) {
134
+ circleFillColor = getDelayColor(delay, cancelled);
135
+ } else {
136
+ circleFillColor = color || getBgColor(type);
137
+ }
138
+
139
+ ctx.save();
140
+ if (isDisplayStrokeAndDelay || hover || selected) {
141
+ ctx.lineWidth = 1 * pixelRatio;
142
+ ctx.strokeStyle = '#000000';
143
+ }
144
+ ctx.fillStyle = circleFillColor;
145
+ ctx.beginPath();
146
+ ctx.arc(origin, origin, radius, 0, 2 * Math.PI, false);
147
+ ctx.fill();
148
+ // Dashed outline if a provider provides realtime but we don't use it.
149
+ if (
150
+ isDisplayStrokeAndDelay &&
151
+ useDelayStyle &&
152
+ delay === null &&
153
+ operatorProvidesRealtime === 'yes'
154
+ ) {
155
+ ctx.setLineDash([5, 3]);
156
+ }
157
+ if (isDisplayStrokeAndDelay || hover || selected) {
158
+ ctx.stroke();
159
+ }
160
+ ctx.restore();
161
+
162
+ // Draw text in the circle
163
+ if (mustDrawText) {
164
+ const fontSize = Math.max(radius, 10 * pixelRatio);
165
+ const textSize = getTextSize(ctx, markerSize, name, fontSize);
166
+
167
+ // Draw a stroke to the text only if a provider provides realtime but we don't use it.
168
+ if (
169
+ useDelayStyle &&
170
+ delay === null &&
171
+ operatorProvidesRealtime === 'yes'
172
+ ) {
173
+ ctx.save();
174
+ ctx.textBaseline = 'middle';
175
+ ctx.textAlign = 'center';
176
+ ctx.font = `bold ${textSize + 2}px Arial`;
177
+ ctx.strokeStyle = circleFillColor;
178
+ ctx.strokeText(name, origin, origin);
179
+ ctx.restore();
180
+ }
181
+
182
+ // Draw a text
183
+ ctx.save();
184
+ ctx.textBaseline = 'middle';
185
+ ctx.textAlign = 'center';
186
+ ctx.fillStyle = !useDelayStyle
187
+ ? textColor || getTextColor(type)
188
+ : '#000000';
189
+ ctx.font = `bold ${textSize}px Arial`;
190
+ ctx.strokeStyle = circleFillColor;
191
+ ctx.strokeText(name, origin, origin);
192
+ ctx.fillText(name, origin, origin);
193
+ ctx.restore();
194
+ }
195
+
196
+ styleCache[key] = canvas;
197
+ }
198
+
199
+ return styleCache[key];
200
+ };
201
+ export default style;