gtfs-to-html 2.3.3 → 2.4.0

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.
@@ -36,6 +36,28 @@ function formatRoutePopup(features) {
36
36
  return html.prop('outerHTML');
37
37
  }
38
38
 
39
+ function formatStopPopup(feature) {
40
+ const routes = JSON.parse(feature.properties.routes);
41
+ const html = $('<div>');
42
+
43
+ $('<div>')
44
+ .addClass('popup-title')
45
+ .text(feature.properties.stop_name)
46
+ .appendTo(html);
47
+
48
+ if (feature.properties.stop_code ?? false) {
49
+ $('<label>').addClass('mr-1').text('Stop Code:').appendTo(html);
50
+
51
+ $('<strong>').text(feature.properties.stop_code).appendTo(html);
52
+ }
53
+
54
+ $('<div>').text('Routes Served:').appendTo(html);
55
+
56
+ $(html).append(routes.map((route) => formatRoute(route)));
57
+
58
+ return html.prop('outerHTML');
59
+ }
60
+
39
61
  function getBounds(geojson) {
40
62
  const bounds = new mapboxgl.LngLatBounds();
41
63
  for (const feature of geojson.features) {
@@ -53,8 +75,10 @@ function getBounds(geojson) {
53
75
 
54
76
  function createSystemMap(id, geojson) {
55
77
  const defaultRouteColor = '#FF4728';
56
- const routeLayerIds = [];
57
- const routeBackgroundLayerIds = [];
78
+ const lineLayout = {
79
+ 'line-join': 'round',
80
+ 'line-cap': 'round',
81
+ };
58
82
 
59
83
  if (!geojson || geojson.features.length === 0) {
60
84
  $('#' + id).hide();
@@ -83,6 +107,9 @@ function createSystemMap(id, geojson) {
83
107
  duration: 0,
84
108
  });
85
109
 
110
+ // Turn of Points of Interest labels
111
+ map.setLayoutProperty('poi-label', 'visibility', 'none');
112
+
86
113
  // Find the index of the first symbol layer in the map style
87
114
  let firstSymbolId;
88
115
  for (const layer of map.getStyle().layers) {
@@ -92,120 +119,400 @@ function createSystemMap(id, geojson) {
92
119
  }
93
120
  }
94
121
 
95
- // Add white outlines to routes first
96
- for (const routeId of Object.keys(routes)) {
97
- routeBackgroundLayerIds.push(`${routeId}outline`);
98
- map.addLayer(
99
- {
100
- id: `${routeId}outline`,
101
- type: 'line',
102
- source: {
103
- type: 'geojson',
104
- data: geojson,
122
+ // Add route drop shadow outline first
123
+ map.addLayer(
124
+ {
125
+ id: 'route-line-shadows',
126
+ type: 'line',
127
+ source: {
128
+ type: 'geojson',
129
+ data: geojson,
130
+ },
131
+ paint: {
132
+ 'line-color': '#000000',
133
+ 'line-opacity': 0.3,
134
+ 'line-width': {
135
+ base: 12,
136
+ stops: [
137
+ [14, 20],
138
+ [18, 42],
139
+ ],
105
140
  },
106
- paint: {
107
- 'line-color': '#FFFFFF',
108
- 'line-opacity': 1,
109
- 'line-width': 6,
141
+ 'line-blur': {
142
+ base: 12,
143
+ stops: [
144
+ [14, 20],
145
+ [18, 42],
146
+ ],
110
147
  },
111
- layout: {
112
- 'line-join': 'round',
113
- 'line-cap': 'round',
148
+ },
149
+ layout: lineLayout,
150
+ filter: ['!has', 'stop_id'],
151
+ },
152
+ firstSymbolId
153
+ );
154
+
155
+ // Add highlighted route drop shadow outlines next
156
+ map.addLayer(
157
+ {
158
+ id: 'highlighted-route-line-shadows',
159
+ type: 'line',
160
+ source: {
161
+ type: 'geojson',
162
+ data: geojson,
163
+ },
164
+ paint: {
165
+ 'line-color': '#000000',
166
+ 'line-opacity': 0.3,
167
+ 'line-width': {
168
+ base: 16,
169
+ stops: [
170
+ [14, 24],
171
+ [18, 50],
172
+ ],
173
+ },
174
+ 'line-blur': {
175
+ base: 16,
176
+ stops: [
177
+ [14, 24],
178
+ [18, 50],
179
+ ],
114
180
  },
115
- filter: ['==', 'route_id', routeId],
116
181
  },
117
- firstSymbolId
118
- );
119
- }
182
+ layout: lineLayout,
183
+ filter: ['==', ['get', 'route_id'], 'none'],
184
+ },
185
+ firstSymbolId
186
+ );
187
+
188
+ // Add white outlines to routes next
189
+ map.addLayer(
190
+ {
191
+ id: `route-outlines`,
192
+ type: 'line',
193
+ source: {
194
+ type: 'geojson',
195
+ data: geojson,
196
+ },
197
+ paint: {
198
+ 'line-color': '#FFFFFF',
199
+ 'line-opacity': 1,
200
+ 'line-width': {
201
+ base: 8,
202
+ stops: [
203
+ [14, 12],
204
+ [18, 32],
205
+ ],
206
+ },
207
+ },
208
+ layout: lineLayout,
209
+ filter: ['has', 'route_id'],
210
+ },
211
+ firstSymbolId
212
+ );
213
+
214
+ // Add highlighted route white outlines next
215
+ map.addLayer(
216
+ {
217
+ id: `highlighted-route-outlines`,
218
+ type: 'line',
219
+ source: {
220
+ type: 'geojson',
221
+ data: geojson,
222
+ },
223
+ paint: {
224
+ 'line-color': '#FFFFFF',
225
+ 'line-opacity': 1,
226
+ 'line-width': {
227
+ base: 10,
228
+ stops: [
229
+ [14, 16],
230
+ [18, 40],
231
+ ],
232
+ },
233
+ },
234
+ layout: lineLayout,
235
+ filter: ['==', ['get', 'route_id'], 'none'],
236
+ },
237
+ firstSymbolId
238
+ );
120
239
 
121
240
  // Add route lines next
122
- for (const routeId of Object.keys(routes)) {
123
- routeLayerIds.push(routeId);
124
- const routeColor = routes[routeId].route_color || defaultRouteColor;
125
- map.addLayer(
126
- {
127
- id: routeId,
128
- type: 'line',
129
- source: {
130
- type: 'geojson',
131
- data: geojson,
241
+ map.addLayer(
242
+ {
243
+ id: 'routes',
244
+ type: 'line',
245
+ source: {
246
+ type: 'geojson',
247
+ data: geojson,
248
+ },
249
+ paint: {
250
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
251
+ 'line-opacity': 1,
252
+ 'line-width': {
253
+ base: 4,
254
+ stops: [
255
+ [14, 6],
256
+ [18, 16],
257
+ ],
258
+ },
259
+ },
260
+ layout: lineLayout,
261
+ filter: ['has', 'route_id'],
262
+ },
263
+ firstSymbolId
264
+ );
265
+
266
+ // Add highlighted route lines next
267
+ map.addLayer(
268
+ {
269
+ id: 'highlighted-routes',
270
+ type: 'line',
271
+ source: {
272
+ type: 'geojson',
273
+ data: geojson,
274
+ },
275
+ paint: {
276
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
277
+ 'line-opacity': 1,
278
+ 'line-width': {
279
+ base: 6,
280
+ stops: [
281
+ [14, 8],
282
+ [18, 20],
283
+ ],
132
284
  },
133
- paint: {
134
- 'line-color': routeColor,
135
- 'line-opacity': 1,
136
- 'line-width': 2,
285
+ },
286
+ layout: lineLayout,
287
+ filter: ['==', ['get', 'route_id'], 'none'],
288
+ },
289
+ firstSymbolId
290
+ );
291
+
292
+ // Add stops when zoomed in
293
+ map.addLayer(
294
+ {
295
+ id: 'stops',
296
+ type: 'circle',
297
+ source: {
298
+ type: 'geojson',
299
+ data: geojson,
300
+ },
301
+ paint: {
302
+ 'circle-color': '#fff',
303
+ 'circle-radius': {
304
+ base: 1.75,
305
+ stops: [
306
+ [12, 4],
307
+ [22, 100],
308
+ ],
137
309
  },
138
- layout: {
139
- 'line-join': 'round',
140
- 'line-cap': 'round',
310
+ 'circle-stroke-color': '#3F4A5C',
311
+ 'circle-stroke-width': 2,
312
+ 'circle-opacity': [
313
+ 'interpolate',
314
+ ['linear'],
315
+ ['zoom'],
316
+ 13,
317
+ 0,
318
+ 13.5,
319
+ 1,
320
+ ],
321
+ 'circle-stroke-opacity': [
322
+ 'interpolate',
323
+ ['linear'],
324
+ ['zoom'],
325
+ 13,
326
+ 0,
327
+ 13.5,
328
+ 1,
329
+ ],
330
+ },
331
+ filter: ['has', 'stop_id'],
332
+ },
333
+ firstSymbolId
334
+ );
335
+
336
+ // Layer for highlighted stops
337
+ map.addLayer(
338
+ {
339
+ id: 'stops-highlighted',
340
+ type: 'circle',
341
+ source: {
342
+ type: 'geojson',
343
+ data: geojson,
344
+ },
345
+ paint: {
346
+ 'circle-color': '#fff',
347
+ 'circle-radius': {
348
+ base: 1.75,
349
+ stops: [
350
+ [12, 5],
351
+ [22, 125],
352
+ ],
141
353
  },
142
- filter: ['==', 'route_id', routeId],
354
+ 'circle-stroke-width': 2,
355
+ 'circle-stroke-color': '#3f4a5c',
356
+ 'circle-opacity': [
357
+ 'interpolate',
358
+ ['linear'],
359
+ ['zoom'],
360
+ 13,
361
+ 0,
362
+ 13.5,
363
+ 1,
364
+ ],
365
+ 'circle-stroke-opacity': [
366
+ 'interpolate',
367
+ ['linear'],
368
+ ['zoom'],
369
+ 13,
370
+ 0,
371
+ 13.5,
372
+ 1,
373
+ ],
143
374
  },
144
- firstSymbolId
145
- );
146
- }
375
+ filter: ['==', 'stop_id', ''],
376
+ },
377
+ firstSymbolId
378
+ );
379
+
380
+ // Add labels
381
+ map.addLayer({
382
+ id: 'route-labels',
383
+ type: 'symbol',
384
+ source: {
385
+ type: 'geojson',
386
+ data: geojson,
387
+ },
388
+ layout: {
389
+ 'symbol-placement': 'line',
390
+ 'text-field': ['get', 'route_short_name'],
391
+ 'text-size': 14,
392
+ },
393
+ paint: {
394
+ 'text-color': '#000000',
395
+ 'text-halo-width': 2,
396
+ 'text-halo-color': '#ffffff',
397
+ },
398
+ filter: ['has', 'route_short_name'],
399
+ });
147
400
 
148
401
  map.on('mousemove', (event) => {
149
402
  const features = map.queryRenderedFeatures(event.point, {
150
- layers: [...routeLayerIds, ...routeBackgroundLayerIds],
403
+ layers: ['routes', 'route-outlines', 'stops-highlighted', 'stops'],
151
404
  });
152
405
  if (features.length > 0) {
153
406
  map.getCanvas().style.cursor = 'pointer';
154
407
  highlightRoutes(
155
- _.uniq(features.map((feature) => feature.properties.route_id))
408
+ _.compact(
409
+ _.uniq(features.map((feature) => feature.properties.route_id))
410
+ )
156
411
  );
412
+
413
+ if (features.some((feature) => feature.layer.id === 'stops')) {
414
+ highlightStop(
415
+ features.find((feature) => feature.layer.id === 'stops').properties
416
+ .stop_id
417
+ );
418
+ }
157
419
  } else {
158
420
  map.getCanvas().style.cursor = '';
159
421
  unHighlightRoutes();
422
+ unHighlightStop();
160
423
  }
161
424
  });
162
425
 
163
426
  map.on('click', (event) => {
164
- // Set bbox as 5px reactangle area around clicked point
427
+ // Set bbox as 5px rectangle area around clicked point
165
428
  const bbox = [
166
429
  [event.point.x - 5, event.point.y - 5],
167
430
  [event.point.x + 5, event.point.y + 5],
168
431
  ];
169
- const features = map.queryRenderedFeatures(bbox, {
170
- layers: routeLayerIds,
432
+
433
+ const stopFeatures = map.queryRenderedFeatures(bbox, {
434
+ layers: ['stops-highlighted', 'stops'],
171
435
  });
172
436
 
173
- if (!features || features.length === 0) {
174
- return;
175
- }
437
+ if (stopFeatures && stopFeatures.length > 0) {
438
+ // Get the stop feature and show popup
439
+ const stopFeature = stopFeatures[0];
176
440
 
177
- const routeFeatures = _.orderBy(
178
- _.uniqBy(features, (feature) => feature.properties.route_short_name),
179
- (feature) => Number.parseInt(feature.properties.route_short_name, 10)
180
- );
441
+ new mapboxgl.Popup()
442
+ .setLngLat(stopFeature.geometry.coordinates)
443
+ .setHTML(formatStopPopup(stopFeature))
444
+ .addTo(map);
445
+ } else {
446
+ const routeFeatures = map.queryRenderedFeatures(bbox, {
447
+ layers: ['routes', 'route-outlines'],
448
+ });
181
449
 
182
- new mapboxgl.Popup()
183
- .setLngLat(event.lngLat)
184
- .setHTML(formatRoutePopup(routeFeatures))
185
- .addTo(map);
450
+ if (routeFeatures && routeFeatures.length > 0) {
451
+ const routes = _.orderBy(
452
+ _.uniqBy(
453
+ routeFeatures,
454
+ (feature) => feature.properties.route_short_name
455
+ ),
456
+ (feature) =>
457
+ Number.parseInt(feature.properties.route_short_name, 10)
458
+ );
459
+
460
+ new mapboxgl.Popup()
461
+ .setLngLat(event.lngLat)
462
+ .setHTML(formatRoutePopup(routes))
463
+ .addTo(map);
464
+ }
465
+ }
186
466
  });
187
467
 
468
+ function highlightStop(stopId) {
469
+ map.setFilter('stops-highlighted', ['==', 'stop_id', stopId]);
470
+ }
471
+
472
+ function unHighlightStop() {
473
+ map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
474
+ }
475
+
188
476
  function highlightRoutes(routeIds, zoom) {
189
- for (const layerId of routeBackgroundLayerIds) {
190
- const color = routeIds.includes(layerId.replace(/outline/, ''))
191
- ? '#FFFD7E'
192
- : '#FFFFFF';
193
- const width = routeIds.includes(layerId.replace(/outline/, ''))
194
- ? 12
195
- : 6;
196
- map.setPaintProperty(layerId, 'line-color', color);
197
- map.setPaintProperty(layerId, 'line-width', width);
198
- }
477
+ map.setFilter('highlighted-routes', [
478
+ 'all',
479
+ ['has', 'route_short_name'],
480
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
481
+ ]);
482
+ map.setFilter('highlighted-route-outlines', [
483
+ 'all',
484
+ ['has', 'route_short_name'],
485
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
486
+ ]);
487
+ map.setFilter('highlighted-route-line-shadows', [
488
+ 'all',
489
+ ['has', 'route_short_name'],
490
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
491
+ ]);
492
+
493
+ // Show labels only for highlighted route
494
+ map.setFilter('route-labels', [
495
+ 'in',
496
+ ['get', 'route_id'],
497
+ ['literal', routeIds],
498
+ ]);
499
+
500
+ const routeLineOpacity = 0.4;
501
+
502
+ // De-emphasize other routes
503
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
504
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
505
+ map.setPaintProperty(
506
+ 'route-line-shadows',
507
+ 'line-opacity',
508
+ routeLineOpacity
509
+ );
199
510
 
200
511
  const highlightedFeatures = geojson.features.filter((feature) =>
201
512
  routeIds.includes(feature.properties.route_id)
202
513
  );
203
514
 
204
- if (highlightedFeatures.length === 0) {
205
- return;
206
- }
207
-
208
- if (zoom) {
515
+ if (highlightedFeatures.length > 0 && zoom) {
209
516
  const zoomBounds = getBounds({
210
517
  features: highlightedFeatures,
211
518
  });
@@ -216,10 +523,31 @@ function createSystemMap(id, geojson) {
216
523
  }
217
524
 
218
525
  function unHighlightRoutes(zoom) {
219
- for (const layerId of routeBackgroundLayerIds) {
220
- map.setPaintProperty(layerId, 'line-color', '#FFFFFF');
221
- map.setPaintProperty(layerId, 'line-width', 6);
222
- }
526
+ map.setFilter('highlighted-routes', ['==', ['get', 'route_id'], 'none']);
527
+ map.setFilter('highlighted-route-outlines', [
528
+ '==',
529
+ ['get', 'route_id'],
530
+ 'none',
531
+ ]);
532
+ map.setFilter('highlighted-route-line-shadows', [
533
+ '==',
534
+ ['get', 'route_id'],
535
+ 'none',
536
+ ]);
537
+
538
+ // Show labels for all routes
539
+ map.setFilter('route-labels', ['has', 'route_short_name']);
540
+
541
+ const routeLineOpacity = 1;
542
+
543
+ // Re-emphasize other routes
544
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
545
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
546
+ map.setPaintProperty(
547
+ 'route-line-shadows',
548
+ 'line-opacity',
549
+ routeLineOpacity
550
+ );
223
551
 
224
552
  if (zoom) {
225
553
  map.fitBounds(bounds);
@@ -229,7 +557,7 @@ function createSystemMap(id, geojson) {
229
557
  // On table hover, highlight route on map
230
558
  $(() => {
231
559
  $('.overview-list a').hover((event) => {
232
- const routeIdString = $(event.target).parents('a').data('route-ids');
560
+ const routeIdString = $(event.target).data('route-ids');
233
561
  if (routeIdString) {
234
562
  const routeIds = routeIdString.toString().split(',');
235
563
  highlightRoutes(routeIds, true);