60 lines
25 KiB
HTML
60 lines
25 KiB
HTML
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Deployed Sign Map</title><style>/*LEAFLET_CSS*/</style><style>/*MAPLIBRE_CSS*/</style><style>
|
||
*{box-sizing:border-box}body{margin:0;background:#f3f5f7;color:#162a38;font:14px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}button,input,select{font:inherit}button,a{cursor:pointer}button{border:1px solid #ced8df;background:white;border-radius:7px;padding:7px 10px;color:#19384b}button:hover{background:#e7f0f5}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #0673b5}header{padding:14px 22px;background:#122f40;color:white;display:flex;justify-content:space-between;align-items:center;gap:15px}h1{margin:0;font-size:22px;font-weight:650}header p{margin:3px 0 0;color:#c7d8e2;font-size:12px}.topstats{display:flex;gap:18px;white-space:nowrap}.topstats b{font-size:22px;display:block}.topstats span{color:#c7d8e2;font-size:12px}.notice{padding:10px 22px;background:#fff2cf;color:#624a0d;border-bottom:1px solid #e7d4a1;font-size:13px}.shell{display:grid;grid-template-columns:350px minmax(0,1fr);height:calc(100vh - 145px);min-height:550px}aside{background:#fff;overflow:auto;border-right:1px solid #d3dce2}.controls{padding:13px;position:sticky;top:0;background:white;z-index:3;border-bottom:1px solid #dae1e6}.controls input{width:100%;padding:8px;border:1px solid #c9d5de;border-radius:6px;margin-bottom:8px}.controls select{padding:6px;border:1px solid #c9d5de;border-radius:5px;width:100%}.listcount{font-size:12px;color:#566e7e;margin-top:8px}.player{display:flex;gap:10px;width:100%;text-align:left;border:0;border-radius:0;border-bottom:1px solid #e5ebef;padding:12px 14px;background:white}.player.selected{background:#e5f1f8;border-left:4px solid #137daf;padding-left:10px}.player strong{font-size:13px;display:block;font-weight:600}.player small{display:block;color:#596b76;font-size:12px;margin-top:3px}.statusicon{display:inline-flex;flex:0 0 auto;width:26px;height:26px;align-items:center;justify-content:center;border:2px solid white;box-shadow:0 1px 4px #0006;background:#17815a;color:white;border-radius:50%;font-size:16px;font-weight:bold}.statusicon.offline{background:#b7303f}.statusicon.kiosk{border-radius:5px;background:#226baf;position:relative}.statusicon.kiosk::after{content:"";position:absolute;right:-3px;bottom:-3px;width:9px;height:9px;background:#17815a;border:2px solid white;border-radius:50%}.statusicon.kiosk.offline::after{background:#b7303f}.statusicon.excluded{background:#6c7781;border-radius:5px}.statusicon.ambiguous{background:white;color:#986609;border:3px dashed #b37a12}.statusicon.ambiguous.offline{color:#b7303f;border-color:#b7303f}.markerwrap{background:none;border:none}.mapcol{position:relative;min-width:0;display:flex;flex-direction:column}#map{flex:1;min-height:350px;background:#e5ebef}.mapbar{display:flex;gap:7px;padding:9px 12px;background:#fff;border-bottom:1px solid #dae1e6;flex-wrap:wrap;align-items:center}.legend{font-size:12px;color:#3f5868;margin-left:auto}.legend i{display:inline-block;width:9px;height:9px;border-radius:50%;background:#17815a;margin-left:10px}.legend .red{background:#b7303f}.legend .blue{background:#226baf;border-radius:1px}.legend .amber{background:white;border:2px dashed #b37a12;width:12px;height:12px}.routepanel{padding:10px 12px;background:#fff;max-height:150px;overflow:auto;border-top:1px solid #dae1e6}.routepanel summary{cursor:pointer;font-weight:600}.routegrid{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.routechip{display:flex;align-items:center;gap:5px;font-size:12px;padding:3px 6px;border:1px solid #dce3e7;border-radius:5px}.routechip input{margin:0}.swatch{height:4px;width:20px;display:inline-block}.detail{position:absolute;right:14px;top:66px;width:370px;max-width:calc(100% - 28px);max-height:calc(100% - 135px);overflow:auto;background:white;box-shadow:0 8px 30px #1236;z-index:600;border-radius:10px;padding:18px}.detail h2{font-size:17px;margin:0 28px 10px 0}.detail h3{font-size:14px;margin:17px 0 5px}.detail p{margin:8px 0;font-size:13px}.detail dl{display:grid;grid-template-columns:105px 1fr;gap:6px;font-size:12px}.detail dt{color:#5b707e}.detail dd{margin:0;overflow-wrap:anywhere}.close{position:absolute;right:8px;top:8px;border:0;padding:3px 8px;font-size:21px}.badge{padding:3px 6px;background:#fff1c8;border-radius:4px;display:inline-block;font-size:12px}.stopcard{border-top:1px solid #e0e6eb;padding-top:8px;margin-top:8px;font-size:13px}.routebadge{display:inline-block;border-bottom:3px solid var(--route);margin:2px 5px 2px 0;padding:2px;color:#263f50;font-size:12px}dialog{border:0;border-radius:10px;width:1000px;max-width:94vw;max-height:90vh;padding:24px;color:#163043}dialog::backdrop{background:#102c40aa}dialog h2{margin:0 30px 12px 0}table{border-collapse:collapse;font-size:12px;width:100%}th,td{border-bottom:1px solid #dce3e7;text-align:left;padding:8px;vertical-align:top}th{background:#edf2f5;position:sticky;top:0}.scrolltable{max-height:52vh;overflow:auto}.muted{color:#617583;font-size:12px}.leaflet-tooltip{font:12px/1.4 -apple-system,sans-serif;max-width:300px;white-space:normal}.leaflet-control-attribution{font-size:10px}#basehint{position:absolute;bottom:39px;left:10px;z-index:401;background:#fffffff0;padding:4px 7px;border-radius:4px;font-size:11px;pointer-events:none}.hidden{display:none!important}@media(max-width:800px){header{padding:10px}.topstats{gap:9px}.topstats b{font-size:18px}.notice{padding:8px;font-size:12px}.shell{grid-template-columns:250px 1fr;height:calc(100vh - 160px)}.legend{display:none}}@media(max-width:560px){.shell{display:flex;flex-direction:column;height:auto}aside{max-height:260px;order:2}.mapcol{height:65vh;min-height:500px}.topstats span{font-size:10px}h1{font-size:17px}header p{font-size:10px}}
|
||
.navigation{display:flex;flex-direction:column;gap:7px;margin:10px 0}.navigation a{display:block;padding:10px 12px;border:1px solid #b8cedb;border-radius:6px;text-decoration:none;color:#075c8e;background:#f1f8fc;font-size:13px;min-height:42px}.navigation a:hover{background:#dfedf5}.basemaplabel{display:flex;align-items:center;gap:7px;font-size:12px;font-weight:600}.basemaplabel select{padding:7px;border:1px solid #b8ccd9;border-radius:6px;background:white;color:#163c51;font-size:13px}.leaflet-gl-layer{pointer-events:none}.maplibregl-control-container{display:none}</style></head><body>
|
||
<header><div><h1>Deployed Sign Map</h1><p>Captured transit-sign deployment snapshot</p></div><div class="topstats"><div><b id="stat-field">—</b><span>field players</span></div><div><b id="stat-health">—</b><span>online / offline in field</span></div><div><b id="stat-excluded">—</b><span>excluded</span></div></div></header>
|
||
<div class="notice"><strong>Location evidence:</strong> Native stop IDs resolved through CAD/AVL → GTFS. Marker and navigation coordinates come only from GTFS. Two kiosks use station reference points; their bay IDs are resolved.</div>
|
||
<div class="shell"><aside><div class="controls"><input id="search" aria-label="Search players, stop IDs or GTFS names" placeholder="Search players, stop IDs or names…"><select id="filter" aria-label="Filter players"><option value="field">All field players</option><option value="offline">Offline field players</option><option value="kiosk">Kiosks</option><option value="review">Coordinate differences to review</option><option value="excluded">Excluded test / showroom</option><option value="all">All account players</option></select><div id="listcount" class="listcount"></div></div><div id="players"></div></aside>
|
||
<main class="mapcol"><div class="mapbar"><label class="basemaplabel">Basemap <select id="basemap" aria-label="Basemap"><option value="positron">Positron · street labels</option><option value="positron-nolabels">Positron · no labels</option><option value="esri">Esri Gray Canvas</option><option value="osm">Original street map</option><option value="plain">Plain background</option></select></label><button id="fit">Fit field locations</button><button id="clear">Clear selection</button><button id="report">ID report</button><button id="sources">Sources & dates</button><div class="legend"><i></i> online <i class="red"></i> offline <i class="blue"></i> kiosk ▧ </div></div><div id="map" role="region" aria-label="Interactive map of RTC Washoe deployed sign reference locations"></div><div id="basehint">Street tiles need internet. Embedded routes and markers work offline.</div><section id="detail" class="detail hidden" aria-label="Selected player details"></section><details class="routepanel"><summary>Route lines · GTFS colors · service October 28, 2026</summary><div><button id="routesall">All routes</button> <button id="routesnone">Hide routes</button> <span class="muted">Select a player to emphasize routes serving its resolved stop(s). Kiosk routes come from the mapped bays only.</span></div><div id="routegrid" class="routegrid"></div></details></main></div>
|
||
<dialog id="reportdialog"><button class="close" aria-label="Close report">×</button><h2>Resolved native stop-ID crosswalk</h2><p><strong>Native stop IDs resolved by explicit source IDs.</strong> Each native GetStop response supplies SrcStopID, which joins to current GTFS stop_id. Names and coordinate differences are included for review. Native IDs must not be assumed to share the GTFS identifier namespace.</p><p>Marker and navigation coordinates always use GTFS stops.txt. Native API coordinates are retained only for comparison; OnSign locations are unused. Native stops that differ from GTFS by more than 25 metres are flagged for coordinate review. This does not invalidate their explicit ID mapping. Kiosk device positions remain unknown; station reference points and mapped bays are clearly labeled.</p><button id="csvdownload">Download full CSV crosswalk</button><div class="scrolltable"><table><thead><tr><th>Player / native ID</th><th>Verified GTFS stop</th><th>Coordinate evidence</th></tr></thead><tbody id="auditbody"></tbody></table></div></dialog>
|
||
<dialog id="sourcesdialog"><button class="close" aria-label="Close sources">×</button><h2>Sources, dates & limits</h2><div id="sourcebody"></div></dialog>
|
||
<script>/*LEAFLET_JS*/</script><script>/*MAPLIBRE_JS*/</script><script>/*MAPLIBRE_LEAFLET_JS*/</script><script>const DATA=/*DATA*/;
|
||
const $=id=>document.getElementById(id),esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const map=L.map('map',{preferCanvas:true,minZoom:1,maxZoom:20}).setView([39.519,-119.79],12), routeLayers={}, markerItems=[];let selected=null;const enabled=new Set(DATA.routes.map(r=>r.id));
|
||
const CARTO_BASEMAP_KEY=/*CARTO_KEY*/;
|
||
const cartoAttribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors · © <a href="https://carto.com/attribution/">CARTO</a>';
|
||
map.createPane('vectorBasemap');map.getPane('vectorBasemap').style.zIndex=200;map.getPane('vectorBasemap').style.pointerEvents='none';
|
||
let activeBasemap=null,baseMode='positron',vectorMap=null;
|
||
function setBasemap(mode){
|
||
baseMode=mode;$('basemap').value=mode;
|
||
if(activeBasemap){map.removeLayer(activeBasemap);activeBasemap=null;}vectorMap=null;
|
||
$('basehint').textContent=mode==='plain'?'Plain background · embedded routes and stops':mode.startsWith('positron')?'CARTO vector · loading basemap…':'Raster basemap · loading…';
|
||
if(mode==='plain'){try{localStorage.setItem('rtc-basemap',mode)}catch{}return;}
|
||
if(mode.startsWith('positron')){
|
||
try{
|
||
const layer=L.maplibreGL({style:'https://basemaps.cartocdn.com/gl/'+mode+'-gl-style/style.json?key='+encodeURIComponent(CARTO_BASEMAP_KEY),pane:'vectorBasemap',interactive:false,attribution:cartoAttribution,attributionControl:{customAttribution:cartoAttribution}});
|
||
activeBasemap=layer;layer.addTo(map);const gl=layer.getMaplibreMap();vectorMap=gl;
|
||
gl.on('style.load',()=>{if(activeBasemap!==layer)return;gl.getStyle().layers.forEach(l=>{if(l.type==='symbol'&&(mode.endsWith('nolabels')||/poi|housenumber/i.test(l.id)||/poi/i.test(l['source-layer']||'')))gl.setLayoutProperty(l.id,'visibility','none');});});
|
||
gl.on('idle',()=>{if(activeBasemap===layer)$('basehint').textContent=mode==='positron'?'CARTO vector · street labels · POIs hidden':'CARTO vector · no labels';});
|
||
gl.on('error',()=>{if(activeBasemap===layer)$('basehint').textContent='Vector basemap could not fully load. Routes and stops remain usable; choose another basemap if needed.';});
|
||
}catch{ $('basehint').textContent='Vector rendering unavailable. Choose Esri, original streets, or plain background.';}
|
||
}else{
|
||
const esri=mode==='esri',layer=L.tileLayer(esri?'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}':'https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:20,maxNativeZoom:esri?16:19,attribution:esri?'Tiles © Esri · Esri, DeLorme, NAVTEQ':'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'});
|
||
activeBasemap=layer;layer.on('load',()=>{if(activeBasemap===layer)$('basehint').textContent=esri?'Esri Gray Canvas · raster tiles':'Original OpenStreetMap · raster tiles';});layer.on('tileerror',()=>{if(activeBasemap===layer)$('basehint').textContent='Basemap tiles unavailable. Embedded routes and stops remain usable.';});layer.addTo(map);
|
||
}
|
||
try{localStorage.setItem('rtc-basemap',mode)}catch{}
|
||
}
|
||
$('basemap').onchange=()=>setBasemap($('basemap').value);
|
||
let savedBase='positron';try{savedBase=localStorage.getItem('rtc-basemap')||savedBase}catch{}setBasemap(['positron','positron-nolabels','esri','osm','plain'].includes(savedBase)?savedBase:'positron');
|
||
L.control.scale({imperial:true,metric:true}).addTo(map);const markerGroup=L.layerGroup().addTo(map);
|
||
DATA.routes.forEach(r=>{routeLayers[r.id]=L.polyline(r.lines,{color:r.color,weight:3,opacity:.65}).addTo(map).bindTooltip(esc(r.short+' · '+r.name));routeLayers[r.id].on('click',()=>{if(r.lines.length)map.fitBounds(routeLayers[r.id].getBounds(),{padding:[30,30]});});});
|
||
function icon(p){let symbol=p.excluded?'×':p.kind==='kiosk'?'▧':p.status==='offline'?'!':'✓';return `<span class="statusicon ${p.status} ${p.kind} ${p.excluded?'excluded':''} ${p.match==='ambiguous'?'ambiguous':''}">${symbol}</span>`;}
|
||
DATA.players.filter(p=>!p.excluded).forEach(p=>p.candidate_ids.forEach(sid=>{const s=DATA.stops[sid],m=L.marker([s.lat,s.lon],{icon:L.divIcon({html:icon(p),className:'markerwrap',iconSize:[28,28],iconAnchor:[14,14]}),title:p.player+' · '+p.status+' · '+p.match,keyboard:true}).bindTooltip(esc(p.player.replace(/ - S[\d,]+$/,''))+`<br>${esc(p.status)} · ${esc(p.match)}<br>GTFS ${sid} · ${esc(s.name)}`).on('click',()=>selectPlayer(p.player_id));markerItems.push({p,sid,m});}));
|
||
function matches(p){const f=$('filter').value,q=$('search').value.toLowerCase();let yes=f==='all'||f==='field'&&!p.excluded||f==='offline'&&!p.excluded&&p.status==='offline'||f==='kiosk'&&p.kind==='kiosk'||f==='review'&&p.coordinate_review||f==='excluded'&&p.excluded;return yes&&(!q||[p.player,p.player_id,...p.candidate_ids.map(s=>s+' '+DATA.stops[s].name)].join(' ').toLowerCase().includes(q));}
|
||
function render(){const ps=DATA.players.filter(matches);$('listcount').textContent=`${ps.length} players · click a player for evidence and details`;$('players').innerHTML=ps.map(p=>`<button class="player ${selected===p.player_id?'selected':''}" data-player="${p.player_id}">${icon(p)}<span><strong>${esc(p.player.replace(/ - S[\d,]+$/,''))}</strong><small>${esc(p.source_ids.length?'S'+p.source_ids.join(', '):p.exclude_reason)}</small><small>${esc(p.status)} · ${esc(p.match)}${p.coordinate_review?' · coordinate review':''}</small></span></button>`).join('');$('players').querySelectorAll('[data-player]').forEach(b=>b.onclick=()=>selectPlayer(b.dataset.player));markerGroup.clearLayers();markerItems.filter(x=>matches(x.p)).forEach(x=>x.m.addTo(markerGroup));}
|
||
function badges(ids){return ids.map(id=>{const r=DATA.routes.find(r=>r.id===id);return `<span class="routebadge" style="--route:${r.color}">${esc(r.short)}</span>`}).join('')||'<span class="muted">No trips on selected service date</span>';}
|
||
function styleRoutes(){const p=DATA.players.find(x=>x.player_id===selected);DATA.routes.forEach(r=>{const l=routeLayers[r.id];if(enabled.has(r.id)){if(!map.hasLayer(l))l.addTo(map);const emphasized=!p||p.routes.includes(r.id);l.setStyle({color:r.color,weight:emphasized?(p?5:3):2,opacity:emphasized?.83:.13});if(emphasized)l.bringToFront();}else map.removeLayer(l);});}
|
||
function navigation(s){const coords=s.lat+','+s.lon;return `<div class="navigation"><a target="_blank" rel="noopener" href="https://maps.apple.com/?daddr=${encodeURIComponent(coords)}&dirflg=d">Navigate to this stop via Apple Maps ↗</a><a target="_blank" rel="noopener" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(coords)}&travelmode=driving">Navigate to this stop via Google Maps ↗</a></div>`;}
|
||
function stopCard(sid,c){const s=DATA.stops[sid];return `<div class="stopcard"><strong>${c?'Native S'+esc(c.native_stop_id)+' → ':''}GTFS ${sid} · ${esc(s.name)}</strong><p>GTFS ${s.lat}, ${s.lon}${c?' · native API difference '+c.coordinate_delta_m+' m':''}${c&&c.coordinate_review?' <span class="badge">Coordinate review</span>':''}</p>${badges(s.routes)}${navigation(s)}</div>`;}
|
||
function selectPlayer(id){selected=id;const p=DATA.players.find(x=>x.player_id===id);$('detail').classList.remove('hidden');$('detail').innerHTML=`<button class="close" aria-label="Close player details">×</button><h2>${esc(p.player.replace(/ - S[\d,]+$/,''))}</h2><span class="badge">${esc(p.match)}</span><dl><dt>Player ID</dt><dd>${esc(p.player_id)}</dd><dt>Status</dt><dd>${esc(p.status)}${p.detached?' · detached':''}</dd><dt>Last seen</dt><dd>${esc(p.last_seen)}</dd><dt>App / model</dt><dd>${esc(p.app_version)} · ${esc(p.model)}</dd><dt>Errors / warnings</dt><dd>${p.errors_24h} / ${p.warnings_24h} in 24h</dd><dt>Native stop IDs</dt><dd>${esc(p.source_ids.join(', ')||'None')}</dd></dl><h3>Location & route evidence</h3><p>${p.excluded?esc(p.exclude_reason)+'. No coordinates assigned.':p.kind==='kiosk'?'All listed native bay IDs are resolved through CAD/AVL SrcStopID to GTFS. The kiosk marker uses the GTFS parent station reference; its exact physical position is unknown. Choose a mapped bay below for stop navigation.':'Native GetStop SrcStopID explicitly identifies this GTFS stop. The marker and navigation links use GTFS coordinates only. OnSign location is unused. This is a bus-stop reference, not a surveyed device position.'}</p>${p.kind==='kiosk'?'<h3>Station reference</h3>'+p.candidate_ids.map(sid=>{const st=DATA.stops[sid];return `<div class="stopcard"><strong>GTFS ${sid} · ${esc(st.name)}</strong><p>Station reference only; not a kiosk GPS location.</p>${navigation(st)}</div>`;}).join('')+'<h3>Resolved bays</h3>':''}${p.crosswalk.map(c=>stopCard(c.gtfs_stop_id,c)).join('')}<h3>${p.kind==='kiosk'?'Routes serving mapped bays':'Routes serving this stop'}</h3>${badges(p.routes)}<p class="muted">Routes joined from verified stop IDs → stop_times → active trips → routes for October 28, 2026. Navigation destinations use GTFS coordinates. Scheduled associations do not establish displayed content or current stop availability.</p>`;$('detail').querySelector('.close').onclick=clearSelection;if(p.candidate_ids.length)map.fitBounds(p.candidate_ids.map(s=>[DATA.stops[s].lat,DATA.stops[s].lon]),{paddingTopLeft:[30,35],paddingBottomRight:[Math.min(400,map.getSize().x*.4),30],maxZoom:16});styleRoutes();render();}
|
||
function clearSelection(){selected=null;$('detail').classList.add('hidden');styleRoutes();render();}
|
||
function fit(){const coords=markerItems.filter(x=>matches(x.p)).map(x=>[DATA.stops[x.sid].lat,DATA.stops[x.sid].lon]);if(coords.length)map.fitBounds(coords,{padding:[36,36],maxZoom:15});}
|
||
$('search').oninput=render;$('filter').onchange=()=>{render();fit()};$('fit').onclick=fit;$('clear').onclick=clearSelection;
|
||
$('routegrid').innerHTML=DATA.routes.map(r=>`<label class="routechip" title="${esc(r.name)}"><input type="checkbox" checked value="${r.id}"><span class="swatch" style="background:${r.color}"></span>${esc(r.short)} · ${esc(r.name)}</label>`).join('');$('routegrid').querySelectorAll('input').forEach(c=>c.onchange=()=>{c.checked?enabled.add(c.value):enabled.delete(c.value);styleRoutes();});function routeToggle(all){enabled.clear();DATA.routes.forEach(r=>{if(all)enabled.add(r.id)});$('routegrid').querySelectorAll('input').forEach(c=>c.checked=all);styleRoutes();}$('routesall').onclick=()=>routeToggle(true);$('routesnone').onclick=()=>routeToggle(false);
|
||
$('report').onclick=()=>$('reportdialog').showModal();$('sources').onclick=()=>$('sourcesdialog').showModal();document.querySelectorAll('dialog .close').forEach(c=>c.onclick=()=>c.closest('dialog').close());
|
||
$('auditbody').innerHTML=DATA.audit.map(a=>`<tr><td>${esc(a.player_name.split(' - S')[0])}<br><b>S${a.native_id}</b></td><td><b>${esc(a.gtfs_stop_id)}</b> ${esc(a.gtfs_name)}<br>${esc(a.mapping_basis)}</td><td>GTFS ${a.gtfs_lat}, ${a.gtfs_lon}<br>Native API difference: ${a.coordinate_delta_m} m${a.coordinate_review?' · REVIEW':''}<br>${a.station_reference?'Kiosk station reference: '+a.station_reference:''}</td></tr>`).join('');
|
||
$('csvdownload').onclick=()=>{const keys=Object.keys(DATA.audit[0]),quote=x=>'"'+String(x??'').replace(/"/g,'""')+'"',csv=[keys,...DATA.audit.map(a=>keys.map(k=>a[k]))].map(r=>r.map(quote).join(',')).join('\r\n'),a=document.createElement('a'),url=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.href=url;a.download='resolved-stop-crosswalk.csv';a.click();setTimeout(()=>URL.revokeObjectURL(url),5000);};
|
||
$('sourcebody').innerHTML=`<p><strong>OnSign:</strong> read-only MCP snapshot. As of ${esc(DATA.meta.as_of)} (cache age ${DATA.meta.cache_age_s}s). All supplied player records are retained, including excluded records. Health is a captured snapshot and does not refresh automatically. OnSign location values are never used.</p><p><strong>Official feed:</strong> linked by <a target="_blank" href="${DATA.meta.official_page}">RTC Washoe’s Resources and Reports page</a>. <a href="${DATA.meta.feed_url}">Download publisher’s current ZIP</a>.</p><p><strong>Verified validity:</strong> feed_info September 5, 2026–January 1, 2027, version 1. HTTP Last-Modified September 8, 2026, 19:07:45 UTC. Downloaded October 28, 2026. October 28 has active weekday service after calendar_dates exceptions. 20 routes, 63 active route/shape combinations; shapes are ordered by shape_pt_sequence and colored with route_color.</p><p><strong>Native crosswalk:</strong> CAD/AVL GetStop responses retrieved ${esc(DATA.meta.native_retrieved_at_utc)}. Native StopID resolves through SrcStopID to GTFS stop_id; names are recorded for corroboration. ${DATA.meta.coordinate_reviews} native stop coordinates differ by more than 25 m; differences are separately reported. All marker and navigation destinations use unmodified GTFS coordinates. Zero unmatched or ambiguous IDs remain.</p><p><strong>Kiosks:</strong> comma-separated IDs resolve individually to station bays. Kiosk markers remain GTFS parent station references, not device coordinates. Route associations use exactly the resolved bays rather than assuming every station bay is assigned.</p><p><strong>Route evidence:</strong> resolved stop IDs → stop_times → active trips → route_id and shape_id. Associations describe GTFS service, not verified OnSign screen content. Native route IDs are not equated to GTFS route IDs. No route is inferred from a nearby line.</p><p><strong>Navigation:</strong> Apple Maps and Google Maps links request driving directions to the selected GTFS stop coordinates. The phone supplies the origin when opened. Street tiles and navigation require internet; embedded map data and controls work offline.</p><p><strong>Basemaps:</strong> CARTO Positron uses vector tiles rendered by MapLibre inside Leaflet. The street-label option hides POIs and house numbers; the no-label option hides all labels. Esri Gray Canvas and original OpenStreetMap are raster alternatives. Plain background needs no basemap connection. Your basemap choice is remembered in this browser. Basemap choice does not alter GTFS coordinates, route colors, or navigation destinations.</p><p><strong>Operating limits:</strong> scheduled geometry; no bus positions, detours, closures or arrival predictions. Native CAD/AVL credentials are excluded. The configured CARTO browser basemap key is used only for basemap requests. No OnSign write operations are included.</p><p class="muted">Feed SHA-256: ${esc(DATA.meta.sha256)}</p>`;
|
||
new ResizeObserver(()=>map.invalidateSize({pan:false})).observe($('map'));
|
||
const field=DATA.players.filter(p=>!p.excluded);$('stat-field').textContent=field.length;$('stat-health').textContent=field.filter(p=>p.status==='online').length+' / '+field.filter(p=>p.status==='offline').length;$('stat-excluded').textContent=DATA.players.filter(p=>p.excluded).length;render();fit();styleRoutes();window.RTC_MAP={data:DATA,map,selectPlayer,markerItems,routeLayers};
|
||
</script></body></html>
|