Skip to content

Commit 6d793e1

Browse files
committed
Show distance along route in hover popup
Display train ID Filter out past trains which still get returned by OBA Improve focus behavior, turn off popup focus steal which was messing with zooming Store map parameters in session instead of URL so hash to be used for intra-page links Add recommended stops for solo runners to logistics FAQ Add convenience shops to lower-zoom-visible POIs Cartography updates to give map more texture at low zooms
1 parent 13d5908 commit 6d793e1

File tree

6 files changed

+208
-202
lines changed

6 files changed

+208
-202
lines changed

js/RelayMap.js

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { Protocol } from 'https://cdn.jsdelivr.net/npm/pmtiles@3.0.7/+esm';
44

55
const mapboxKey = 'pk.eyJ1Ijoibmlja3N3YWxrZXIiLCJhIjoiY2t0ZjgyenE4MDR1YjJ1cno0N3hxYzI4YSJ9.ivPdsoEtV9TaLGbOOfFXKA'
66
import pmtiles from 'https://cdn.jsdelivr.net/npm/pmtiles@3.0.7/+esm'
7+
8+
import {distance, point, nearestPointOnLine, lineString} from '@turf';
79
const transformRequest = (url, resourceType) => {
810
if (isMapboxURL(url)) {
911
return transformMapboxUrl(url, resourceType, mapboxKey)
@@ -107,7 +109,23 @@ export class RelayMap extends HTMLElement {
107109
this.mapReady.then(() => {
108110
this.map.getSource("rail-lines").setData(railLines)
109111
})
110-
112+
}
113+
computeDistanceAlongLegs(queryPoint, legs) {
114+
let totalDistance = 0
115+
// Merge all legs into a single line
116+
let line = lineString(legs.reduce((acc, leg) => acc.concat(leg.geometry.coordinates), []))
117+
// Get nearest point on line
118+
let nearest = nearestPointOnLine(line, queryPoint, {units: 'meters'})
119+
// Calculate cumulative distance up to index
120+
for (let i = 1; i < line.geometry.coordinates.length; i++) {
121+
let segmentDist = distance(line.geometry.coordinates[i-1], line.geometry.coordinates[i], {units: 'meters'})
122+
totalDistance += segmentDist
123+
if (i === nearest.properties.index) {
124+
totalDistance += distance(nearest.geometry.coordinates, line.geometry.coordinates[i], {units: 'meters'})
125+
break
126+
}
127+
}
128+
return [totalDistance, {lng: nearest.geometry.coordinates[0], lat: nearest.geometry.coordinates[1]}]
111129
}
112130
addRelayLine(legs, exchanges, exchangeNames, useStationCodes=false, lineColors={}, imgBasePath="") {
113131

@@ -201,16 +219,16 @@ export class RelayMap extends HTMLElement {
201219
let currentActiveLeg = null
202220
let currentLegPopup = null
203221
map.on('click', 'legs', (e) => {
204-
const coordinates = e.features[0].geometry.coordinates;
205-
const leg = e.features[0]
222+
let leg = legsData.slice().filter(l => l.properties.id === e.features[0].id)[0]
223+
let coordinates = leg.geometry.coordinates
206224
const bounds = coordinates.reduce((bounds, coord) => {
207225
return bounds.extend(coord);
208226
}, new maplibregl.LngLatBounds(coordinates[0], coordinates[0]));
209227
currentLegPopup = new maplibregl.Popup({
210228
anchor: "bottom-left",
211229
offset: [16, 0],
212230
className: "leg-popup",
213-
focusAfterOpen: true
231+
focusAfterOpen: false
214232
})
215233
.setLngLat([bounds.getEast(), bounds.getCenter().lat])
216234
.setMaxWidth("300px")
@@ -225,19 +243,44 @@ export class RelayMap extends HTMLElement {
225243
profile.style.width = "100%"
226244
profile.style.height = "64px"
227245
// Maplibre strips out elevation (and any further data) per point. Get data straight from legs
228-
profile.elevationData = legs.features.filter(l => l.properties.id === leg.id)[0].geometry.coordinates
246+
profile.elevationData = leg.geometry.coordinates
229247
map.fitBounds(bounds, {
230248
padding: 32
231249
});
232250
this.highlightLeg(leg.id)
233251
})
252+
// Create a popup, but don't add it to the map yet.
253+
const distancePopup = new maplibregl.Popup({
254+
closeButton: false,
255+
closeOnClick: false,
256+
focusAfterOpen: false,
257+
className: 'distance-popup'
258+
});
234259

235-
map.on('mouseenter', 'legs', () => {
260+
map.on('mouseenter', 'legs', (e) => {
236261
map.getCanvas().style.cursor = 'pointer';
237-
262+
const coordinates = [e.lngLat.lng, e.lngLat.lat];
263+
let leg = legsData.slice().filter(l => l.properties.id === e.features[0].id)
264+
const [distanceAlongLine, _] = this.computeDistanceAlongLegs(point(coordinates), legsData.slice())
265+
const [distanceAlongLeg, nearestPoint] = this.computeDistanceAlongLegs(point(coordinates), leg)
266+
267+
distancePopup
268+
.setLngLat(nearestPoint)
269+
.setHTML(`${(distanceAlongLine / 1609.34).toFixed(2)}mi <br\><span class="leg-dist">${e.features[0].id + 1}: ${(distanceAlongLeg / 1609.34).toFixed(2)}mi</span>`)
270+
.addTo(map);
271+
});
272+
map.on('mousemove', 'legs', (e) => {
273+
const coordinates = [e.lngLat.lng, e.lngLat.lat];
274+
let leg = legsData.slice().filter(l => l.properties.id === e.features[0].id)
275+
const [distanceAlongLine, _] = this.computeDistanceAlongLegs(point(coordinates), legsData)
276+
const [distanceAlongLeg, nearestPoint] = this.computeDistanceAlongLegs(point(coordinates), leg)
277+
distancePopup
278+
.setLngLat(nearestPoint)
279+
.setHTML(`${(distanceAlongLine / 1609.34).toFixed(2)}mi <br\><span class="leg-dist">${e.features[0].id + 1}: ${(distanceAlongLeg / 1609.34).toFixed(2)}mi</span>`)
238280
});
239281
map.on('mouseleave', 'legs', () => {
240282
map.getCanvas().style.cursor = '';
283+
distancePopup.remove()
241284
});
242285
})
243286
}
@@ -282,8 +325,8 @@ export class RelayMap extends HTMLElement {
282325
const { stopCodeNorth, stopCodeSouth } = exchange.properties;
283326

284327
const updateArrivals = async () => {
285-
const northboundArrivals = await endpoint(stopCodeNorth);
286-
const southboundArrivals = await endpoint(stopCodeSouth);
328+
let northboundArrivals = await endpoint(stopCodeNorth);
329+
let southboundArrivals = await endpoint(stopCodeSouth);
287330

288331
const currentTime = new Date();
289332

@@ -292,7 +335,7 @@ export class RelayMap extends HTMLElement {
292335
const isRealtime = arrival.predictedArrivalTime !== null;
293336
const minutesUntilArrival = Math.round((new Date(arrivalTime) - currentTime) / 60000);
294337
let duration = `${minutesUntilArrival} min`;
295-
if (minutesUntilArrival <= 0) {
338+
if (minutesUntilArrival === 0) {
296339
duration = 'now';
297340
}
298341
let realtimeSymbol = '';
@@ -302,9 +345,14 @@ export class RelayMap extends HTMLElement {
302345
return {
303346
time: new Date(arrivalTime),
304347
realtime: isRealtime,
348+
minutesUntilArrival: minutesUntilArrival,
305349
html: `<div><span class="trip-destination float-start"><span class="line-marker line-${arrival.routeId}"></span> ${arrival.headsign}</span>&nbsp;&nbsp;<span class="trip-eta float-end">${realtimeSymbol}${duration}</span></div>`
306350
};
307351
}
352+
// Filter out arrivals that have already passed
353+
northboundArrivals = northboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
354+
southboundArrivals = southboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
355+
308356

309357
// At most, show next two arrivals for each direction
310358
northboundArrivals.splice(2);
@@ -331,7 +379,7 @@ export class RelayMap extends HTMLElement {
331379
};
332380

333381
// Create and show a single popup anchored at the top left
334-
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup' })
382+
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup', closeOnClick: false, focusAfterOpen: false})
335383
.setLngLat(exchangeCoords)
336384
.setHTML('Loading...')
337385
.addTo(map);
@@ -346,7 +394,6 @@ export class RelayMap extends HTMLElement {
346394
}
347395
};
348396

349-
// Bind the handler to the map's moveend event
350397
map.on('moveend', handleMapMoveEnd);
351398

352399
// Call the handler immediately to handle the initial load
@@ -371,8 +418,6 @@ relay-map {
371418
height: 100%;
372419
}
373420
`
374-
// Make the map focusable
375-
this.tabIndex = 0
376421
let centerValue = this.attributes.getNamedItem("center").value
377422
let boundaryValue = this.attributes.getNamedItem("max-bounds").value
378423
this.style.display = "block"
@@ -381,23 +426,36 @@ relay-map {
381426
container: this,
382427
attributionControl: false,
383428
style: this.attributes.getNamedItem("style-href").value,
384-
center: center,
385-
zoom: 9,
429+
center: sessionStorage.getItem('mapCenter') ? JSON.parse(sessionStorage.getItem('mapCenter')) : center,
430+
zoom: Number(sessionStorage.getItem('mapZoom')) || 9,
431+
pitch: Number(sessionStorage.getItem('mapPitch')) || 0,
432+
bearing: Number(sessionStorage.getItem('mapBearing')) || 0,
386433
minZoom: 8,
387434
maxBounds: boundary,
388-
hash: true,
435+
hash: false,
389436
transformRequest: transformRequest,
390437
});
391438
// Don't break basic page scrolling until the map is focused
392439
map.scrollZoom.disable()
393-
let container = map.getContainer();
394-
// container needs to have tabindex=0 to be focusable
395-
map.on("click", () => container.focus())
396-
map.on("pitchstart", () => container.focus())
397-
map.on("drag", () => container.focus())
440+
let canvas = map.getCanvas()
441+
map.on("click", () => canvas.focus())
442+
map.on("pitchstart", () => canvas.focus())
443+
map.on("drag", () => canvas.focus())
398444
map.on("load", () => this._resolveMapReady())
399-
container.addEventListener('focus', () => map.scrollZoom.enable());
400-
container.addEventListener('blur', () => map.scrollZoom.disable());
445+
map.on('moveend', () => {
446+
// Store the current center, zoom, pitch, and bearing in session storage
447+
sessionStorage.setItem('mapCenter', JSON.stringify(map.getCenter()));
448+
sessionStorage.setItem('mapZoom', JSON.stringify(map.getZoom()));
449+
sessionStorage.setItem('mapPitch', JSON.stringify(map.getPitch()));
450+
sessionStorage.setItem('mapBearing', JSON.stringify(map.getBearing()));
451+
});
452+
canvas.addEventListener('focus', () => map.scrollZoom.enable());
453+
canvas.addEventListener('blur', () => {
454+
// Check whether focus is within container
455+
if (!this.contains(document.activeElement))
456+
map.scrollZoom.disable()
457+
});
458+
401459
let nav = new maplibregl.NavigationControl();
402460
map.addControl(nav, 'top-left');
403461
map.addControl(new maplibregl.FullscreenControl({container: map.getContainer()}), 'top-left');

js/TransitVehicleTracker.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ export class TransitVehicleTracker {
4848
const vehicles = trips.map(trip => {
4949
const status = trip.status;
5050
const tripHeadsign = tripMap.get(status.activeTripId);
51-
51+
let lastIdNumber = status.activeTripId.split('_')
52+
lastIdNumber = lastIdNumber[lastIdNumber.length - 1]
5253
return {
53-
id: status.activeTripId,
54+
id: lastIdNumber,
5455
lat: status.position.lat,
5556
lon: status.position.lon,
5657
bearing: status.orientation,
@@ -59,7 +60,6 @@ export class TransitVehicleTracker {
5960
headsign: tripHeadsign
6061
};
6162
});
62-
6363
this.vehicles = vehicles;
6464
this.emitVehicleData(vehicles);
6565
}

0 commit comments

Comments
 (0)