gtfs-to-html 2.9.0 → 2.9.2

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/config-sample.json +68 -0
  2. package/dist/app/index.js +2 -1
  3. package/dist/app/index.js.map +1 -1
  4. package/dist/bin/gtfs-to-html.js +16 -9
  5. package/dist/bin/gtfs-to-html.js.map +1 -1
  6. package/dist/index.js +16 -9
  7. package/dist/index.js.map +1 -1
  8. package/docker/Dockerfile +14 -0
  9. package/docker/README.md +5 -0
  10. package/docker/config.json +21 -0
  11. package/docker/docker-compose.yml +10 -0
  12. package/examples/stop_attributes.txt +6 -0
  13. package/examples/timetable_notes.txt +8 -0
  14. package/examples/timetable_notes_references.txt +8 -0
  15. package/examples/timetable_pages.txt +3 -0
  16. package/examples/timetable_stop_order.txt +16 -0
  17. package/examples/timetables.txt +9 -0
  18. package/package.json +8 -3
  19. package/views/default/css/overview_styles.css +198 -0
  20. package/views/default/css/timetable_pdf_styles.css +69 -0
  21. package/views/default/css/timetable_styles.css +522 -0
  22. package/views/default/formatting_functions.pug +104 -0
  23. package/views/default/js/system-map.js +594 -0
  24. package/views/default/js/timetable-map.js +753 -0
  25. package/views/default/js/timetable-menu.js +57 -0
  26. package/views/default/layout.pug +11 -0
  27. package/views/default/overview.pug +27 -0
  28. package/views/default/overview_full.pug +16 -0
  29. package/views/default/timetable_continuation_as.pug +7 -0
  30. package/views/default/timetable_continuation_from.pug +7 -0
  31. package/views/default/timetable_horizontal.pug +42 -0
  32. package/views/default/timetable_hourly.pug +30 -0
  33. package/views/default/timetable_map.pug +15 -0
  34. package/views/default/timetable_menu.pug +48 -0
  35. package/views/default/timetable_note_symbol.pug +5 -0
  36. package/views/default/timetable_stop_name.pug +13 -0
  37. package/views/default/timetable_stoptime.pug +17 -0
  38. package/views/default/timetable_vertical.pug +67 -0
  39. package/views/default/timetablepage.pug +64 -0
  40. package/views/default/timetablepage_full.pug +25 -0
