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,4 +1,4 @@
1
- /* global document, jQuery, mapboxgl, Pbf, stopData, routeData, routeIds, tripIds, geojsons, gtfsRealtimeUrls */
1
+ /* global document, jQuery, maplibregl, Pbf, mapStyleUrl, stopData, routeData, routeIds, tripIds, geojsons, gtfsRealtimeUrls */
2
2
  /* eslint prefer-arrow-callback: "off", no-unused-vars: "off" */
3
3
 
4
4
  const maps = {};
@@ -93,13 +93,83 @@ function getStopPopupHtml(feature, stop) {
93
93
  if (stop.stop_code ?? false) {
94
94
  jQuery('<div>')
95
95
  .html([
96
- jQuery('<label>').addClass('popup-label').text('Stop Code:'),
96
+ jQuery('<div>').addClass('popup-label').text('Stop Code:'),
97
97
  jQuery('<strong>').text(stop.stop_code),
98
98
  ])
99
99
  .appendTo(html);
100
100
  }
101
101
 
102
- jQuery('<label>').text('Routes Served:').appendTo(html);
102
+ if (tripUpdates) {
103
+ const stopTimeUpdates = {
104
+ 0: [],
105
+ 1: [],
106
+ };
107
+
108
+ for (const tripUpdate of tripUpdates) {
109
+ const stopTimeUpdatesForStop =
110
+ tripUpdate.trip_update.stop_time_update.filter(
111
+ (stopTimeUpdate) =>
112
+ stopTimeUpdate.stop_id === stop.stop_id &&
113
+ (stopTimeUpdate.departure !== null ||
114
+ stopTimeUpdate.arrival !== null) &&
115
+ stopTimeUpdate.schedule_relationship !== 3,
116
+ );
117
+ if (stopTimeUpdatesForStop.length > 0) {
118
+ stopTimeUpdates[tripUpdate.trip_update.trip.direction_id].push(
119
+ ...stopTimeUpdatesForStop,
120
+ );
121
+ }
122
+ }
123
+
124
+ stopTimeUpdates['0'].sort((a, b) => {
125
+ const timeA = a.departure ? a.departure.time : a.arrival.time;
126
+ const timeB = b.departure ? b.departure.time : b.arrival.time;
127
+ return timeA - timeB;
128
+ });
129
+
130
+ stopTimeUpdates['1'].sort((a, b) => {
131
+ const timeA = a.departure ? a.departure.time : a.arrival.time;
132
+ const timeB = b.departure ? b.departure.time : b.arrival.time;
133
+ return timeA - timeB;
134
+ });
135
+
136
+ if (stopTimeUpdates['0'].length > 0 || stopTimeUpdates['1'].length > 0) {
137
+ jQuery('<div>')
138
+ .addClass('popup-label')
139
+ .text('Upcoming Departures:')
140
+ .appendTo(html);
141
+
142
+ for (const direction of ['0', '1']) {
143
+ if (stopTimeUpdates[direction].length > 0) {
144
+ const directionName = jQuery(
145
+ `.timetable[data-direction-id="${direction}"]`,
146
+ ).data('direction-name');
147
+ const departureTimes = stopTimeUpdates[direction].map(
148
+ (stopTimeUpdate) =>
149
+ Math.round(
150
+ ((stopTimeUpdate.departure
151
+ ? stopTimeUpdate.departure.time
152
+ : stopTimeUpdate.arrival.time) -
153
+ Date.now() / 1000) /
154
+ 60,
155
+ ),
156
+ );
157
+
158
+ // Only use the next 4 departures
159
+ const formattedDepartures = new Intl.ListFormat('en', {
160
+ style: 'long',
161
+ type: 'conjunction',
162
+ }).format(departureTimes.slice(0, 4).map((time) => `<b>${time}</b>`));
163
+
164
+ jQuery('<div>')
165
+ .html(`<b>${directionName}</b> in ${formattedDepartures} min`)
166
+ .appendTo(html);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ jQuery('<div>').addClass('popup-label').text('Routes Served:').appendTo(html);
103
173
 
104
174
  jQuery(html).append(
105
175
  jQuery('<div>')
@@ -122,7 +192,7 @@ function getStopPopupHtml(feature, stop) {
122
192
  }
123
193
 
124
194
  function getBounds(geojson) {
125
- const bounds = new mapboxgl.LngLatBounds();
195
+ const bounds = new maplibregl.LngLatBounds();
126
196
  for (const feature of geojson.features) {
127
197
  if (feature.geometry.type.toLowerCase() === 'point') {
128
198
  bounds.extend(feature.geometry.coordinates);
@@ -389,7 +459,10 @@ function addVehicleMarker(vehiclePosition, vehicleTripUpdate) {
389
459
  ];
390
460
 
391
461
  // Add marker to map
392
- const vehicleMarker = new mapboxgl.Marker(el)
462
+ const vehicleMarker = new maplibregl.Marker({
463
+ element: el,
464
+ anchor: 'center',
465
+ })
393
466
  .setLngLat(coordinates)
394
467
  .addTo(maps[visibleTimetableId]);
395
468
 
@@ -502,10 +575,7 @@ async function updateArrivals() {
502
575
 
503
576
  jQuery('.vehicle-legend-item').show();
504
577
 
505
- vehiclePositions = latestVehiclePositions;
506
- tripUpdates = latestTripUpdates;
507
-
508
- const routeVehiclePositions = vehiclePositions.filter((vehiclePosition) => {
578
+ vehiclePositions = latestVehiclePositions.filter((vehiclePosition) => {
509
579
  if (
510
580
  !vehiclePosition ||
511
581
  !vehiclePosition.vehicle ||
@@ -534,7 +604,19 @@ async function updateArrivals() {
534
604
  return tripIds.includes(vehiclePosition.vehicle.trip.trip_id);
535
605
  });
536
606
 
537
- for (const vehiclePosition of routeVehiclePositions) {
607
+ tripUpdates = latestTripUpdates.filter((tripUpdate) => {
608
+ if (
609
+ !tripUpdate ||
610
+ !tripUpdate.trip_update ||
611
+ !tripUpdate.trip_update.trip
612
+ ) {
613
+ return false;
614
+ }
615
+
616
+ return tripIds.includes(tripUpdate.trip_update.trip.trip_id);
617
+ });
618
+
619
+ for (const vehiclePosition of vehiclePositions) {
538
620
  const vehicleId = vehiclePosition.vehicle.vehicle.id;
539
621
 
540
622
  let vehicleTripUpdate = tripUpdates?.find(
@@ -575,7 +657,7 @@ async function updateArrivals() {
575
657
  // Remove vehicles not in the feed
576
658
  for (const vehicleId of Object.keys(vehicleMarkers)) {
577
659
  if (
578
- !routeVehiclePositions.find(
660
+ !vehiclePositions.find(
579
661
  (vehiclePosition) => vehiclePosition.vehicle.vehicle.id === vehicleId,
580
662
  )
581
663
  ) {
@@ -630,319 +712,332 @@ function createMap(id) {
630
712
  }
631
713
 
632
714
  const bounds = getBounds(geojson);
633
- const map = new mapboxgl.Map({
715
+ const map = new maplibregl.Map({
634
716
  container: `map_timetable_id_${id}`,
635
- style: 'mapbox://styles/mapbox/light-v11',
717
+ style: mapStyleUrl,
636
718
  center: bounds.getCenter(),
637
719
  zoom: 12,
638
720
  preserveDrawingBuffer: true,
639
721
  });
640
722
 
641
- map.initialize = () =>
642
- map.fitBounds(bounds, {
643
- padding: {
644
- top: 40,
645
- bottom: 40,
646
- left: 20,
647
- right: 40,
648
- },
649
- duration: 0,
650
- });
723
+ map.initialize = () => fitMapToBounds(map, bounds);
651
724
 
652
725
  map.scrollZoom.disable();
653
- map.addControl(new mapboxgl.NavigationControl());
726
+ map.addControl(new maplibregl.NavigationControl());
727
+ map.addControl(new maplibregl.FullscreenControl());
654
728
 
655
729
  map.on('load', () => {
656
- map.fitBounds(bounds, {
657
- padding: {
658
- top: 40,
659
- bottom: 40,
660
- left: 20,
661
- right: 40,
662
- },
663
- duration: 0,
664
- });
730
+ fitMapToBounds(map, bounds);
731
+ disablePointsOfInterest(map);
732
+ addMapLayers(map, geojson, defaultRouteColor, lineLayout);
733
+ setupEventListeners(map, id);
734
+ });
665
735
 
666
- // Turn off Points of Interest labels
667
- map.setLayoutProperty('poi-label', 'visibility', 'none');
736
+ return map;
737
+ }
668
738
 
669
- // Find the index of the first symbol layer in the map style to put the route lines underneath
670
- let firstSymbolId;
671
- for (const layer of map.getStyle().layers) {
672
- if (layer.type === 'symbol') {
673
- firstSymbolId = layer.id;
674
- break;
675
- }
676
- }
739
+ function fitMapToBounds(map, bounds) {
740
+ map.fitBounds(bounds, {
741
+ padding: { top: 40, bottom: 40, left: 20, right: 40 },
742
+ duration: 0,
743
+ });
744
+ }
677
745
 
678
- // Add route drop shadow outline first
679
- map.addLayer(
680
- {
681
- id: 'route-line-shadow',
682
- type: 'line',
683
- source: {
684
- type: 'geojson',
685
- data: geojson,
686
- },
687
- paint: {
688
- 'line-color': '#000000',
689
- 'line-opacity': 0.3,
690
- 'line-width': {
691
- base: 12,
692
- stops: [
693
- [14, 20],
694
- [18, 42],
695
- ],
696
- },
697
- 'line-blur': {
698
- base: 12,
699
- stops: [
700
- [14, 20],
701
- [18, 42],
702
- ],
703
- },
704
- },
705
- layout: lineLayout,
706
- filter: ['!has', 'stop_id'],
707
- },
708
- firstSymbolId,
709
- );
746
+ function disablePointsOfInterest(map) {
747
+ const layers = map.getStyle().layers;
748
+ const poiLayerIds = layers
749
+ .filter((layer) => layer.id.startsWith('poi'))
750
+ ?.map((layer) => layer.id);
751
+ poiLayerIds.forEach((layerId) => {
752
+ map.setLayoutProperty(layerId, 'visibility', 'none');
753
+ });
754
+ }
710
755
 
711
- // Add route line outline
712
- map.addLayer(
713
- {
714
- id: 'route-line-outline',
715
- type: 'line',
716
- source: {
717
- type: 'geojson',
718
- data: geojson,
719
- },
720
- paint: {
721
- 'line-color': '#FFFFFF',
722
- 'line-opacity': 1,
723
- 'line-width': {
724
- base: 8,
725
- stops: [
726
- [14, 12],
727
- [18, 32],
728
- ],
729
- },
730
- },
731
- layout: lineLayout,
732
- filter: ['!has', 'stop_id'],
733
- },
734
- firstSymbolId,
735
- );
756
+ function addMapLayers(map, geojson, defaultRouteColor, lineLayout) {
757
+ const layers = map.getStyle().layers;
758
+ const firstLabelLayerId = layers.find(
759
+ (layer) => layer.type === 'symbol' && layer.id.includes('label'),
760
+ )?.id;
761
+
762
+ addRouteLineShadow(map, geojson, lineLayout, firstLabelLayerId);
763
+ addRouteLineOutline(map, geojson, lineLayout, firstLabelLayerId);
764
+ addRouteLine(map, geojson, defaultRouteColor, lineLayout, firstLabelLayerId);
765
+ addStops(map, geojson);
766
+ addHighlightedStops(map, geojson);
767
+ }
736
768
 
737
- // Add route line
738
- map.addLayer(
739
- {
740
- id: 'route-line',
741
- type: 'line',
742
- source: {
743
- type: 'geojson',
744
- data: geojson,
769
+ function addRouteLineShadow(map, geojson, lineLayout, firstSymbolId) {
770
+ map.addLayer(
771
+ {
772
+ id: 'route-line-shadow',
773
+ type: 'line',
774
+ source: { type: 'geojson', data: geojson },
775
+ paint: {
776
+ 'line-color': '#000000',
777
+ 'line-opacity': 0.3,
778
+ 'line-width': {
779
+ base: 12,
780
+ stops: [
781
+ [14, 20],
782
+ [18, 42],
783
+ ],
745
784
  },
746
- paint: {
747
- 'line-color': ['to-color', ['get', 'route_color'], defaultRouteColor],
748
- 'line-opacity': 1,
749
- 'line-width': {
750
- base: 4,
751
- stops: [
752
- [14, 6],
753
- [18, 16],
754
- ],
755
- },
785
+ 'line-blur': {
786
+ base: 12,
787
+ stops: [
788
+ [14, 20],
789
+ [18, 42],
790
+ ],
756
791
  },
757
- layout: lineLayout,
758
- filter: ['!has', 'stop_id'],
759
792
  },
760
- firstSymbolId,
761
- );
793
+ layout: lineLayout,
794
+ filter: ['!has', 'stop_id'],
795
+ },
796
+ firstSymbolId,
797
+ );
798
+ }
762
799
 
763
- // Add stops
764
- map.addLayer({
765
- id: 'stops',
766
- type: 'circle',
767
- source: {
768
- type: 'geojson',
769
- data: geojson,
770
- },
800
+ function addRouteLineOutline(map, geojson, lineLayout, firstSymbolId) {
801
+ map.addLayer(
802
+ {
803
+ id: 'route-line-outline',
804
+ type: 'line',
805
+ source: { type: 'geojson', data: geojson },
771
806
  paint: {
772
- 'circle-color': '#fff',
773
- 'circle-radius': {
774
- base: 1.75,
807
+ 'line-color': '#FFFFFF',
808
+ 'line-opacity': 1,
809
+ 'line-width': {
810
+ base: 8,
775
811
  stops: [
776
- [12, 4],
777
- [22, 100],
812
+ [14, 12],
813
+ [18, 32],
778
814
  ],
779
815
  },
780
- 'circle-stroke-color': '#3f4a5c',
781
- 'circle-stroke-width': 2,
782
816
  },
783
- filter: ['has', 'stop_id'],
784
- });
817
+ layout: lineLayout,
818
+ filter: ['!has', 'stop_id'],
819
+ },
820
+ firstSymbolId,
821
+ );
822
+ }
785
823
 
786
- // Layer for highlighted stops
787
- map.addLayer({
788
- id: 'stops-highlighted',
789
- type: 'circle',
790
- source: {
791
- type: 'geojson',
792
- data: geojson,
793
- },
824
+ function addRouteLine(
825
+ map,
826
+ geojson,
827
+ defaultRouteColor,
828
+ lineLayout,
829
+ firstSymbolId,
830
+ ) {
831
+ map.addLayer(
832
+ {
833
+ id: 'route-line',
834
+ type: 'line',
835
+ source: { type: 'geojson', data: geojson },
794
836
  paint: {
795
- 'circle-color': '#fff',
796
- 'circle-radius': {
797
- base: 1.75,
837
+ 'line-color': ['to-color', ['get', 'route_color'], defaultRouteColor],
838
+ 'line-opacity': 1,
839
+ 'line-width': {
840
+ base: 4,
798
841
  stops: [
799
- [12, 5],
800
- [22, 125],
842
+ [14, 6],
843
+ [18, 16],
801
844
  ],
802
845
  },
803
- 'circle-stroke-width': 2,
804
- 'circle-stroke-color': '#3f4a5c',
805
846
  },
806
- filter: ['==', 'stop_id', ''],
807
- });
847
+ layout: lineLayout,
848
+ filter: ['!has', 'stop_id'],
849
+ },
850
+ firstSymbolId,
851
+ );
852
+ }
808
853
 
809
- map.on('mousemove', (event) => {
810
- const features = map.queryRenderedFeatures(event.point, {
811
- layers: ['stops'],
812
- });
813
- if (features.length > 0) {
814
- map.getCanvas().style.cursor = 'pointer';
815
- const stopIds = [features[0].properties.stop_id];
816
- if (features[0].properties.parent_station) {
817
- stopIds.push(features[0].properties.parent_station);
818
- }
854
+ function addStops(map, geojson) {
855
+ map.addLayer({
856
+ id: 'stops',
857
+ type: 'circle',
858
+ source: { type: 'geojson', data: geojson },
859
+ paint: {
860
+ 'circle-color': '#ffffff',
861
+ 'circle-radius': {
862
+ base: 1.75,
863
+ stops: [
864
+ [12, 4],
865
+ [22, 100],
866
+ ],
867
+ },
868
+ 'circle-stroke-color': '#3f4a5c',
869
+ 'circle-stroke-width': 2,
870
+ },
871
+ filter: ['has', 'stop_id'],
872
+ });
873
+ }
819
874
 
820
- highlightStop(stopIds);
821
- } else {
822
- map.getCanvas().style.cursor = '';
823
- unHighlightStop();
824
- }
825
- });
875
+ function addHighlightedStops(map, geojson) {
876
+ map.addLayer({
877
+ id: 'stops-highlighted',
878
+ type: 'circle',
879
+ source: { type: 'geojson', data: geojson },
880
+ paint: {
881
+ 'circle-color': '#f8f8b9',
882
+ 'circle-radius': {
883
+ base: 1.75,
884
+ stops: [
885
+ [12, 6],
886
+ [22, 150],
887
+ ],
888
+ },
889
+ 'circle-stroke-width': 2,
890
+ 'circle-stroke-color': '#3f4a5c',
891
+ },
892
+ filter: ['==', 'stop_id', ''],
893
+ });
894
+ }
826
895
 
827
- map.on('click', (event) => {
828
- // Set bbox as 5px rectangle area around clicked point
829
- const bbox = [
830
- [event.point.x - 5, event.point.y - 5],
831
- [event.point.x + 5, event.point.y + 5],
832
- ];
833
- const features = map.queryRenderedFeatures(bbox, {
834
- layers: ['stops-highlighted', 'stops'],
835
- });
836
-
837
- if (!features || features.length === 0) {
838
- return;
839
- }
896
+ function setupEventListeners(map, id) {
897
+ map.on('mousemove', (event) => handleMouseMove(event, map, id));
898
+ map.on('click', (event) => handleClick(event, map));
899
+ setupTableHoverListeners(id, map);
900
+ }
840
901
 
841
- // Get the first feature and show popup
842
- const feature = features[0];
902
+ function handleMouseMove(event, map, id) {
903
+ const features = map.queryRenderedFeatures(event.point, {
904
+ layers: ['stops'],
905
+ });
906
+ if (features.length > 0) {
907
+ map.getCanvas().style.cursor = 'pointer';
908
+ const stopIds = [features[0].properties.stop_id];
909
+ if (features[0].properties.parent_station) {
910
+ stopIds.push(features[0].properties.parent_station);
911
+ }
912
+ highlightStop(map, id, stopIds);
913
+ } else {
914
+ map.getCanvas().style.cursor = '';
915
+ unHighlightStop(map, id);
916
+ }
917
+ }
843
918
 
844
- new mapboxgl.Popup()
845
- .setLngLat(feature.geometry.coordinates)
846
- .setHTML(
847
- getStopPopupHtml(feature, stopData[feature.properties.stop_id]),
848
- )
849
- .addTo(map);
850
- });
919
+ function handleClick(event, map) {
920
+ const bbox = [
921
+ [event.point.x - 5, event.point.y - 5],
922
+ [event.point.x + 5, event.point.y + 5],
923
+ ];
924
+ const features = map.queryRenderedFeatures(bbox, {
925
+ layers: ['stops-highlighted', 'stops'],
926
+ });
851
927
 
852
- function highlightStop(stopIds) {
853
- map.setFilter('stops-highlighted', [
854
- 'any',
855
- ['in', 'stop_id', ...stopIds],
856
- ['in', 'parent_station', ...stopIds],
857
- ]);
928
+ if (!features || features.length === 0) return;
858
929
 
859
- if (
860
- jQuery(`#timetable_id_${id} table`).data('orientation') === 'vertical'
861
- ) {
862
- const columnIndexes = [];
863
- const stopIdSelectors = stopIds
864
- .map(
865
- (stopId) =>
866
- `#timetable_id_${id} table colgroup col[data-stop-id="${stopId}"]`,
867
- )
868
- .join(',');
869
- jQuery(stopIdSelectors).each((index, col) => {
870
- columnIndexes.push(
871
- jQuery(`#timetable_id_${id} table colgroup col`).index(col),
872
- );
873
- });
930
+ const feature = features[0];
931
+ showStopPopup(map, feature);
932
+ }
874
933
 
875
- jQuery(`#timetable_id_${id} table .stop-time`).removeClass(
876
- 'highlighted',
877
- );
878
- jQuery(`#timetable_id_${id} table thead .stop-header`).removeClass(
879
- 'highlighted',
880
- );
881
- jQuery(`#timetable_id_${id} table .trip-row`).each((index, row) => {
882
- jQuery('.stop-time', row).each((index, el) => {
883
- if (columnIndexes.includes(index)) {
884
- jQuery(el).addClass('highlighted');
885
- }
886
- });
887
- });
934
+ function showStopPopup(map, feature) {
935
+ new maplibregl.Popup()
936
+ .setLngLat(feature.geometry.coordinates)
937
+ .setHTML(getStopPopupHtml(feature, stopData[feature.properties.stop_id]))
938
+ .addTo(map);
939
+ }
888
940
 
889
- jQuery(`#timetable_id_${id} table thead`).each((index, thead) => {
890
- jQuery('.stop-header', thead).each((index, el) => {
891
- if (columnIndexes.includes(index)) {
892
- jQuery(el).addClass('highlighted');
893
- }
894
- });
895
- });
896
- } else {
897
- jQuery(`#timetable_id_${id} table .stop-row`).removeClass(
898
- 'highlighted',
899
- );
900
- const stopIdSelectors = stopIds
901
- .map((stopId) => `#timetable_id_${id} table #stop_id_${stopId}`)
902
- .join(',');
903
- jQuery(stopIdSelectors).addClass('highlighted');
904
- }
905
- }
941
+ function highlightStop(map, id, stopIds) {
942
+ map.setFilter('stops-highlighted', [
943
+ 'any',
944
+ ['in', 'stop_id', ...stopIds],
945
+ ['in', 'parent_station', ...stopIds],
946
+ ]);
906
947
 
907
- function unHighlightStop() {
908
- map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
948
+ highlightTimetableStops(id, stopIds);
949
+ }
909
950
 
910
- if (
911
- jQuery(`#timetable_id_${id} table`).data('orientation') === 'vertical'
912
- ) {
913
- jQuery(`#timetable_id_${id} table .stop-time`).removeClass(
914
- 'highlighted',
915
- );
916
- jQuery(`#timetable_id_${id} table thead .stop-header`).removeClass(
917
- 'highlighted',
918
- );
919
- } else {
920
- jQuery(`#timetable_id_${id} table .stop-row`).removeClass(
921
- 'highlighted',
922
- );
923
- }
924
- }
951
+ function unHighlightStop(map, id) {
952
+ map.setFilter('stops-highlighted', ['==', 'stop_id', '']);
953
+ unHighlightTimetableStops(id);
954
+ }
925
955
 
926
- // On table hover, highlight stop on map
927
- jQuery('th, td', jQuery(`#timetable_id_${id} table`)).hover((event) => {
928
- let stopId;
929
- const table = jQuery(event.target).parents('table');
930
- if (table.data('orientation') === 'vertical') {
931
- var index = jQuery(event.target).index();
932
- stopId = jQuery('colgroup col', table).eq(index).data('stop-id');
933
- } else {
934
- stopId = jQuery(event.target).parents('tr').data('stop-id');
935
- }
956
+ function highlightTimetableStops(id, stopIds) {
957
+ const table = jQuery(`#timetable_id_${id} table`);
958
+ const isVertical = table.data('orientation') === 'vertical';
959
+
960
+ if (isVertical) {
961
+ highlightVerticalTimetableStops(id, stopIds);
962
+ } else {
963
+ highlightHorizontalTimetableStops(id, stopIds);
964
+ }
965
+ }
936
966
 
937
- if (stopId === undefined) {
938
- return;
967
+ function highlightVerticalTimetableStops(id, stopIds) {
968
+ const table = jQuery(`#timetable_id_${id} table`);
969
+ const columnIndexes = [];
970
+ const stopIdSelectors = stopIds
971
+ .map(
972
+ (stopId) =>
973
+ `#timetable_id_${id} table colgroup col[data-stop-id="${stopId}"]`,
974
+ )
975
+ .join(',');
976
+
977
+ jQuery(stopIdSelectors).each((index, col) => {
978
+ columnIndexes.push(
979
+ jQuery(`#timetable_id_${id} table colgroup col`).index(col),
980
+ );
981
+ });
982
+
983
+ table.find('.stop-time, thead .stop-header').removeClass('highlighted');
984
+ table.find('.trip-row').each((index, row) => {
985
+ jQuery('.stop-time', row).each((index, el) => {
986
+ if (columnIndexes.includes(index)) {
987
+ jQuery(el).addClass('highlighted');
939
988
  }
989
+ });
990
+ });
940
991
 
941
- highlightStop([stopId.toString()]);
942
- }, unHighlightStop);
992
+ table.find('thead').each((index, thead) => {
993
+ jQuery('.stop-header', thead).each((index, el) => {
994
+ if (columnIndexes.includes(index)) {
995
+ jQuery(el).addClass('highlighted');
996
+ }
997
+ });
943
998
  });
999
+ }
944
1000
 
945
- return map;
1001
+ function highlightHorizontalTimetableStops(id, stopIds) {
1002
+ const table = jQuery(`#timetable_id_${id} table`);
1003
+ table.find('.stop-row').removeClass('highlighted');
1004
+ const stopIdSelectors = stopIds
1005
+ .map((stopId) => `#timetable_id_${id} table #stop_id_${stopId}`)
1006
+ .join(',');
1007
+ jQuery(stopIdSelectors).addClass('highlighted');
1008
+ }
1009
+
1010
+ function unHighlightTimetableStops(id) {
1011
+ const table = jQuery(`#timetable_id_${id} table`);
1012
+ const isVertical = table.data('orientation') === 'vertical';
1013
+
1014
+ if (isVertical) {
1015
+ table.find('.stop-time, thead .stop-header').removeClass('highlighted');
1016
+ } else {
1017
+ table.find('.stop-row').removeClass('highlighted');
1018
+ }
1019
+ }
1020
+
1021
+ function setupTableHoverListeners(id, map) {
1022
+ jQuery('th, td', jQuery(`#timetable_id_${id} table`)).hover(
1023
+ (event) => {
1024
+ const stopId = getStopIdFromTableCell(event.target);
1025
+ if (stopId !== undefined) {
1026
+ highlightStop(map, id, [stopId.toString()]);
1027
+ }
1028
+ },
1029
+ () => unHighlightStop(map, id),
1030
+ );
1031
+ }
1032
+
1033
+ function getStopIdFromTableCell(cell) {
1034
+ const table = jQuery(cell).closest('table');
1035
+ if (table.data('orientation') === 'vertical') {
1036
+ const index = jQuery(cell).index();
1037
+ return jQuery('colgroup col', table).eq(index).data('stop-id');
1038
+ } else {
1039
+ return jQuery(cell).closest('tr').data('stop-id');
1040
+ }
946
1041
  }
947
1042
 
948
1043
  function createMaps() {
@@ -956,18 +1051,27 @@ function createMaps() {
956
1051
  gtfsRealtimeUrls?.realtimeVehiclePositions?.url
957
1052
  ) {
958
1053
  // Popup for realtime vehicle locations
959
- vehiclePopup = new mapboxgl.Popup({
1054
+ const markerHeight = 20;
1055
+ const markerRadius = 10;
1056
+ const linearOffset = 15;
1057
+ vehiclePopup = new maplibregl.Popup({
960
1058
  closeOnClick: false,
961
1059
  className: 'vehicle-popup',
962
1060
  offset: {
963
- top: [0, 10],
964
- bottom: [0, -10],
965
- left: [10, 0],
966
- right: [-10, 0],
967
- 'top-left': [10, 10],
968
- 'top-right': [-10, 10],
969
- 'bottom-left': [10, -10],
970
- 'bottom-right': [-10, -10],
1061
+ top: [0, 0],
1062
+ 'top-left': [0, 0],
1063
+ 'top-right': [0, 0],
1064
+ bottom: [0, -markerHeight],
1065
+ 'bottom-left': [
1066
+ linearOffset,
1067
+ (markerHeight - markerRadius + linearOffset) * -1,
1068
+ ],
1069
+ 'bottom-right': [
1070
+ -linearOffset,
1071
+ (markerHeight - markerRadius + linearOffset) * -1,
1072
+ ],
1073
+ left: [markerRadius, (markerHeight - markerRadius) * -1],
1074
+ right: [-markerRadius, (markerHeight - markerRadius) * -1],
971
1075
  },
972
1076
  });
973
1077