import * as L from "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js"; import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/module.esm.min.js"; import * as shapes from "./shapes.js"; import stations from "./stations.js" /** @type {number} */ let lastUpdated; function displayLastUpdated() { document.getElementById("last-updated").innerText = `${Math.floor((Date.now() - (lastUpdated * 1000)) / 1000)}s ago` } setInterval(displayLastUpdated, 1000) const RouteType = { TRAM: 1, SUBWAY: 2, RAIL: 3, BUS: 4, } const ICONS = { bus: ``, tram: ``, train: ``, }; var map = L.map("map").setView([40.656734, -111.890818], 12); L.tileLayer( "https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiY2pkZW5pbyIsImEiOiJjbHdiMG52amcwaGd4MmttbWtlOWt5Mm1iIn0.GhHTt4W_mZpQcLYNkhsG_w", { maxZoom: 19, attribution: '© OpenStreetMap', } ).addTo(map); map.on("zoomstart", () => { document.body.classList.add("skip-vehicle-animation") }) map.on("zoomend", () => { document.body.classList.remove("skip-vehicle-animation") }) const busLayer = L.layerGroup().addTo(map); const brtLayer = L.layerGroup().addTo(map); const traxLayer = L.layerGroup().addTo(map); const frontRunnerLayer = L.layerGroup().addTo(map); L.polyline(shapes.blueLine, { color: "#004a97", zIndexOffset: 10 }).addTo(map); L.polyline(shapes.redLine, { color: "#be2036", zIndexOffset: 10 }).addTo(map); L.polyline(shapes.greenLine, { color: "#2eb566", zIndexOffset: 10 }).addTo(map); L.polyline(shapes.sLine, { color: "#77777a", zIndexOffset: 10 }).addTo(map); L.polyline(shapes.frontRunner, { color: "#c227b9", zIndexOffset: 10 }).addTo(map); // render stations for (const station of stations) { let color = ""; switch (station.lines[0]) { case "red": color = "#be2036" break case "blue": color = "#004a97" break case "green": color = "#2eb566" break case "s-line": color = "#77777a" break case "frontrunner": color = "#c227b9" break } L.marker([station.lat, station.lon], { zIndexOffset: 2000, icon: L.divIcon({ className: "", html: `
`, iconSize: [5, 5], }) }).addTo(map).bindPopup(station.name.replace(" Station", "")) } function clearMap() { busLayer.clearLayers(); brtLayer.clearLayers(); traxLayer.clearLayers(); frontRunnerLayer.clearLayers(); } function routeDesignator(route) { if (route.id == "92235") { return "OGX"; } else if (route.id == "3686") { return "UVX"; } else if (route.id == "87711") { return "MVX"; } else if (route.type == RouteType.BUS) { return "#" + route.short_name; } else { return route.long_name; } } function populateTotals(vehicles) { const totals = { bus: 0, trax: 0, frontrunner: 0, } for (const vehicle of vehicles) { switch(vehicle.route.type) { case RouteType.BUS: totals.bus++; break; case RouteType.TRAM: totals.trax++; break; case RouteType.RAIL: totals.frontrunner++; break; } } document.getElementById("count-bus").innerText = totals.bus.toString() document.getElementById("count-trax").innerText = totals.trax.toString() document.getElementById("count-frontrunner").innerText = totals.frontrunner.toString() } let bannerVisible = false let currentVehicles = new Map() function renderVehicleIcon(vehicle) { return L.divIcon({ className: "", html: `
${ vehicle.route.type == RouteType.BUS ? ICONS.bus : vehicle.route.type == RouteType.TRAM ? ICONS.tram : vehicle.route.type == RouteType.RAIL ? ICONS.train : "" }
`, }) } function vehiclePopupContent(vehicle) { let content = `${routeDesignator(vehicle.route)}` if (vehicle.headsign) { content += ` to ${vehicle.headsign.replace( /^to /i, "" )}` } if (vehicle.nearest_station) { content += `
@ ${vehicle.nearest_station.name}` } content += `

vehicle #: ${vehicle.id} · trip ID: ${vehicle.trip_id}` return content } function renderVehicle(vehicle) { return L.marker([vehicle.lat, vehicle.lon], { zIndexOffset: vehicle.route.type == RouteType.TRAM || vehicle.route.type == RouteType.RAIL ? 4000 : 3000, icon: renderVehicleIcon(vehicle), }).bindPopup(vehiclePopupContent(vehicle)); } async function reload() { const { vehicles, info } = await fetch("/api", { headers: { Accept: "application/json" } }).then((r) => r.json()); lastUpdated = info.last_update displayLastUpdated() populateTotals(vehicles) if (vehicles.length == 0) { if (!bannerVisible) { document.getElementById("banner").classList.remove("hidden") bannerVisible = true map.invalidateSize() } } else { if (bannerVisible) { document.getElementById("banner").classList.add("hidden") bannerVisible = false map.invalidateSize() } } const seenVehicleIds = new Set() vehicles.forEach((vehicle) => { seenVehicleIds.add(vehicle.id) if (currentVehicles.has(vehicle.id)) { // if this vehicle is already on the map, update its location currentVehicles.get(vehicle.id).marker.setLatLng([vehicle.lat, vehicle.lon]) currentVehicles.get(vehicle.id).marker.setIcon(renderVehicleIcon(vehicle)) currentVehicles.get(vehicle.id).marker.setPopupContent(vehiclePopupContent(vehicle)) } else { const marker = renderVehicle(vehicle) marker.addTo( ["92235", "3686", "87711"].includes(vehicle.route.id) ? brtLayer : vehicle.route.type == RouteType.TRAM ? traxLayer : vehicle.route.type == RouteType.RAIL ? frontRunnerLayer : busLayer ); currentVehicles.set(vehicle.id, { vehicle, marker, }) } }); // remove any vehicles that should be removed for (const key of currentVehicles.keys()) { if (!seenVehicleIds.has(key)) { currentVehicles.get(key).marker.remove() currentVehicles.delete(key) } } } reload(); setInterval(reload, 5000); Alpine.data('app', () => ({ busLayer: true, brtLayer: true, traxLayer: true, frontRunnerLayer: true, toggleLayer(name) { let layer; switch (name) { case "bus": layer = busLayer; break; case "brt": layer = brtLayer; break; case "trax": layer = traxLayer; break; case "frontRunner": layer = frontRunnerLayer; break; } let oldState = this[`${name}Layer`] if (!oldState) { layer?.addTo(map); } else { layer?.remove(); } this[`${name}Layer`] = !oldState }, })) Alpine.start()