1jQuery(async function ($) { 2 var bustimeAPI = '/wp-content/themes/cccta/api/bustime.php'; 3 var routeList; 4 var stopList; 5 var selectedStop; 6 7 function capitalize(string) { 8 return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); 9 } 10 11 function formatPrediction(time) { 12 var ms = moment(time, 'YYYYMMDD HH:mm').diff(moment()); 13 return Math.floor(moment.duration(ms).asMinutes()) + ' min'; 14 } 15 16 async function getRouteList() { 17 try { 18 const response = await fetch('/wp-json/wp/v2/route?per_page=100'); 19 if (!response.ok) { 20 throw new Error(`HTTP error! status: ${response.status}`); 21 } 22 const data = await response.json(); 23 routeList = data; 24 } catch (error) { 25 console.log(error); 26 } 27 } 28 29 async function getRoutes() { 30 try { 31 const params = new URLSearchParams({ command: 'getroutes' }); 32 const response = await fetch(`${bustimeAPI}?${params}`); 33 if (!response.ok) { 34 throw new Error(`HTTP error! status: ${response.status}`); 35 } 36 const data = await response.json(); 37 38 if (data['bustime-response'].error) { 39 return console.error(data['bustime-response'].error) 40 } 41 var routes = data['bustime-response'].routes; 42 var options = routes.map(function (route) { 43 return $('<option>') 44 .text('Route ' + route.rt) 45 .attr('value', route.rt); 46 }); 47 48 $('#real-time-arrival-route option').not(':first-child').remove(); 49 $('#real-time-arrival-route') 50 .append(options) 51 .prop('disabled', false); 52 53 clearDirections(); 54 clearStops(); 55 } catch (error) { 56 console.log(error); 57 } 58 } 59 60 async function getStopList() { 61 62 const mapRowsToObjects = ({ fields, rows }) => 63 rows.map(row => Object.fromEntries(fields.map((field, index) => [field, row[index]]))) 64 65 try { 66 const response = await fetch('/data/stops.json'); 67 stopList = mapRowsToObjects(await response.json()); 68 } catch (error) { 69 console.log(error); 70 } 71 const parentStations = _.compact(_.uniqBy(stopList, stop => stop.parent_station).map(stop => stop.parent_station)); 72 const source = _.sortBy(stopList, stop => stop.is_parent_station) 73 .map(stop => { 74 const stopDisplayId = parentStations.includes(stop.stop_id) ? 'All' : stop.stop_id; 75 76 return { 77 name: `${stop.stop_name} (${stopDisplayId})`, 78 id: stop.stop_id 79 } 80 }) 81 82 $('#real-time-arrival-stop-id').typeahead({ 83 source, 84 autoSelect: true, 85 afterSelect: currentItem => { 86 selectedStop = currentItem; 87 $('#real-time-arrival-stop-id-form').submit() 88 }, 89 items: 15 90 }); 91 } 92 93 async function getDirections(routeId) { 94 try { 95 const params = new URLSearchParams({ 96 command: 'getdirections', 97 rt: routeId 98 }); 99 const response = await fetch(`${bustimeAPI}?${params}`); 100 if (!response.ok) { 101 throw new Error(`HTTP error! status: ${response.status}`); 102 } 103 const data = await response.json(); 104 105 clearDirections(); 106 107 if (data['bustime-response'].error) { 108 return console.error(data['bustime-response'].error) 109 } 110 var directions = data['bustime-response'].directions; 111 var options = directions.map(function (direction) { 112 return $('<option>') 113 .text(capitalize(direction.name.toLowerCase())) 114 .attr('value', direction.name); 115 }); 116 117 $('#real-time-arrival-direction') 118 .append(options) 119 .prop('disabled', false); 120 121 clearStops(); 122 123 if (directions.length === 1) { 124 $('#real-time-arrival-direction') 125 .val(directions[0].name) 126 .trigger('change'); 127 } 128 } catch (error) { 129 console.log(error); 130 } 131 } 132 133 async function getStops(routeId, directionId) { 134 try { 135 const params = new URLSearchParams({ 136 command: 'getstops', 137 rt: routeId, 138 dir: directionId 139 }); 140 const response = await fetch(`${bustimeAPI}?${params}`); 141 if (!response.ok) { 142 throw new Error(`HTTP error! status: ${response.status}`); 143 } 144 const data = await response.json(); 145 146 clearStops(); 147 148 if (data['bustime-response'].error) { 149 return console.error(data['bustime-response'].error) 150 } 151 152 var stops = data['bustime-response'].stops; 153 var options = stops.map(function (stop) { 154 return $('<option>') 155 .text(stop.stpnm) 156 .attr('value', stop.stpid); 157 }); 158 159 $('#real-time-arrival-stop') 160 .append(options) 161 .prop('disabled', false); 162 } catch (error) { 163 console.log(error); 164 } 165 } 166 167 function showResults(results) { 168 $('#real-time-arrival-results').empty() 169 .append(results); 170 } 171 172 async function getPredictions(routeId, directionId, stopIds) { 173 async function fetchPredictions(routeId, directionId, stopIdsToFetch) { 174 const params = new URLSearchParams({ 175 command: 'getpredictions' 176 }); 177 178 if (routeId) { 179 params.set('rt', routeId); 180 } 181 if (directionId) { 182 params.set('dir', directionId); 183 } 184 if (stopIdsToFetch.length > 0) { 185 params.set('stpid', stopIdsToFetch.join(',')); 186 } 187 188 const response = await fetch(`${bustimeAPI}?${params}`); 189 if (!response.ok) { 190 throw new Error(`HTTP error! status: ${response.status}`); 191 } 192 return await response.json(); 193 } 194 195 $('#refresh-results .fa-sync-alt').addClass('fa-spin'); 196 $('.real-time-arrival-prediction').each(function () { 197 $(this).addClass('stale'); 198 }); 199 200 try { 201 // Split stopIds into chunks of 10
202 const stopIdChunks = []; 203 const stopIdsCopy = [...stopIds]; 204 while (stopIdsCopy.length > 0) { 205 stopIdChunks.push(stopIdsCopy.splice(0, 10)); 206 } 207 208 const results = await Promise.all(stopIdChunks.map(stopIdChunk => 209 fetchPredictions(routeId, directionId, stopIdChunk) 210 )); 211 212 const predictions = []; 213 const errors = []; 214 215 results.forEach(data => { 216 if (!data?.['bustime-response']) { 217 errors.push({ msg: 'Invalid response from server' }); 218 return; 219 } 220 221 if (data['bustime-response'].error) { 222 errors.push(...data['bustime-response'].error); 223 } 224 225 if (data['bustime-response'].prd) { 226 predictions.push(...data['bustime-response'].prd); 227 } 228 }); 229 230 $('#refresh-results .fa-sync-alt').removeClass('fa-spin'); 231 renderPredictions(routeId, predictions, errors); 232 } catch (error) { 233 console.error('Error fetching predictions:', error); 234 showResults('Unable to fetch predictions. Please try again.'); 235 $('#refresh-results .fa-sync-alt').removeClass('fa-spin'); 236 } 237 } 238 239 function renderPredictions(routeId, predictions, errors) { 240 if (!predictions.length) { 241 var results = 'No predictions available.'; 242 243 if (errors.length > 0) { 244 results = _.uniq(errors.map(error => error.msg)).join(', ') 245 } 246 247 showResults(results); 248 return; 249 } 250 251 predictions.forEach(prediction => { 252 const routeInfo = _.find(routeList, { 253 route_short_name: prediction.rt 254 }); 255 if (routeInfo) { 256 prediction.route_short_name = routeInfo.route_short_name; 257 prediction.route_color = routeInfo.route_color; 258 prediction.route_text_color = routeInfo.route_text_color; 259 } 260 }); 261 262 var futurePredictions = predictions.filter(prediction => { 263 return moment(prediction.prdtm, 'YYYYMMDD HH:mm').diff(moment()) >= 0; 264 }) 265 266 var results = _.sortBy(futurePredictions, prediction => { 267 return parseInt(prediction.route_short_name, 10); 268 }).map(prediction => { 269 var routeCircle = ''; 270 271 if (prediction.route_short_name) { 272 routeCircle = $('<a>') 273 .attr('href', `/routes/${prediction.route_short_name}`) 274 .addClass('route-circle route-circle-medium') 275 .css('background-color', prediction.route_color) 276 .css('color', prediction.route_text_color) 277 .text(prediction.route_short_name); 278 } 279 280 return $('<div>') 281 .addClass('row no-gutters real-time-arrival-prediction') 282 .append($('<div>') 283 .addClass('col-sm-9 col-8 d-flex align-items-center') 284 .append(routeCircle) 285 .append($('<span>') 286 .addClass('font-weight-bold') 287 .text(prediction.des)) 288 ) 289 .append($('<div>') 290 .addClass('font-weight-bold col-sm-3 col-4 d-flex align-items-center justify-content-end') 291 .text(formatPrediction(prediction.prdtm))); 292 }); 293 294 var headers = $('<div>') 295 .addClass('row no-gutters mb-1') 296 .append($('<div>') 297 .addClass('real-time-arrival-label col-sm-9 col-8 d-flex align-items-end') 298 .text('ROUTE') 299 ) 300 .append($('<div>') 301 .addClass('real-time-arrival-label col-sm-3 col-4 text-right') 302 .text('ESTIMATED ARRIVAL') 303 ) 304 305 results.unshift(headers); 306 307 if (_.uniqBy(predictions, 'stpid').length === 1) { 308 results.unshift($('<div>').addClass('real-time-arrival-label').text('Stop ID: ' + predictions[0].stpid)); 309 } 310 311 if (routeId) { 312 var direction = $('#real-time-arrival-direction option:selected').text(); 313 results.unshift($('<div>').addClass('real-time-arrival-label').text('Direction: ' + direction)); 314 } 315 316 results.unshift( 317 $('<div class="d-flex justify-content-between">') 318 .append([ 319 $('<h4>').addClass('mt-2 mb-0').text(predictions[0].stpnm), 320 $('<a class="results-time" href="#" id="refresh-results">').append(`<i class="fas fa-sync-alt"></i> ${moment().format('h:mm A')}`) 321 ]) 322 ); 323 324 showResults(results); 325 } 326 327 function resetRoutes() { 328 $('#real-time-arrival-route option').first().prop('selected', true); 329 } 330 331 function clearDirections() { 332 $('#real-time-arrival-direction option').not(':first-child').remove(); 333 $('#real-time-arrival-direction').prop('disabled', true); 334 } 335 336 function clearStops() { 337 $('#real-time-arrival-stop option').not(':first-child').remove(); 338 $('#real-time-arrival-stop').prop('disabled', true); 339 clearResults(); 340 } 341 342 function clearResults() { 343 $('#real-time-arrival-results').empty(); 344 } 345 346 $('#real-time-arrival-route').change(function () { 347 var routeId = $('#real-time-arrival-route').val(); 348 349 if (routeId === '') { 350 clearDirections(); 351 clearStops(); 352 return; 353 } 354 355 getDirections(routeId); 356 }); 357 358 $('#real-time-arrival-direction').change(function () { 359 var routeId = $('#real-time-arrival-route').val(); 360 var directionId = $('#real-time-arrival-direction').val(); 361 362 if (directionId === '') { 363 clearStops(); 364 return; 365 } 366 367 getStops(routeId, directionId); 368 }); 369 370 $('#real-time-arrival-stop').change(function () { 371 refreshPredictions(); 372 }); 373 374 $('input[name="arrival_type"]').change(function () { 375 var type = $('input[name="arrival_type"]:checked').val(); 376 377 $('#' + type).prop('hidden', false) 378 .siblings().prop('hidden', true); 379 380 resetRoutes(); 381 clearDirections(); 382 clearStops(); 383 clearResults(); 384 }); 385 386 $('#real-time-arrival-stop-id').change(() => { 387 selectedStop = undefined; 388 }) 389 390 $('#real-time-arrival-stop-id-form').submit(function (e) { 391 e.preventDefault(); 392 393 var stopIds; 394 395 if (selectedStop) { 396 // Check if selected item is parent_station 397 const childStations = stopList.filter(stop => stop.parent_station === selectedStop.id); 398 399 if (childStations.length > 0) { 400 stopIds = childStations.map(stop => stop.stop_id); 401 } else { 402 stopIds = [selectedStop.id] 403 } 404 } else { 405 var stopId = $('#real-time-arrival-stop-id').val(); 406 407 if (!stopId) { 408 return; 409 } 410 411 stopIds = [stopId] 412 } 413 414 getPredictions(undefined, undefined, stopIds); 415 }) 416 417 $('#real-time-arrival-route-form').submit(function (e) { 418 e.preventDefault(); 419 420 var routeId = $('#real-time-arrival-route').val(); 421 var directionId = $('#real-time-arrival-direction').val(); 422 var stopId = $('#real-time-arrival-stop').val(); 423 424 if (stopId === '') { 425 return; 426 } 427 428 getPredictions(routeId, directionId, [stopId]); 429 }) 430 431 $('#real-time-arrival-results').on('click', '#refresh-results', e => { 432 e.preventDefault(); 433 refreshPredictions(); 434 }) 435 436 function refreshPredictions() { 437 if ($('input[name="arrival_type"]:checked').val() === 'arrival-type-stop-id') { 438 $('#real-time-arrival-stop-id-form').submit(); 439 } else if ($('input[name="arrival_type"]:checked').val() === 'arrival-type-route') { 440 $('#real-time-arrival-route-form').submit(); 441 } 442 } 443 444 // Re-request predictions every 30 seconds 445 setInterval(() => { 446 refreshPredictions() 447 }, 30000) 448 449 // On page load 450 await getRoutes(); 451 await getRouteList(); 452 await getStopList(); 453});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.