@@ -0,0 +1,594 @@
1
+ /* global window, document, _, $, mapboxgl */
2
+ /* eslint prefer-arrow-callback: "off", no-unused-vars: "off" */
3
+
4
+ const maps = {};
5
+
6
+ function formatRouteColor(route) {
7
+ return route.route_color || '#000000';
8
+ }
9
+
10
+ function formatRouteTextColor(route) {
11
+ return route.route_text_color || '#FFFFFF';
12
+ }
13
+
14
+ function formatRoute(route) {
15
+ const html = route.route_url
16
+ ? $('<a>').attr('href', route.route_url)
17
+ : $('<div>');
18
+
19
+ html.addClass('map-route-item');
20
+
21
+ // Only add color swatch if route has a color
22
+ const routeItemDivs = [];
23
+
24
+ if (route.route_color) {
25
+ routeItemDivs.push(
26
+ $('<div>')
27
+ .addClass('route-color-swatch')
28
+ .css('backgroundColor', formatRouteColor(route))
29
+ .css('color', formatRouteTextColor(route))
30
+ .text(route.route_short_name ?? ''),
31
+ );
32
+ }
33
+ routeItemDivs.push(
34
+ $('<div>')
35
+ .addClass('underline-hover')
36
+ .text(route.route_long_name ?? `Route ${route.route_short_name}`),
37
+ );
38
+
39
+ html.append(routeItemDivs);
40
+
41
+ return html.prop('outerHTML');
42
+ }
43
+
44
+ function formatRoutePopup(features) {
45
+ const html = $('<div>');
46
+
47
+ if (features.length > 1) {
48
+ $('<div>').addClass('popup-title').text('Routes').appendTo(html);
49
+ }
50
+
51
+ $(html).append(features.map((feature) => formatRoute(feature.properties)));
52
+
53
+ return html.prop('outerHTML');
54
+ }
55
+
56
+ function formatStopPopup(feature) {
57
+ const routes = JSON.parse(feature.properties.routes);
58
+ const html = $('<div>');
59
+
60
+ $('<div>')
61
+ .addClass('popup-title')
62
+ .text(feature.properties.stop_name)
63
+ .appendTo(html);
64
+
65
+ if (feature.properties.stop_code ?? false) {
66
+ $('<div>')
67
+ .html([
68
+ $('<label>').addClass('popup-label').text('Stop Code:'),
69
+ $('<strong>').text(feature.properties.stop_code),
70
+ ])
71
+ .appendTo(html);
72
+ }
73
+
74
+ $('<label>').text('Routes Served:').appendTo(html);
75
+
76
+ $(html).append(
77
+ $('<div>')
78
+ .addClass('route-list')
79
+ .html(routes.map((route) => formatRoute(route))),
80
+ );
81
+
82
+ $('<a>')
83
+ .addClass('btn-blue btn-sm')
84
+ .prop(
85
+ 'href',
86
+ `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${feature.geometry.coordinates[1]},${feature.geometry.coordinates[0]}&heading=0&pitch=0&fov=90`,
87
+ )
88
+ .prop('target', '_blank')
89
+ .prop('rel', 'noopener noreferrer')
90
+ .html('View on Streetview')
91
+ .appendTo(html);
92
+
93
+ return html.prop('outerHTML');
94
+ }
95
+
96
+ function getBounds(geojson) {
97
+ const bounds = new mapboxgl.LngLatBounds();
98
+ for (const feature of geojson.features) {
99
+ if (feature.geometry.type.toLowerCase() === 'point') {
100
+ bounds.extend(feature.geometry.coordinates);
101
+ } else if (feature.geometry.type.toLowerCase() === 'linestring') {
102
+ for (const coordinate of feature.geometry.coordinates) {
103
+ bounds.extend(coordinate);
104
+ }
105
+ } else if (feature.geometry.type.toLowerCase() === 'multilinestring') {
106
+ for (const linestring of feature.geometry.coordinates) {
107
+ for (const coordinate of linestring) {
108
+ bounds.extend(coordinate);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return bounds;
115
+ }
116
+
117
+ function createSystemMap(id, geojson) {
118
+ const defaultRouteColor = '#000000';
119
+ const lineLayout = {
120
+ 'line-join': 'round',
121
+ 'line-cap': 'round',
122
+ };
123
+
124
+ if (!geojson || geojson.features.length === 0) {
125
+ $('#' + id).hide();
126
+ return false;
127
+ }
128
+
129
+ const bounds = getBounds(geojson);
130
+ const map = new mapboxgl.Map({
131
+ container: id,
132
+ style: 'mapbox://styles/mapbox/light-v11',
133
+ center: bounds.getCenter(),
134
+ zoom: 12,
135
+ });
136
+ const routes = {};
137
+
138
+ for (const feature of geojson.features) {
139
+ routes[feature.properties.route_id] = feature.properties;
140
+ }
141
+
142
+ map.scrollZoom.disable();
143
+ map.addControl(new mapboxgl.NavigationControl());
144
+
145
+ map.on('load', () => {
146
+ map.fitBounds(bounds, {
147
+ padding: 20,
148
+ duration: 0,
149
+ });
150
+
151
+ // Turn off Points of Interest labels
152
+ map.setLayoutProperty('poi-label', 'visibility', 'none');
153
+
154
+ // Find the index of the first symbol layer in the map style to put the route lines underneath
155
+ let firstSymbolId;
156
+ for (const layer of map.getStyle().layers) {
157
+ if (layer.type === 'symbol') {
158
+ firstSymbolId = layer.id;
159
+ break;
160
+ }
161
+ }
162
+
163
+ // Add route drop shadow outline first
164
+ map.addLayer(
165
+ {
166
+ id: 'route-line-shadows',
167
+ type: 'line',
168
+ source: {
169
+ type: 'geojson',
170
+ data: geojson,
171
+ },
172
+ paint: {
173
+ 'line-color': '#000000',
174
+ 'line-opacity': 0.3,
175
+ 'line-width': {
176
+ base: 12,
177
+ stops: [
178
+ [14, 20],
179
+ [18, 42],
180
+ ],
181
+ },
182
+ 'line-blur': {
183
+ base: 12,
184
+ stops: [
185
+ [14, 20],
186
+ [18, 42],
187
+ ],
188
+ },
189
+ },
190
+ layout: lineLayout,
191
+ filter: ['!has', 'stop_id'],
192
+ },
193
+ firstSymbolId,
194
+ );
195
+
196
+ // Add highlighted route drop shadow outlines next
197
+ map.addLayer(
198
+ {
199
+ id: 'highlighted-route-line-shadows',
200
+ type: 'line',
201
+ source: {
202
+ type: 'geojson',
203
+ data: geojson,
204
+ },
205
+ paint: {
206
+ 'line-color': '#000000',
207
+ 'line-opacity': 0.3,
208
+ 'line-width': {
209
+ base: 16,
210
+ stops: [
211
+ [14, 24],
212
+ [18, 50],
213
+ ],
214
+ },
215
+ 'line-blur': {
216
+ base: 16,
217
+ stops: [
218
+ [14, 24],
219
+ [18, 50],
220
+ ],
221
+ },
222
+ },
223
+ layout: lineLayout,
224
+ filter: ['==', ['get', 'route_id'], 'none'],
225
+ },
226
+ firstSymbolId,
227
+ );
228
+
229
+ // Add white outlines to routes next
230
+ map.addLayer(
231
+ {
232
+ id: `route-outlines`,
233
+ type: 'line',
234
+ source: {
235
+ type: 'geojson',
236
+ data: geojson,
237
+ },
238
+ paint: {
239
+ 'line-color': '#FFFFFF',
240
+ 'line-opacity': 1,
241
+ 'line-width': {
242
+ base: 8,
243
+ stops: [
244
+ [14, 12],
245
+ [18, 32],
246
+ ],
247
+ },
248
+ },
249
+ layout: lineLayout,
250
+ filter: ['has', 'route_id'],
251
+ },
252
+ firstSymbolId,
253
+ );
254
+
255
+ // Add route lines next
256
+ map.addLayer(
257
+ {
258
+ id: 'routes',
259
+ type: 'line',
260
+ source: {
261
+ type: 'geojson',
262
+ data: geojson,
263
+ },
264
+ paint: {
265
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
266
+ 'line-opacity': 1,
267
+ 'line-width': {
268
+ base: 4,
269
+ stops: [
270
+ [14, 6],
271
+ [18, 16],
272
+ ],
273
+ },
274
+ },
275
+ layout: lineLayout,
276
+ filter: ['has', 'route_id'],
277
+ },
278
+ firstSymbolId,
279
+ );
280
+
281
+ // Add highlighted route white outlines next
282
+ map.addLayer(
283
+ {
284
+ id: `highlighted-route-outlines`,
285
+ type: 'line',
286
+ source: {
287
+ type: 'geojson',
288
+ data: geojson,
289
+ },
290
+ paint: {
291
+ 'line-color': '#FFFFFF',
292
+ 'line-opacity': 1,
293
+ 'line-width': {
294
+ base: 10,
295
+ stops: [
296
+ [14, 16],
297
+ [18, 40],
298
+ ],
299
+ },
300
+ },
301
+ layout: lineLayout,
302
+ filter: ['==', ['get', 'route_id'], 'none'],
303
+ },
304
+ firstSymbolId,
305
+ );
306
+
307
+ // Add highlighted route lines next
308
+ map.addLayer(
309
+ {
310
+ id: 'highlighted-routes',
311
+ type: 'line',
312
+ source: {
313
+ type: 'geojson',
314
+ data: geojson,
315
+ },
316
+ paint: {
317
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
318
+ 'line-opacity': 1,
319
+ 'line-width': {
320
+ base: 6,
321
+ stops: [
322
+ [14, 8],
323
+ [18, 20],
324
+ ],
325
+ },
326
+ },
327
+ layout: lineLayout,
328
+ filter: ['==', ['get', 'route_id'], 'none'],
329
+ },
330
+ firstSymbolId,
331
+ );
332
+
333
+ // Add stops when zoomed in
334
+ map.addLayer({
335
+ id: 'stops',
336
+ type: 'circle',
337
+ source: {
338
+ type: 'geojson',
339
+ data: geojson,
340
+ },
341
+ paint: {
342
+ 'circle-color': '#fff',
343
+ 'circle-radius': {
344
+ base: 1.75,
345
+ stops: [
346
+ [12, 4],
347
+ [22, 100],
348
+ ],
349
+ },
350
+ 'circle-stroke-color': '#3F4A5C',
351
+ 'circle-stroke-width': 2,
352
+ 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
353
+ 'circle-stroke-opacity': [
354
+ 'interpolate',
355
+ ['linear'],
356
+ ['zoom'],
357
+ 13,
358
+ 0,
359
+ 13.5,
360
+ 1,
361
+ ],
362
+ },
363
+ filter: ['has', 'stop_id'],
364
+ });
365
+
366
+ // Layer for highlighted stops
367
+ map.addLayer({
368
+ id: 'stops-highlighted',
369
+ type: 'circle',
370
+ source: {
371
+ type: 'geojson',
372
+ data: geojson,
373
+ },
374
+ paint: {
375
+ 'circle-color': '#fff',
376
+ 'circle-radius': {
377
+ base: 1.75,
378
+ stops: [
379
+ [12, 5],
380
+ [22, 125],
381
+ ],
382
+ },
383
+ 'circle-stroke-width': 2,
384
+ 'circle-stroke-color': '#3f4a5c',
385
+ 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
386
+ 'circle-stroke-opacity': [
387
+ 'interpolate',
388
+ ['linear'],
389
+ ['zoom'],
390
+ 13,
391
+ 0,
392
+ 13.5,
393
+ 1,
394
+ ],
395
+ },
396
+ filter: ['==', 'stop_id', ''],
397
+ });
398
+
399
+ // Add labels
400
+ map.addLayer({
401
+ id: 'route-labels',
402
+ type: 'symbol',
403
+ source: {
404
+ type: 'geojson',
405
+ data: geojson,
406
+ },
407
+ layout: {
408
+ 'symbol-placement': 'line',
409
+ 'text-field': ['get', 'route_short_name'],
410
+ 'text-size': 14,
411
+ },
412
+ paint: {
413
+ 'text-color': '#000000',
414
+ 'text-halo-width': 2,
415
+ 'text-halo-color': '#ffffff',
416
+ },
417
+ filter: ['has', 'route_short_name'],
418
+ });
419
+
420
+ map.on('mousemove', (event) => {
421
+ const features = map.queryRenderedFeatures(event.point, {
422
+ layers: ['routes', 'route-outlines', 'stops-highlighted', 'stops'],
423
+ });
424
+ if (features.length > 0) {
425
+ map.getCanvas().style.cursor = 'pointer';
426
+ highlightRoutes(
427
+ _.compact(
428
+ _.uniq(features.map((feature) => feature.properties.route_id)),
429
+ ),
430
+ );
431
+
432
+ if (features.some((feature) => feature.layer.id === 'stops')) {
433
+ highlightStop(
434
+ features.find((feature) => feature.layer.id === 'stops').properties
435
+ .stop_id,
436
+ );
437
+ }
438
+ } else {
439
+ map.getCanvas().style.cursor = '';
440
+ unHighlightRoutes();
441
+ unHighlightStop();
442
+ }
443
+ });
444
+
445
+ map.on('click', (event) => {
446
+ // Set bbox as 5px rectangle area around clicked point
447
+ const bbox = [
448
+ [event.point.x - 5, event.point.y - 5],
449
+ [event.point.x + 5, event.point.y + 5],
450
+ ];
451
+
452
+ const stopFeatures = map.queryRenderedFeatures(bbox, {
453
+ layers: ['stops-highlighted', 'stops'],
454
+ });
455
+
456
+ if (stopFeatures && stopFeatures.length > 0) {
457
+ // Get the stop feature and show popup
458
+ const stopFeature = stopFeatures[0];
459
+
460
+ new mapboxgl.Popup()
461
+ .setLngLat(stopFeature.geometry.coordinates)
462
+ .setHTML(formatStopPopup(stopFeature))
463
+ .addTo(map);
464
+ } else {
465
+ const routeFeatures = map.queryRenderedFeatures(bbox, {
466
+ layers: ['routes', 'route-outlines'],
467
+ });
468
+
469
+ if (routeFeatures && routeFeatures.length > 0) {
470
+ const routes = _.orderBy(
471
+ _.uniqBy(
472
+ routeFeatures,
473
+ (feature) => feature.properties.route_short_name,
474
+ ),
475
+ (feature) =>
476
+ Number.parseInt(feature.properties.route_short_name, 10),
477
+ );
478
+
479
+ new mapboxgl.Popup()
480
+ .setLngLat(event.lngLat)
481
+ .setHTML(formatRoutePopup(routes))
482
+ .addTo(map);
483
+ }
484
+ }
485
+ });
486
+
487
+ function highlightStop(stopId) {
488
+ map.setFilter('stops-highlighted', ['==', 'stop_id', stopId]);
489
+ }
490
+
491
+ function unHighlightStop() {
492
+ map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
493
+ }
494
+
495
+ function highlightRoutes(routeIds, zoom) {
496
+ map.setFilter('highlighted-routes', [
497
+ 'all',
498
+ ['has', 'route_short_name'],
499
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
500
+ ]);
501
+ map.setFilter('highlighted-route-outlines', [
502
+ 'all',
503
+ ['has', 'route_short_name'],
504
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
505
+ ]);
506
+ map.setFilter('highlighted-route-line-shadows', [
507
+ 'all',
508
+ ['has', 'route_short_name'],
509
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
510
+ ]);
511
+
512
+ // Show labels only for highlighted route
513
+ map.setFilter('route-labels', [
514
+ 'in',
515
+ ['get', 'route_id'],
516
+ ['literal', routeIds],
517
+ ]);
518
+
519
+ const routeLineOpacity = 0.4;
520
+
521
+ // De-emphasize other routes
522
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
523
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
524
+ map.setPaintProperty(
525
+ 'route-line-shadows',
526
+ 'line-opacity',
527
+ routeLineOpacity,
528
+ );
529
+
530
+ const highlightedFeatures = geojson.features.filter((feature) =>
531
+ routeIds.includes(feature.properties.route_id),
532
+ );
533
+
534
+ if (highlightedFeatures.length > 0 && zoom) {
535
+ const zoomBounds = getBounds({
536
+ features: highlightedFeatures,
537
+ });
538
+ map.fitBounds(zoomBounds, {
539
+ padding: 20,
540
+ });
541
+ }
542
+ }
543
+
544
+ function unHighlightRoutes(zoom) {
545
+ map.setFilter('highlighted-routes', ['==', ['get', 'route_id'], 'none']);
546
+ map.setFilter('highlighted-route-outlines', [
547
+ '==',
548
+ ['get', 'route_id'],
549
+ 'none',
550
+ ]);
551
+ map.setFilter('highlighted-route-line-shadows', [
552
+ '==',
553
+ ['get', 'route_id'],
554
+ 'none',
555
+ ]);
556
+
557
+ // Show labels for all routes
558
+ map.setFilter('route-labels', ['has', 'route_short_name']);
559
+
560
+ const routeLineOpacity = 1;
561
+
562
+ // Re-emphasize other routes
563
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
564
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
565
+ map.setPaintProperty(
566
+ 'route-line-shadows',
567
+ 'line-opacity',
568
+ routeLineOpacity,
569
+ );
570
+
571
+ if (zoom) {
572
+ map.fitBounds(bounds);
573
+ }
574
+ }
575
+
576
+ // On table hover, highlight route on map
577
+ $(() => {
578
+ $('.overview-list a').hover((event) => {
579
+ const routeIdString = $(event.target).data('route-ids');
580
+ if (routeIdString) {
581
+ const routeIds = routeIdString.toString().split(',');
582
+ highlightRoutes(routeIds, true);
583
+ }
584
+ });
585
+
586
+ $('.overview-list').hover(
587
+ () => {},
588
+ () => unHighlightRoutes(true),
589
+ );
590
+ });
591
+ });
592
+
593
+ maps[id] = map;
594
+ }