gtfs-to-html 2.9.14 → 2.10.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.
@@ -1,8 +1,6 @@
1
- /* global window, document, _, $, mapboxgl */
1
+ /* global document, jQuery, _, maplibregl, geojson, mapStyleUrl */
2
2
  /* eslint prefer-arrow-callback: "off", no-unused-vars: "off" */
3
3
 
4
- const maps = {};
5
-
6
4
  function formatRouteColor(route) {
7
5
  return route.route_color || '#000000';
8
6
  }
@@ -18,7 +16,6 @@ function formatRoute(route) {
18
16
 
19
17
  html.addClass('map-route-item');
20
18
 
21
- // Only add color swatch if route has a color
22
19
  const routeItemDivs = [];
23
20
 
24
21
  if (route.route_color) {
@@ -67,13 +64,13 @@ function formatStopPopup(feature) {
67
64
  if (feature.properties.stop_code ?? false) {
68
65
  jQuery('<div>')
69
66
  .html([
70
- jQuery('<label>').addClass('popup-label').text('Stop Code:'),
67
+ jQuery('<div>').addClass('popup-label').text('Stop Code:'),
71
68
  jQuery('<strong>').text(feature.properties.stop_code),
72
69
  ])
73
70
  .appendTo(html);
74
71
  }
75
72
 
76
- jQuery('<label>').text('Routes Served:').appendTo(html);
73
+ jQuery('<div>').addClass('popup-label').text('Routes Served:').appendTo(html);
77
74
 
78
75
  jQuery(html).append(
79
76
  jQuery('<div>')
@@ -96,7 +93,7 @@ function formatStopPopup(feature) {
96
93
  }
97
94
 
98
95
  function getBounds(geojson) {
99
- const bounds = new mapboxgl.LngLatBounds();
96
+ const bounds = new maplibregl.LngLatBounds();
100
97
  for (const feature of geojson.features) {
101
98
  if (feature.geometry.type.toLowerCase() === 'point') {
102
99
  bounds.extend(feature.geometry.coordinates);
@@ -116,7 +113,7 @@ function getBounds(geojson) {
116
113
  return bounds;
117
114
  }
118
115
 
119
- function createSystemMap(id, geojson) {
116
+ function createSystemMap() {
120
117
  const defaultRouteColor = '#000000';
121
118
  const lineLayout = {
122
119
  'line-join': 'round',
@@ -129,9 +126,9 @@ function createSystemMap(id, geojson) {
129
126
  }
130
127
 
131
128
  const bounds = getBounds(geojson);
132
- const map = new mapboxgl.Map({
133
- container: id,
134
- style: 'mapbox://styles/mapbox/light-v11',
129
+ const map = new maplibregl.Map({
130
+ container: 'system_map',
131
+ style: mapStyleUrl,
135
132
  center: bounds.getCenter(),
136
133
  zoom: 12,
137
134
  });
@@ -142,399 +139,493 @@ function createSystemMap(id, geojson) {
142
139
  }
143
140
 
144
141
  map.scrollZoom.disable();
145
- map.addControl(new mapboxgl.NavigationControl());
146
-
147
- map.on('load', () => {
148
- map.fitBounds(bounds, {
149
- padding: 20,
150
- duration: 0,
151
- });
142
+ map.addControl(new maplibregl.NavigationControl());
143
+ map.addControl(new maplibregl.FullscreenControl());
152
144
 
153
- // Turn off Points of Interest labels
154
- map.setLayoutProperty('poi-label', 'visibility', 'none');
145
+ addGeocoder(map, bounds);
155
146
 
156
- // Find the index of the first symbol layer in the map style to put the route lines underneath
157
- let firstSymbolId;
158
- for (const layer of map.getStyle().layers) {
159
- if (layer.type === 'symbol') {
160
- firstSymbolId = layer.id;
161
- break;
162
- }
163
- }
147
+ map.on('load', () => {
148
+ fitMapToBounds(map, bounds);
149
+ disablePointsOfInterest(map);
150
+ addMapLayers(map, geojson, defaultRouteColor, lineLayout);
151
+ setupEventListeners(map, routes);
152
+ });
153
+ }
164
154
 
165
- // Add route drop shadow outline first
166
- map.addLayer(
155
+ function addGeocoder(map, bounds) {
156
+ map.addControl(
157
+ new MaplibreGeocoder(
167
158
  {
168
- id: 'route-line-shadows',
169
- type: 'line',
170
- source: {
171
- type: 'geojson',
172
- data: geojson,
173
- },
174
- paint: {
175
- 'line-color': '#000000',
176
- 'line-opacity': 0.3,
177
- 'line-width': {
178
- base: 12,
179
- stops: [
180
- [14, 20],
181
- [18, 42],
182
- ],
183
- },
184
- 'line-blur': {
185
- base: 12,
186
- stops: [
187
- [14, 20],
188
- [18, 42],
189
- ],
190
- },
159
+ forwardGeocode: async (config) => {
160
+ const features = [];
161
+ try {
162
+ const request = `https://nominatim.openstreetmap.org/search?q=${
163
+ config.query
164
+ }&format=geojson&polygon_geojson=1&addressdetails=1&viewbox=${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}&bounded=1`;
165
+ const response = await fetch(request);
166
+ const geojson = await response.json();
167
+ for (const feature of geojson.features) {
168
+ const center = [
169
+ feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2,
170
+ feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2,
171
+ ];
172
+ const point = {
173
+ type: 'Feature',
174
+ geometry: {
175
+ type: 'Point',
176
+ coordinates: center,
177
+ },
178
+ place_name: feature.properties.display_name,
179
+ properties: feature.properties,
180
+ text: feature.properties.display_name,
181
+ place_type: ['place'],
182
+ center,
183
+ };
184
+ features.push(point);
185
+ }
186
+ } catch (e) {
187
+ console.error(`Failed to forwardGeocode with error: ${e}`);
188
+ }
189
+
190
+ return {
191
+ features,
192
+ type: 'FeatureCollection',
193
+ };
191
194
  },
192
- layout: lineLayout,
193
- filter: ['!has', 'stop_id'],
194
195
  },
195
- firstSymbolId,
196
- );
197
-
198
- // Add highlighted route drop shadow outlines next
199
- map.addLayer(
200
196
  {
201
- id: 'highlighted-route-line-shadows',
202
- type: 'line',
203
- source: {
204
- type: 'geojson',
205
- data: geojson,
206
- },
207
- paint: {
208
- 'line-color': '#000000',
209
- 'line-opacity': 0.3,
210
- 'line-width': {
211
- base: 16,
212
- stops: [
213
- [14, 24],
214
- [18, 50],
215
- ],
216
- },
217
- 'line-blur': {
218
- base: 16,
219
- stops: [
220
- [14, 24],
221
- [18, 50],
222
- ],
223
- },
224
- },
225
- layout: lineLayout,
226
- filter: ['==', ['get', 'route_id'], 'none'],
197
+ maplibregl,
198
+ zoom: 12,
227
199
  },
228
- firstSymbolId,
229
- );
200
+ ),
201
+ 'top-left',
202
+ );
203
+ }
230
204
 
231
- // Add white outlines to routes next
232
- map.addLayer(
233
- {
234
- id: `route-outlines`,
235
- type: 'line',
236
- source: {
237
- type: 'geojson',
238
- data: geojson,
205
+ function fitMapToBounds(map, bounds) {
206
+ map.fitBounds(bounds, {
207
+ padding: 20,
208
+ duration: 0,
209
+ });
210
+ }
211
+
212
+ function disablePointsOfInterest(map) {
213
+ const layers = map.getStyle().layers;
214
+ const poiLayerIds = layers
215
+ .filter((layer) => layer.id.startsWith('poi'))
216
+ ?.map((layer) => layer.id);
217
+ poiLayerIds.forEach((layerId) => {
218
+ map.setLayoutProperty(layerId, 'visibility', 'none');
219
+ });
220
+ }
221
+
222
+ function addMapLayers(map, geojson, defaultRouteColor, lineLayout) {
223
+ const layers = map.getStyle().layers;
224
+ const firstLabelLayerId = layers.find(
225
+ (layer) => layer.type === 'symbol' && layer.id.includes('label'),
226
+ )?.id;
227
+
228
+ addRouteLineShadow(map, geojson, lineLayout, firstLabelLayerId);
229
+ addHighlightedRouteLineShadow(map, geojson, lineLayout, firstLabelLayerId);
230
+ addRouteLineOutline(map, geojson, lineLayout, firstLabelLayerId);
231
+ addHighlightedRouteLineOutline(map, geojson, lineLayout, firstLabelLayerId);
232
+ addRouteLine(map, geojson, defaultRouteColor, lineLayout, firstLabelLayerId);
233
+ addHighlightedRouteLine(
234
+ map,
235
+ geojson,
236
+ defaultRouteColor,
237
+ lineLayout,
238
+ firstLabelLayerId,
239
+ );
240
+ addStops(map, geojson);
241
+ addHighlightedStops(map, geojson);
242
+ addRouteLabels(map, geojson);
243
+ }
244
+
245
+ function getFirstSymbolLayerId(map) {
246
+ const layers = map.getStyle().layers;
247
+ return layers.find((layer) => layer.type === 'symbol').id;
248
+ }
249
+
250
+ function addRouteLineShadow(map, geojson, lineLayout, firstSymbolId) {
251
+ map.addLayer(
252
+ {
253
+ id: 'route-line-shadows',
254
+ type: 'line',
255
+ source: { type: 'geojson', data: geojson },
256
+ paint: {
257
+ 'line-color': '#000000',
258
+ 'line-opacity': 0.3,
259
+ 'line-width': {
260
+ base: 12,
261
+ stops: [
262
+ [14, 20],
263
+ [18, 42],
264
+ ],
239
265
  },
240
- paint: {
241
- 'line-color': '#FFFFFF',
242
- 'line-opacity': 1,
243
- 'line-width': {
244
- base: 8,
245
- stops: [
246
- [14, 12],
247
- [18, 32],
248
- ],
249
- },
266
+ 'line-blur': {
267
+ base: 12,
268
+ stops: [
269
+ [14, 20],
270
+ [18, 42],
271
+ ],
250
272
  },
251
- layout: lineLayout,
252
- filter: ['has', 'route_id'],
253
273
  },
254
- firstSymbolId,
255
- );
274
+ layout: lineLayout,
275
+ filter: ['!has', 'stop_id'],
276
+ },
277
+ firstSymbolId,
278
+ );
279
+ }
256
280
 
257
- // Add route lines next
258
- map.addLayer(
259
- {
260
- id: 'routes',
261
- type: 'line',
262
- source: {
263
- type: 'geojson',
264
- data: geojson,
281
+ function addHighlightedRouteLineShadow(
282
+ map,
283
+ geojson,
284
+ lineLayout,
285
+ firstSymbolId,
286
+ ) {
287
+ map.addLayer(
288
+ {
289
+ id: 'highlighted-route-line-shadows',
290
+ type: 'line',
291
+ source: { type: 'geojson', data: geojson },
292
+ paint: {
293
+ 'line-color': '#000000',
294
+ 'line-opacity': 0.3,
295
+ 'line-width': {
296
+ base: 16,
297
+ stops: [
298
+ [14, 24],
299
+ [18, 50],
300
+ ],
265
301
  },
266
- paint: {
267
- 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
268
- 'line-opacity': 1,
269
- 'line-width': {
270
- base: 4,
271
- stops: [
272
- [14, 6],
273
- [18, 16],
274
- ],
275
- },
302
+ 'line-blur': {
303
+ base: 16,
304
+ stops: [
305
+ [14, 24],
306
+ [18, 50],
307
+ ],
276
308
  },
277
- layout: lineLayout,
278
- filter: ['has', 'route_id'],
279
309
  },
280
- firstSymbolId,
281
- );
310
+ layout: lineLayout,
311
+ filter: ['==', ['get', 'route_id'], 'none'],
312
+ },
313
+ firstSymbolId,
314
+ );
315
+ }
282
316
 
283
- // Add highlighted route white outlines next
284
- map.addLayer(
285
- {
286
- id: `highlighted-route-outlines`,
287
- type: 'line',
288
- source: {
289
- type: 'geojson',
290
- data: geojson,
291
- },
292
- paint: {
293
- 'line-color': '#FFFFFF',
294
- 'line-opacity': 1,
295
- 'line-width': {
296
- base: 10,
297
- stops: [
298
- [14, 16],
299
- [18, 40],
300
- ],
301
- },
317
+ function addRouteLineOutline(map, geojson, lineLayout, firstSymbolId) {
318
+ map.addLayer(
319
+ {
320
+ id: 'route-outlines',
321
+ type: 'line',
322
+ source: { type: 'geojson', data: geojson },
323
+ paint: {
324
+ 'line-color': '#FFFFFF',
325
+ 'line-opacity': 1,
326
+ 'line-width': {
327
+ base: 8,
328
+ stops: [
329
+ [14, 12],
330
+ [18, 32],
331
+ ],
302
332
  },
303
- layout: lineLayout,
304
- filter: ['==', ['get', 'route_id'], 'none'],
305
333
  },
306
- firstSymbolId,
307
- );
334
+ layout: lineLayout,
335
+ filter: ['has', 'route_id'],
336
+ },
337
+ firstSymbolId,
338
+ );
339
+ }
308
340
 
309
- // Add highlighted route lines next
310
- map.addLayer(
311
- {
312
- id: 'highlighted-routes',
313
- type: 'line',
314
- source: {
315
- type: 'geojson',
316
- data: geojson,
317
- },
318
- paint: {
319
- 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
320
- 'line-opacity': 1,
321
- 'line-width': {
322
- base: 6,
323
- stops: [
324
- [14, 8],
325
- [18, 20],
326
- ],
327
- },
341
+ function addHighlightedRouteLineOutline(
342
+ map,
343
+ geojson,
344
+ lineLayout,
345
+ firstSymbolId,
346
+ ) {
347
+ map.addLayer(
348
+ {
349
+ id: 'highlighted-route-outlines',
350
+ type: 'line',
351
+ source: { type: 'geojson', data: geojson },
352
+ paint: {
353
+ 'line-color': '#FFFFFF',
354
+ 'line-opacity': 1,
355
+ 'line-width': {
356
+ base: 10,
357
+ stops: [
358
+ [14, 16],
359
+ [18, 40],
360
+ ],
328
361
  },
329
- layout: lineLayout,
330
- filter: ['==', ['get', 'route_id'], 'none'],
331
362
  },
332
- firstSymbolId,
333
- );
363
+ layout: lineLayout,
364
+ filter: ['==', ['get', 'route_id'], 'none'],
365
+ },
366
+ firstSymbolId,
367
+ );
368
+ }
334
369
 
335
- // Add stops when zoomed in
336
- map.addLayer({
337
- id: 'stops',
338
- type: 'circle',
339
- source: {
340
- type: 'geojson',
341
- data: geojson,
342
- },
370
+ function addRouteLine(
371
+ map,
372
+ geojson,
373
+ defaultRouteColor,
374
+ lineLayout,
375
+ firstSymbolId,
376
+ ) {
377
+ map.addLayer(
378
+ {
379
+ id: 'routes',
380
+ type: 'line',
381
+ source: { type: 'geojson', data: geojson },
343
382
  paint: {
344
- 'circle-color': '#fff',
345
- 'circle-radius': {
346
- base: 1.75,
383
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
384
+ 'line-opacity': 1,
385
+ 'line-width': {
386
+ base: 4,
347
387
  stops: [
348
- [12, 4],
349
- [22, 100],
388
+ [14, 6],
389
+ [18, 16],
350
390
  ],
351
391
  },
352
- 'circle-stroke-color': '#3F4A5C',
353
- 'circle-stroke-width': 2,
354
- 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
355
- 'circle-stroke-opacity': [
356
- 'interpolate',
357
- ['linear'],
358
- ['zoom'],
359
- 13,
360
- 0,
361
- 13.5,
362
- 1,
363
- ],
364
392
  },
365
- filter: ['has', 'stop_id'],
366
- });
393
+ layout: lineLayout,
394
+ filter: ['has', 'route_id'],
395
+ },
396
+ firstSymbolId,
397
+ );
398
+ }
367
399
 
368
- // Layer for highlighted stops
369
- map.addLayer({
370
- id: 'stops-highlighted',
371
- type: 'circle',
372
- source: {
373
- type: 'geojson',
374
- data: geojson,
375
- },
400
+ function addHighlightedRouteLine(
401
+ map,
402
+ geojson,
403
+ defaultRouteColor,
404
+ lineLayout,
405
+ firstSymbolId,
406
+ ) {
407
+ map.addLayer(
408
+ {
409
+ id: 'highlighted-routes',
410
+ type: 'line',
411
+ source: { type: 'geojson', data: geojson },
376
412
  paint: {
377
- 'circle-color': '#fff',
378
- 'circle-radius': {
379
- base: 1.75,
413
+ 'line-color': ['coalesce', ['get', 'route_color'], defaultRouteColor],
414
+ 'line-opacity': 1,
415
+ 'line-width': {
416
+ base: 6,
380
417
  stops: [
381
- [12, 5],
382
- [22, 125],
418
+ [14, 8],
419
+ [18, 20],
383
420
  ],
384
421
  },
385
- 'circle-stroke-width': 2,
386
- 'circle-stroke-color': '#3f4a5c',
387
- 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
388
- 'circle-stroke-opacity': [
389
- 'interpolate',
390
- ['linear'],
391
- ['zoom'],
392
- 13,
393
- 0,
394
- 13.5,
395
- 1,
396
- ],
397
422
  },
398
- filter: ['==', 'stop_id', ''],
399
- });
423
+ layout: lineLayout,
424
+ filter: ['==', ['get', 'route_id'], 'none'],
425
+ },
426
+ firstSymbolId,
427
+ );
428
+ }
400
429
 
401
- // Add labels
402
- map.addLayer({
403
- id: 'route-labels',
404
- type: 'symbol',
405
- source: {
406
- type: 'geojson',
407
- data: geojson,
408
- },
409
- layout: {
410
- 'symbol-placement': 'line',
411
- 'text-field': ['get', 'route_short_name'],
412
- 'text-size': 14,
430
+ function addStops(map, geojson) {
431
+ map.addLayer({
432
+ id: 'stops',
433
+ type: 'circle',
434
+ source: { type: 'geojson', data: geojson },
435
+ paint: {
436
+ 'circle-color': '#fff',
437
+ 'circle-radius': {
438
+ base: 1.75,
439
+ stops: [
440
+ [12, 4],
441
+ [22, 100],
442
+ ],
413
443
  },
414
- paint: {
415
- 'text-color': '#000000',
416
- 'text-halo-width': 2,
417
- 'text-halo-color': '#ffffff',
444
+ 'circle-stroke-color': '#3F4A5C',
445
+ 'circle-stroke-width': 2,
446
+ 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
447
+ 'circle-stroke-opacity': [
448
+ 'interpolate',
449
+ ['linear'],
450
+ ['zoom'],
451
+ 13,
452
+ 0,
453
+ 13.5,
454
+ 1,
455
+ ],
456
+ },
457
+ filter: ['has', 'stop_id'],
458
+ });
459
+ }
460
+
461
+ function addHighlightedStops(map, geojson) {
462
+ map.addLayer({
463
+ id: 'stops-highlighted',
464
+ type: 'circle',
465
+ source: { type: 'geojson', data: geojson },
466
+ paint: {
467
+ 'circle-color': '#fff',
468
+ 'circle-radius': {
469
+ base: 1.75,
470
+ stops: [
471
+ [12, 5],
472
+ [22, 125],
473
+ ],
418
474
  },
419
- filter: ['has', 'route_short_name'],
420
- });
475
+ 'circle-stroke-width': 2,
476
+ 'circle-stroke-color': '#3f4a5c',
477
+ 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 13, 0, 13.5, 1],
478
+ 'circle-stroke-opacity': [
479
+ 'interpolate',
480
+ ['linear'],
481
+ ['zoom'],
482
+ 13,
483
+ 0,
484
+ 13.5,
485
+ 1,
486
+ ],
487
+ },
488
+ filter: ['==', 'stop_id', ''],
489
+ });
490
+ }
421
491
 
422
- map.on('mousemove', (event) => {
423
- const features = map.queryRenderedFeatures(event.point, {
424
- layers: ['routes', 'route-outlines', 'stops-highlighted', 'stops'],
425
- });
426
- if (features.length > 0) {
427
- map.getCanvas().style.cursor = 'pointer';
428
- highlightRoutes(
429
- _.compact(
430
- _.uniq(features.map((feature) => feature.properties.route_id)),
431
- ),
432
- );
433
-
434
- if (features.some((feature) => feature.layer.id === 'stops')) {
435
- highlightStop(
436
- features.find((feature) => feature.layer.id === 'stops').properties
437
- .stop_id,
438
- );
439
- }
440
- } else {
441
- map.getCanvas().style.cursor = '';
442
- unHighlightRoutes();
443
- unHighlightStop();
444
- }
445
- });
492
+ function addRouteLabels(map, geojson) {
493
+ map.addLayer({
494
+ id: 'route-labels',
495
+ type: 'symbol',
496
+ source: { type: 'geojson', data: geojson },
497
+ layout: {
498
+ 'symbol-placement': 'line',
499
+ 'text-field': ['get', 'route_short_name'],
500
+ 'text-size': 14,
501
+ },
502
+ paint: {
503
+ 'text-color': '#000000',
504
+ 'text-halo-width': 2,
505
+ 'text-halo-color': '#ffffff',
506
+ },
507
+ filter: ['has', 'route_short_name'],
508
+ });
509
+ }
446
510
 
447
- map.on('click', (event) => {
448
- // Set bbox as 5px rectangle area around clicked point
449
- const bbox = [
450
- [event.point.x - 5, event.point.y - 5],
451
- [event.point.x + 5, event.point.y + 5],
452
- ];
453
-
454
- const stopFeatures = map.queryRenderedFeatures(bbox, {
455
- layers: ['stops-highlighted', 'stops'],
456
- });
457
-
458
- if (stopFeatures && stopFeatures.length > 0) {
459
- // Get the stop feature and show popup
460
- const stopFeature = stopFeatures[0];
461
-
462
- new mapboxgl.Popup()
463
- .setLngLat(stopFeature.geometry.coordinates)
464
- .setHTML(formatStopPopup(stopFeature))
465
- .addTo(map);
466
- } else {
467
- const routeFeatures = map.queryRenderedFeatures(bbox, {
468
- layers: ['routes', 'route-outlines'],
469
- });
511
+ function setupEventListeners(map, routes) {
512
+ map.on('mousemove', (event) => handleMouseMove(event, map, routes));
513
+ map.on('click', (event) => handleClick(event, map));
514
+ setupTableHoverListeners(map);
515
+ }
470
516
 
471
- if (routeFeatures && routeFeatures.length > 0) {
472
- const routes = _.orderBy(
473
- _.uniqBy(
474
- routeFeatures,
475
- (feature) => feature.properties.route_short_name,
476
- ),
477
- (feature) =>
478
- Number.parseInt(feature.properties.route_short_name, 10),
479
- );
480
-
481
- new mapboxgl.Popup()
482
- .setLngLat(event.lngLat)
483
- .setHTML(formatRoutePopup(routes))
484
- .addTo(map);
485
- }
486
- }
487
- });
517
+ function handleMouseMove(event, map, routes) {
518
+ const features = map.queryRenderedFeatures(event.point, {
519
+ layers: ['routes', 'route-outlines', 'stops-highlighted', 'stops'],
520
+ });
521
+ if (features.length > 0) {
522
+ map.getCanvas().style.cursor = 'pointer';
523
+ highlightRoutes(
524
+ map,
525
+ _.compact(_.uniq(features.map((feature) => feature.properties.route_id))),
526
+ );
488
527
 
489
- function highlightStop(stopId) {
490
- map.setFilter('stops-highlighted', ['==', 'stop_id', stopId]);
528
+ if (features.some((feature) => feature.layer.id === 'stops')) {
529
+ highlightStop(
530
+ map,
531
+ features.find((feature) => feature.layer.id === 'stops').properties
532
+ .stop_id,
533
+ );
491
534
  }
535
+ } else {
536
+ map.getCanvas().style.cursor = '';
537
+ unHighlightRoutes(map);
538
+ unHighlightStop(map);
539
+ }
540
+ }
541
+
542
+ function handleClick(event, map) {
543
+ const bbox = [
544
+ [event.point.x - 5, event.point.y - 5],
545
+ [event.point.x + 5, event.point.y + 5],
546
+ ];
547
+ const stopFeatures = map.queryRenderedFeatures(bbox, {
548
+ layers: ['stops-highlighted', 'stops'],
549
+ });
550
+
551
+ if (stopFeatures && stopFeatures.length > 0) {
552
+ showStopPopup(map, stopFeatures[0]);
553
+ } else {
554
+ const routeFeatures = map.queryRenderedFeatures(bbox, {
555
+ layers: ['routes', 'route-outlines'],
556
+ });
492
557
 
493
- function unHighlightStop() {
494
- map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
558
+ if (routeFeatures && routeFeatures.length > 0) {
559
+ showRoutePopup(map, routeFeatures, event.lngLat);
495
560
  }
561
+ }
562
+ }
496
563
 
497
- function highlightRoutes(routeIds, zoom) {
498
- map.setFilter('highlighted-routes', [
499
- 'all',
500
- ['has', 'route_short_name'],
501
- ['in', ['get', 'route_id'], ['literal', routeIds]],
502
- ]);
503
- map.setFilter('highlighted-route-outlines', [
504
- 'all',
505
- ['has', 'route_short_name'],
506
- ['in', ['get', 'route_id'], ['literal', routeIds]],
507
- ]);
508
- map.setFilter('highlighted-route-line-shadows', [
509
- 'all',
510
- ['has', 'route_short_name'],
511
- ['in', ['get', 'route_id'], ['literal', routeIds]],
512
- ]);
513
-
514
- // Show labels only for highlighted route
515
- map.setFilter('route-labels', [
516
- 'in',
517
- ['get', 'route_id'],
518
- ['literal', routeIds],
519
- ]);
520
-
521
- const routeLineOpacity = 0.4;
522
-
523
- // De-emphasize other routes
524
- map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
525
- map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
526
- map.setPaintProperty(
527
- 'route-line-shadows',
528
- 'line-opacity',
529
- routeLineOpacity,
530
- );
564
+ function showStopPopup(map, feature) {
565
+ new maplibregl.Popup()
566
+ .setLngLat(feature.geometry.coordinates)
567
+ .setHTML(formatStopPopup(feature))
568
+ .addTo(map);
569
+ }
570
+
571
+ function showRoutePopup(map, features, lngLat) {
572
+ const routes = _.orderBy(
573
+ _.uniqBy(features, (feature) => feature.properties.route_short_name),
574
+ (feature) => Number.parseInt(feature.properties.route_short_name, 10),
575
+ );
576
+
577
+ new maplibregl.Popup()
578
+ .setLngLat(lngLat)
579
+ .setHTML(formatRoutePopup(routes))
580
+ .addTo(map);
581
+ }
582
+
583
+ function highlightStop(map, stopId) {
584
+ map.setFilter('stops-highlighted', ['==', 'stop_id', stopId]);
585
+ }
531
586
 
532
- const highlightedFeatures = geojson.features.filter((feature) =>
587
+ function unHighlightStop(map) {
588
+ map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
589
+ }
590
+
591
+ function highlightRoutes(map, routeIds, zoom) {
592
+ map.setFilter('highlighted-routes', [
593
+ 'all',
594
+ ['has', 'route_short_name'],
595
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
596
+ ]);
597
+ map.setFilter('highlighted-route-outlines', [
598
+ 'all',
599
+ ['has', 'route_short_name'],
600
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
601
+ ]);
602
+ map.setFilter('highlighted-route-line-shadows', [
603
+ 'all',
604
+ ['has', 'route_short_name'],
605
+ ['in', ['get', 'route_id'], ['literal', routeIds]],
606
+ ]);
607
+
608
+ map.setFilter('route-labels', [
609
+ 'in',
610
+ ['get', 'route_id'],
611
+ ['literal', routeIds],
612
+ ]);
613
+
614
+ const routeLineOpacity = 0.4;
615
+
616
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
617
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
618
+ map.setPaintProperty('route-line-shadows', 'line-opacity', routeLineOpacity);
619
+
620
+ if (zoom) {
621
+ const data = map.querySourceFeatures('routes');
622
+ if (data) {
623
+ const highlightedFeatures = data.filter((feature) =>
533
624
  routeIds.includes(feature.properties.route_id),
534
625
  );
535
-
536
- if (highlightedFeatures.length > 0 && zoom) {
626
+ if (highlightedFeatures.length > 0) {
537
627
  const zoomBounds = getBounds({
628
+ type: 'FeatureCollection',
538
629
  features: highlightedFeatures,
539
630
  });
540
631
  map.fitBounds(zoomBounds, {
@@ -542,55 +633,56 @@ function createSystemMap(id, geojson) {
542
633
  });
543
634
  }
544
635
  }
636
+ }
637
+ }
545
638
 
546
- function unHighlightRoutes(zoom) {
547
- map.setFilter('highlighted-routes', ['==', ['get', 'route_id'], 'none']);
548
- map.setFilter('highlighted-route-outlines', [
549
- '==',
550
- ['get', 'route_id'],
551
- 'none',
552
- ]);
553
- map.setFilter('highlighted-route-line-shadows', [
554
- '==',
555
- ['get', 'route_id'],
556
- 'none',
557
- ]);
558
-
559
- // Show labels for all routes
560
- map.setFilter('route-labels', ['has', 'route_short_name']);
561
-
562
- const routeLineOpacity = 1;
563
-
564
- // Re-emphasize other routes
565
- map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
566
- map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
567
- map.setPaintProperty(
568
- 'route-line-shadows',
569
- 'line-opacity',
570
- routeLineOpacity,
639
+ function unHighlightRoutes(map, zoom) {
640
+ map.setFilter('highlighted-routes', ['==', ['get', 'route_id'], 'none']);
641
+ map.setFilter('highlighted-route-outlines', [
642
+ '==',
643
+ ['get', 'route_id'],
644
+ 'none',
645
+ ]);
646
+ map.setFilter('highlighted-route-line-shadows', [
647
+ '==',
648
+ ['get', 'route_id'],
649
+ 'none',
650
+ ]);
651
+
652
+ map.setFilter('route-labels', ['has', 'route_short_name']);
653
+
654
+ const routeLineOpacity = 1;
655
+
656
+ map.setPaintProperty('routes', 'line-opacity', routeLineOpacity);
657
+ map.setPaintProperty('route-outlines', 'line-opacity', routeLineOpacity);
658
+ map.setPaintProperty('route-line-shadows', 'line-opacity', routeLineOpacity);
659
+
660
+ if (zoom) {
661
+ const data = map.querySourceFeatures('routes');
662
+ if (data) {
663
+ map.fitBounds(
664
+ getBounds({
665
+ type: 'FeatureCollection',
666
+ features: data,
667
+ }),
571
668
  );
572
-
573
- if (zoom) {
574
- map.fitBounds(bounds);
575
- }
576
669
  }
670
+ }
671
+ }
577
672
 
578
- // On table hover, highlight route on map
579
- jQuery(() => {
580
- jQuery('.overview-list a').hover((event) => {
581
- const routeIdString = jQuery(event.target).data('route-ids');
582
- if (routeIdString) {
583
- const routeIds = routeIdString.toString().split(',');
584
- highlightRoutes(routeIds, true);
585
- }
586
- });
587
-
588
- jQuery('.overview-list').hover(
589
- () => {},
590
- () => unHighlightRoutes(true),
591
- );
673
+ function setupTableHoverListeners(map) {
674
+ jQuery(() => {
675
+ jQuery('.overview-list a').hover((event) => {
676
+ const routeIdString = jQuery(event.target).data('route-ids');
677
+ if (routeIdString) {
678
+ const routeIds = routeIdString.toString().split(',');
679
+ highlightRoutes(map, routeIds, true);
680
+ }
592
681
  });
593
- });
594
682
 
595
- maps[id] = map;
683
+ jQuery('.overview-list').hover(
684
+ () => {},
685
+ () => unHighlightRoutes(map, true),
686
+ );
687
+ });
596
688
  }