@@ -4,6 +4,8 @@ import { Protocol } from 'https://cdn.jsdelivr.net/npm/pmtiles@3.0.7/+esm';
44
55const mapboxKey = 'pk.eyJ1Ijoibmlja3N3YWxrZXIiLCJhIjoiY2t0ZjgyenE4MDR1YjJ1cno0N3hxYzI4YSJ9.ivPdsoEtV9TaLGbOOfFXKA'
66import pmtiles from 'https://cdn.jsdelivr.net/npm/pmtiles@3.0.7/+esm'
7+
8+ import { distance , point , nearestPointOnLine , lineString } from '@turf' ;
79const 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> <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' ) ;
0 commit comments