gtfs-to-html 2.11.4 → 2.12.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, maplibregl, Pbf, mapStyleUrl, stopData, routeData, routeIds, tripIds, geojsons, gtfsRealtimeUrls */
1
+ /* global maplibregl, Pbf, FeedMessage, mapStyleUrl, stopData, routeData, routeIds, tripIds, geojsons, gtfsRealtimeUrls */
2
2
  /* eslint prefer-arrow-callback: "off", no-unused-vars: "off" */
3
3
 
4
4
  const maps = {};
@@ -18,8 +18,8 @@ function formatRouteTextColor(route) {
18
18
  }
19
19
 
20
20
  function degToCompass(num) {
21
- var val = Math.floor(num / 22.5 + 0.5);
22
- var arr = [
21
+ const val = Math.floor(num / 22.5 + 0.5);
22
+ const arr = [
23
23
  'N',
24
24
  'NNE',
25
25
  'NE',
@@ -56,47 +56,56 @@ function formatSeconds(seconds) {
56
56
 
57
57
  function formatRoute(route) {
58
58
  const html = route.route_url
59
- ? jQuery('<a>').attr('href', route.route_url)
60
- : jQuery('<div>');
59
+ ? document.createElement('a')
60
+ : document.createElement('div');
61
61
 
62
- html.addClass('map-route-item');
62
+ if (route.route_url) {
63
+ html.href = route.route_url;
64
+ }
63
65
 
64
- // Only add color swatch if route has a color
65
- const routeItemDivs = [];
66
+ html.className = 'map-route-item';
66
67
 
68
+ // Only add color swatch if route has a color
67
69
  if (route.route_color) {
68
- routeItemDivs.push(
69
- jQuery('<div>')
70
- .addClass('route-color-swatch')
71
- .css('backgroundColor', formatRouteColor(route))
72
- .css('color', formatRouteTextColor(route))
73
- .text(route.route_short_name ?? ''),
74
- );
70
+ const swatch = document.createElement('div');
71
+ swatch.className = 'route-color-swatch';
72
+ swatch.style.backgroundColor = formatRouteColor(route);
73
+ swatch.style.color = formatRouteTextColor(route);
74
+ swatch.textContent = route.route_short_name ?? '';
75
+ html.appendChild(swatch);
75
76
  }
76
- routeItemDivs.push(
77
- jQuery('<div>')
78
- .addClass('underline-hover')
79
- .text(route.route_long_name ?? `Route ${route.route_short_name}`),
80
- );
81
77
 
82
- html.append(routeItemDivs);
78
+ const textDiv = document.createElement('div');
79
+ textDiv.className = 'underline-hover';
80
+ textDiv.textContent =
81
+ route.route_long_name ?? `Route ${route.route_short_name}`;
82
+ html.appendChild(textDiv);
83
83
 
84
- return html.prop('outerHTML');
84
+ return html.outerHTML;
85
85
  }
86
86
 
87
87
  function getStopPopupHtml(feature, stop) {
88
88
  const routeIds = JSON.parse(feature.properties.route_ids);
89
- const html = jQuery('<div>');
89
+ const html = document.createElement('div');
90
90
 
91
- jQuery('<div>').addClass('popup-title').text(stop.stop_name).appendTo(html);
91
+ const title = document.createElement('div');
92
+ title.className = 'popup-title';
93
+ title.textContent = stop.stop_name;
94
+ html.appendChild(title);
92
95
 
93
96
  if (stop.stop_code ?? false) {
94
- jQuery('<div>')
95
- .html([
96
- jQuery('<div>').addClass('popup-label').text('Stop Code:'),
97
- jQuery('<strong>').text(stop.stop_code),
98
- ])
99
- .appendTo(html);
97
+ const stopCodeContainer = document.createElement('div');
98
+
99
+ const label = document.createElement('div');
100
+ label.className = 'popup-label';
101
+ label.textContent = 'Stop Code:';
102
+ stopCodeContainer.appendChild(label);
103
+
104
+ const code = document.createElement('strong');
105
+ code.textContent = stop.stop_code;
106
+ stopCodeContainer.appendChild(code);
107
+
108
+ html.appendChild(stopCodeContainer);
100
109
  }
101
110
 
102
111
  if (tripUpdates) {
@@ -134,16 +143,19 @@ function getStopPopupHtml(feature, stop) {
134
143
  });
135
144
 
136
145
  if (stopTimeUpdates['0'].length > 0 || stopTimeUpdates['1'].length > 0) {
137
- jQuery('<div>')
138
- .addClass('popup-label')
139
- .text('Upcoming Departures:')
140
- .appendTo(html);
146
+ const departuresLabel = document.createElement('div');
147
+ departuresLabel.className = 'popup-label';
148
+ departuresLabel.textContent = 'Upcoming Departures:';
149
+ html.appendChild(departuresLabel);
141
150
 
142
151
  for (const direction of ['0', '1']) {
143
152
  if (stopTimeUpdates[direction].length > 0) {
144
- const directionName = jQuery(
153
+ const timetableElement = document.querySelector(
145
154
  `.timetable[data-direction-id="${direction}"]`,
146
- ).data('direction-name');
155
+ );
156
+ const directionName = timetableElement
157
+ ? timetableElement.dataset.directionName
158
+ : '';
147
159
  const departureTimes = stopTimeUpdates[direction].map(
148
160
  (stopTimeUpdate) =>
149
161
  Math.round(
@@ -161,34 +173,35 @@ function getStopPopupHtml(feature, stop) {
161
173
  type: 'conjunction',
162
174
  }).format(departureTimes.slice(0, 4).map((time) => `<b>${time}</b>`));
163
175
 
164
- jQuery('<div>')
165
- .html(`<b>${directionName}</b> in ${formattedDepartures} min`)
166
- .appendTo(html);
176
+ const departureDiv = document.createElement('div');
177
+ departureDiv.innerHTML = `<b>${directionName}</b> in ${formattedDepartures} min`;
178
+ html.appendChild(departureDiv);
167
179
  }
168
180
  }
169
181
  }
170
182
  }
171
183
 
172
- jQuery('<div>').addClass('popup-label').text('Routes Served:').appendTo(html);
173
-
174
- jQuery(html).append(
175
- jQuery('<div>')
176
- .addClass('route-list')
177
- .html(routeIds.map((routeId) => formatRoute(routeData[routeId]))),
178
- );
179
-
180
- jQuery('<a>')
181
- .addClass('btn-blue btn-sm')
182
- .prop(
183
- 'href',
184
- `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`,
185
- )
186
- .prop('target', '_blank')
187
- .prop('rel', 'noopener noreferrer')
188
- .html('View on Streetview')
189
- .appendTo(html);
190
-
191
- return html.prop('outerHTML');
184
+ const routesLabel = document.createElement('div');
185
+ routesLabel.className = 'popup-label';
186
+ routesLabel.textContent = 'Routes Served:';
187
+ html.appendChild(routesLabel);
188
+
189
+ const routeList = document.createElement('div');
190
+ routeList.className = 'route-list';
191
+ routeList.innerHTML = routeIds
192
+ .map((routeId) => formatRoute(routeData[routeId]))
193
+ .join('');
194
+ html.appendChild(routeList);
195
+
196
+ const streetviewLink = document.createElement('a');
197
+ streetviewLink.className = 'btn-blue btn-sm';
198
+ streetviewLink.href = `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`;
199
+ streetviewLink.target = '_blank';
200
+ streetviewLink.rel = 'noopener noreferrer';
201
+ streetviewLink.textContent = 'View on Streetview';
202
+ html.appendChild(streetviewLink);
203
+
204
+ return html.outerHTML;
192
205
  }
193
206
 
194
207
  function getBounds(geojson) {
@@ -213,7 +226,7 @@ function getBounds(geojson) {
213
226
  }
214
227
 
215
228
  function secondsInFuture(dateString) {
216
- // Takes a dateString in the format of "YYYYMMDD HH:mm:ss" and returns true if the date is more than 15 minutes in the future
229
+ // Takes a dateString in the format of "YYYYMMDD HH:mm:ss" and returns number of seconds in the future or 0 if the date is in the past
217
230
 
218
231
  const inputDate = new Date(
219
232
  dateString.substring(0, 4), // Year
@@ -257,28 +270,33 @@ function formatMovingText(vehiclePosition) {
257
270
  }
258
271
 
259
272
  function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
260
- const html = jQuery('<div>', {
261
- id: `vehicle-popup-${vehiclePosition.vehicle.vehicle.id}`,
262
- });
273
+ const html = document.createElement('div');
274
+ html.dataset.vehicleId = vehiclePosition.vehicle.vehicle.id;
263
275
 
264
276
  const lastUpdated = new Date(vehiclePosition.vehicle.timestamp * 1000);
265
- const directionName = jQuery(
266
- '.timetable #trip_id_' + vehiclePosition.vehicle.trip.trip_id,
267
- )
268
- .parents('.timetable')
269
- .data('direction-name');
277
+ const tripElement = document.querySelector(
278
+ `.timetable [data-trip-id="${vehiclePosition.vehicle.trip.trip_id}"]`,
279
+ );
280
+ const timetableElement = tripElement
281
+ ? tripElement.closest('.timetable')
282
+ : null;
283
+ const directionName = timetableElement
284
+ ? timetableElement.dataset.directionName
285
+ : null;
270
286
 
271
287
  if (directionName) {
272
- jQuery('<div>')
273
- .addClass('popup-title')
274
- .text(`Vehicle: ${directionName}`)
275
- .appendTo(html);
288
+ const title = document.createElement('div');
289
+ title.className = 'popup-title';
290
+ title.textContent = `Vehicle: ${directionName}`;
291
+ html.appendChild(title);
276
292
  }
277
293
 
278
294
  const movingText = formatMovingText(vehiclePosition);
279
295
 
280
296
  if (movingText) {
281
- jQuery('<div>').text(movingText).appendTo(html);
297
+ const movingDiv = document.createElement('div');
298
+ movingDiv.textContent = movingText;
299
+ html.appendChild(movingDiv);
282
300
  }
283
301
 
284
302
  const numberOfArrivalsToShow = 5;
@@ -308,37 +326,44 @@ function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
308
326
  }
309
327
 
310
328
  if (nextArrivals.length > 0) {
311
- jQuery('<div>')
312
- .addClass('upcoming-stops')
313
- .append([
314
- jQuery('<div>').text('Time'),
315
- jQuery('<div>').text('Upcoming Stop'),
316
- ])
317
- .append(
318
- nextArrivals.flatMap((arrival) => {
319
- let delay = '';
320
-
321
- if (arrival.delay > 0) {
322
- delay = `(${formatSeconds(arrival.delay)} behind schedule)`;
323
- } else if (arrival.delay < 0) {
324
- delay = `(${formatSeconds(arrival.delay)} ahead of schedule)`;
325
- }
326
-
327
- return [
328
- jQuery('<div>').text(formatSeconds(arrival.secondsToArrival)),
329
- jQuery('<div>').text(`${arrival.stopName} ${delay}`),
330
- ];
331
- }),
332
- )
333
- .appendTo(html);
329
+ const upcomingStops = document.createElement('div');
330
+ upcomingStops.className = 'upcoming-stops';
331
+
332
+ const timeHeader = document.createElement('div');
333
+ timeHeader.textContent = 'Time';
334
+ upcomingStops.appendChild(timeHeader);
335
+
336
+ const stopHeader = document.createElement('div');
337
+ stopHeader.textContent = 'Upcoming Stop';
338
+ upcomingStops.appendChild(stopHeader);
339
+
340
+ for (const arrival of nextArrivals) {
341
+ let delay = '';
342
+
343
+ if (arrival.delay > 0) {
344
+ delay = `(${formatSeconds(arrival.delay)} behind schedule)`;
345
+ } else if (arrival.delay < 0) {
346
+ delay = `(${formatSeconds(arrival.delay)} ahead of schedule)`;
347
+ }
348
+
349
+ const timeDiv = document.createElement('div');
350
+ timeDiv.textContent = formatSeconds(arrival.secondsToArrival);
351
+ upcomingStops.appendChild(timeDiv);
352
+
353
+ const stopDiv = document.createElement('div');
354
+ stopDiv.textContent = `${arrival.stopName} ${delay}`;
355
+ upcomingStops.appendChild(stopDiv);
356
+ }
357
+
358
+ html.appendChild(upcomingStops);
334
359
  }
335
360
 
336
- jQuery('<div>')
337
- .addClass('vehicle-updated')
338
- .text(`Updated: ${lastUpdated.toLocaleTimeString()}`)
339
- .appendTo(html);
361
+ const updatedDiv = document.createElement('div');
362
+ updatedDiv.className = 'vehicle-updated';
363
+ updatedDiv.textContent = `Updated: ${lastUpdated.toLocaleTimeString()}`;
364
+ html.appendChild(updatedDiv);
340
365
 
341
- return html.prop('outerHTML');
366
+ return html.outerHTML;
342
367
  }
343
368
 
344
369
  function getVehicleBearing(vehiclePosition, vehicleTripUpdate) {
@@ -351,10 +376,7 @@ function getVehicleBearing(vehiclePosition, vehicleTripUpdate) {
351
376
  }
352
377
 
353
378
  // Else try to calculate bearing from next stop
354
- if (
355
- vehicleTripUpdate &&
356
- vehicleTripUpdate?.trip_update?.stop_time_update?.length > 0
357
- ) {
379
+ if (vehicleTripUpdate?.trip_update?.stop_time_update?.length > 0) {
358
380
  const nextStopTimeUpdate =
359
381
  vehicleTripUpdate.trip_update.stop_time_update[0];
360
382
  const nextStop = stopData[nextStopTimeUpdate.stop_id];
@@ -436,7 +458,16 @@ function addVehicleMarker(vehiclePosition, vehicleTripUpdate) {
436
458
  return;
437
459
  }
438
460
 
439
- const visibleTimetableId = jQuery('.timetable:visible').data('timetable-id');
461
+ const visibleTimetable = document.querySelector(
462
+ '.timetable:not([style*="display: none"])',
463
+ );
464
+ const visibleTimetableId = visibleTimetable
465
+ ? visibleTimetable.dataset.timetableId
466
+ : null;
467
+
468
+ if (!visibleTimetableId) {
469
+ return;
470
+ }
440
471
 
441
472
  const vehicleDirectionArrow = getVehicleDirectionArrow(
442
473
  vehiclePosition,
@@ -495,9 +526,11 @@ function animateVehicleMarker(vehicleMarker, vehiclePosition) {
495
526
 
496
527
  // Check if vehiclePopup element exists and is for this vehicle
497
528
  const popupElement = vehiclePopup.getElement();
498
- const vehiclePopupContentId = `vehicle-popup-${vehiclePosition.vehicle.vehicle.id}`;
499
529
  const markerPopupIsOpenForThisVehicle =
500
- popupElement && popupElement.querySelector(`#${vehiclePopupContentId}`);
530
+ popupElement &&
531
+ popupElement.querySelector(
532
+ `[data-vehicle-id="${vehiclePosition.vehicle.vehicle.id}"]`,
533
+ );
501
534
 
502
535
  // Check if the open vehicle popup is for this vehicle
503
536
  if (vehiclePopup.isOpen() && markerPopupIsOpenForThisVehicle) {
@@ -505,7 +538,7 @@ function animateVehicleMarker(vehicleMarker, vehiclePosition) {
505
538
  vehiclePopup.setLngLat([newLongitude, newLatitude]);
506
539
  }
507
540
 
508
- if (safeProgress != 1) {
541
+ if (safeProgress !== 1) {
509
542
  requestAnimationFrame(animation);
510
543
  }
511
544
  };
@@ -569,11 +602,13 @@ async function updateArrivals() {
569
602
  ]);
570
603
 
571
604
  if (!latestVehiclePositions?.length) {
572
- jQuery('.vehicle-legend-item').hide();
605
+ const legendItems = document.querySelectorAll('.vehicle-legend-item');
606
+ legendItems.forEach((item) => (item.style.display = 'none'));
573
607
  return;
574
608
  }
575
609
 
576
- jQuery('.vehicle-legend-item').show();
610
+ const legendItems = document.querySelectorAll('.vehicle-legend-item');
611
+ legendItems.forEach((item) => (item.style.display = ''));
577
612
 
578
613
  vehiclePositions = latestVehiclePositions.filter((vehiclePosition) => {
579
614
  if (
@@ -604,7 +639,7 @@ async function updateArrivals() {
604
639
  return tripIds.includes(vehiclePosition.vehicle.trip.trip_id);
605
640
  });
606
641
 
607
- tripUpdates = latestTripUpdates.filter((tripUpdate) => {
642
+ tripUpdates = latestTripUpdates?.filter((tripUpdate) => {
608
643
  if (
609
644
  !tripUpdate ||
610
645
  !tripUpdate.trip_update ||
@@ -645,13 +680,20 @@ async function updateArrivals() {
645
680
  );
646
681
  }
647
682
 
648
- const visibleTimetableId =
649
- jQuery('.timetable:visible').data('timetable-id');
650
- attachVehicleMarkerClickHandler(
651
- vehiclePosition,
652
- vehicleTripUpdate,
653
- maps[visibleTimetableId],
683
+ const visibleTimetable = document.querySelector(
684
+ '.timetable:not([style*="display: none"])',
654
685
  );
686
+ const visibleTimetableId = visibleTimetable
687
+ ? visibleTimetable.dataset.timetableId
688
+ : null;
689
+
690
+ if (visibleTimetableId) {
691
+ attachVehicleMarkerClickHandler(
692
+ vehiclePosition,
693
+ vehicleTripUpdate,
694
+ maps[visibleTimetableId],
695
+ );
696
+ }
655
697
  }
656
698
 
657
699
  // Remove vehicles not in the feed
@@ -677,11 +719,11 @@ function toggleMap(id) {
677
719
 
678
720
  // Update vehicle markers to use the current visible map
679
721
  for (const [vehicleId, vehicleMarker] of Object.entries(vehicleMarkers)) {
680
- const vehiclePosition = vehiclePositions.find(
722
+ const vehiclePosition = vehiclePositions?.find(
681
723
  (vehiclePosition) => vehiclePosition.vehicle.vehicle.id === vehicleId,
682
724
  );
683
725
 
684
- const vehicleTripUpdate = tripUpdates.find(
726
+ const vehicleTripUpdate = tripUpdates?.find(
685
727
  (tripUpdate) => tripUpdate.trip_update.vehicle.id === vehicleId,
686
728
  );
687
729
 
@@ -707,13 +749,18 @@ function createMap(id) {
707
749
  const geojson = geojsons[id];
708
750
 
709
751
  if (!geojson || geojson.features.length === 0) {
710
- jQuery(`#map_timetable_id_${id}`).hide();
752
+ const mapElement = document.querySelector(
753
+ `.map[data-timetable-id="${id}"]`,
754
+ );
755
+ if (mapElement) {
756
+ mapElement.style.display = 'none';
757
+ }
711
758
  return false;
712
759
  }
713
760
 
714
761
  const bounds = getBounds(geojson);
715
762
  const map = new maplibregl.Map({
716
- container: `map_timetable_id_${id}`,
763
+ container: document.querySelector(`.map[data-timetable-id="${id}"]`),
717
764
  style: mapStyleUrl,
718
765
  center: bounds.getCenter(),
719
766
  zoom: 12,
@@ -954,8 +1001,12 @@ function unHighlightStop(map, id) {
954
1001
  }
955
1002
 
956
1003
  function highlightTimetableStops(id, stopIds) {
957
- const table = jQuery(`#timetable_id_${id} table`);
958
- const isVertical = table.data('orientation') === 'vertical';
1004
+ const table = document.querySelector(
1005
+ `.timetable[data-timetable-id="${id}"] table`,
1006
+ );
1007
+ if (!table) return;
1008
+
1009
+ const isVertical = table.dataset.orientation === 'vertical';
959
1010
 
960
1011
  if (isVertical) {
961
1012
  highlightVerticalTimetableStops(id, stopIds);
@@ -965,79 +1016,128 @@ function highlightTimetableStops(id, stopIds) {
965
1016
  }
966
1017
 
967
1018
  function highlightVerticalTimetableStops(id, stopIds) {
968
- const table = jQuery(`#timetable_id_${id} table`);
1019
+ const table = document.querySelector(
1020
+ `.timetable[data-timetable-id="${id}"] table`,
1021
+ );
1022
+ if (!table) return;
1023
+
969
1024
  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
- });
1025
+ const allCols = Array.from(table.querySelectorAll('colgroup col'));
1026
+
1027
+ for (const stopId of stopIds) {
1028
+ const col = table.querySelector(`colgroup col[data-stop-id="${stopId}"]`);
1029
+ if (col) {
1030
+ const index = allCols.indexOf(col);
1031
+ if (index !== -1) {
1032
+ columnIndexes.push(index);
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ // Remove highlighted class from all cells
1038
+ table
1039
+ .querySelectorAll('td, thead th')
1040
+ .forEach((el) => el.classList.remove('highlighted'));
982
1041
 
983
- table.find('td, thead th').removeClass('highlighted');
984
- table.find('.trip-row').each((index, row) => {
985
- jQuery('td', row).each((index, el) => {
1042
+ // Add highlighted class to cells in matching columns
1043
+ table.querySelectorAll('.trip-row').forEach((row) => {
1044
+ const cells = row.querySelectorAll('td');
1045
+ cells.forEach((cell, index) => {
986
1046
  if (columnIndexes.includes(index)) {
987
- jQuery(el).addClass('highlighted');
1047
+ cell.classList.add('highlighted');
988
1048
  }
989
1049
  });
990
1050
  });
991
1051
 
992
- table.find('thead').each((index, thead) => {
993
- jQuery('th', thead).each((index, el) => {
1052
+ table.querySelectorAll('thead').forEach((thead) => {
1053
+ const headers = thead.querySelectorAll('th');
1054
+ headers.forEach((header, index) => {
994
1055
  if (columnIndexes.includes(index)) {
995
- jQuery(el).addClass('highlighted');
1056
+ header.classList.add('highlighted');
996
1057
  }
997
1058
  });
998
1059
  });
999
1060
  }
1000
1061
 
1001
1062
  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');
1063
+ const table = document.querySelector(
1064
+ `.timetable[data-timetable-id="${id}"] table`,
1065
+ );
1066
+ if (!table) return;
1067
+
1068
+ // Remove highlighted class from all stop rows
1069
+ table
1070
+ .querySelectorAll('.stop-row')
1071
+ .forEach((row) => row.classList.remove('highlighted'));
1072
+
1073
+ // Add highlighted class to matching stop rows
1074
+ for (const stopId of stopIds) {
1075
+ const stopRow = table.querySelector(`[data-stop-id="${stopId}"]`);
1076
+ if (stopRow) {
1077
+ stopRow.classList.add('highlighted');
1078
+ }
1079
+ }
1008
1080
  }
1009
1081
 
1010
1082
  function unHighlightTimetableStops(id) {
1011
- const table = jQuery(`#timetable_id_${id} table`);
1012
- const isVertical = table.data('orientation') === 'vertical';
1083
+ const table = document.querySelector(
1084
+ `.timetable[data-timetable-id="${id}"] table`,
1085
+ );
1086
+ if (!table) return;
1087
+
1088
+ const isVertical = table.dataset.orientation === 'vertical';
1013
1089
 
1014
1090
  if (isVertical) {
1015
- table.find('td, thead th').removeClass('highlighted');
1091
+ table
1092
+ .querySelectorAll('td, thead th')
1093
+ .forEach((el) => el.classList.remove('highlighted'));
1016
1094
  } else {
1017
- table.find('.stop-row').removeClass('highlighted');
1095
+ table
1096
+ .querySelectorAll('.stop-row')
1097
+ .forEach((row) => row.classList.remove('highlighted'));
1018
1098
  }
1019
1099
  }
1020
1100
 
1021
1101
  function setupTableHoverListeners(id, map) {
1022
- jQuery('th, td', jQuery(`#timetable_id_${id} table`)).hover(
1023
- (event) => {
1102
+ const table = document.querySelector(
1103
+ `.timetable[data-timetable-id="${id}"] table`,
1104
+ );
1105
+ if (!table) return;
1106
+
1107
+ const cells = table.querySelectorAll('th, td');
1108
+ cells.forEach((cell) => {
1109
+ cell.addEventListener('mouseenter', (event) => {
1024
1110
  const stopId = getStopIdFromTableCell(event.target);
1025
1111
  if (stopId !== undefined) {
1026
1112
  highlightStop(map, id, [stopId.toString()]);
1027
1113
  }
1028
- },
1029
- () => unHighlightStop(map, id),
1030
- );
1114
+ });
1115
+
1116
+ cell.addEventListener('mouseleave', () => {
1117
+ unHighlightStop(map, id);
1118
+ });
1119
+ });
1031
1120
  }
1032
1121
 
1033
1122
  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');
1123
+ const table = cell.closest('table');
1124
+ if (!table) return undefined;
1125
+
1126
+ if (table.dataset.orientation === 'vertical') {
1127
+ const row = cell.parentElement;
1128
+ const cellIndex = Array.from(row.children).indexOf(cell);
1129
+ const cols = table.querySelectorAll('colgroup col');
1130
+ if (cols[cellIndex]) {
1131
+ return cols[cellIndex].dataset.stopId;
1132
+ }
1038
1133
  } else {
1039
- return jQuery(cell).closest('tr').data('stop-id');
1134
+ const row = cell.closest('tr');
1135
+ if (row) {
1136
+ return row.dataset.stopId;
1137
+ }
1040
1138
  }
1139
+
1140
+ return undefined;
1041
1141
  }
1042
1142
 
1043
1143
  function createMaps() {