From b002cdecd1636881f6ebd7f26b8d5e521b83ed63 Mon Sep 17 00:00:00 2001 From: Nick Walker Date: Mon, 18 Sep 2023 23:42:11 -0700 Subject: [PATCH] Add LRR segment calculator scheduling aide --- pages/light-rail-relay-23.html | 211 +++++++++++++++++++++++++++------ 1 file changed, 173 insertions(+), 38 deletions(-) diff --git a/pages/light-rail-relay-23.html b/pages/light-rail-relay-23.html index 5635c5c..1d892e4 100644 --- a/pages/light-rail-relay-23.html +++ b/pages/light-rail-relay-23.html @@ -29,6 +29,8 @@ + + @@ -313,7 +315,6 @@ .accordion { --bs-accordion-btn-focus-border-color: var(--theme-primary-color-subdued); --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem var(--theme-primary-color-subdued); - --bs-accordion-inner-border-radius: 1rem; --bs-accordion-bg: #111; --bs-accordion-active-color: var(--bs-emphasis-color); --bs-accordion-active-bg: #333; @@ -389,6 +390,43 @@ color: var(--bs-body-color); text-decoration-color: var(--theme-primary-color); } + + #leg-calculator { + border: 1px solid #111; + } + + .noUi-connect { + background-color: var(--theme-primary-color); + } + .noUi-value { + display: none; /* No tick labels */ + } + .noUi-target { + background: #333; + border-radius: 0; + border: 1px solid var(--bs-dark-border-subtle); + box-shadow: none; + } + + .noUi-handle { + box-shadow: none; + } + + .noUi-marker-large { + background-color: #555; + } + .noUi-tooltip { + width: 2rem; + height: 2rem; + border-radius: 1rem; + padding: 3px 0 0 0; + } + + .noUi-horizontal .noUi-tooltip { + bottom: 130%; + } + + .imprint { text-align: center; margin-bottom: 1.5rem; @@ -462,6 +500,15 @@
18 legs - 28.10 mi ↑2200ft ↓2300ft
visits to each station on the way. The route passes through Sea-Tac, Tukwila, and Bryn-Mawr Skyway before traversing the length of Seattle.

+ + +

@@ -483,18 +530,6 @@

-
@@ -542,7 +577,7 @@

data-bs-parent="#faq-accordion">
-

Anyone who would like to form a team is welcome. There are three team categories for results purposes: Competitive for teams of four, Ultra for teams of one, and Open for all other formats. All teams will:

+

Anyone who would like to form a team is welcome. There are three categories for results purposes: Competitive for teams of four, Ultra for teams of one, and Open for all other formats. All teams will:

  • Ensure all members are registered.
  • Have a runner present at the Angle Lake Station platform at 08:30 on the morning of the event to receive their baton.
  • @@ -551,7 +586,7 @@

  • Gather and submit station photos after the event.
-

Teams are responsible for their own logistics during the event.

+

Teams are responsible for their own logistics and support during the event.

If you're interested in forming a team, please complete the team signup. There is no cost to register. If there are more teams than we can handle, we may ask any clubs fielding multiple teams to consolidate. Once your team is confirmed, you and your runners will be able to use the main sign-up form.

@@ -866,9 +901,33 @@

return {url} } + let routeData = fetch("{{ site.baseurl }}/maps/lrr22.geojson").then(res => res.json()) + let railData = fetch("{{ site.baseurl }}/maps/link-light-rail.geojson").then(res => res.json()) + let processedRouteData = routeData.then((relay) => { + let [legs, exchanges] = processRelayGeoJSON(relay) + let exchangeNames = new Array(exchanges.length); + for (let exchange of exchanges.features) { + exchangeNames[exchange.properties.id] = exchange.properties.name + } + return [legs, exchanges, exchangeNames] + }) + + + processedRouteData.then(([legs, exchanges, exchangeNames]) => { + let legsData = legs.features + createLegDetailsTable(document.getElementById("leg-details-table"), legsData, exchanges.features) + const downloadRouteButton = document.getElementById("download-route-gpx-btn") + downloadRouteButton.addEventListener("click", event => { + let gpxString = relayToGPX("Race Condition Running Light Rail Relay 2023", "Light Rail Relay 2023", legs.features, exchanges.features, 2023) + download(gpxString, "application/gpx+xml", `light-rail-relay-2023.gpx`) + event.preventDefault() + }) + downloadRouteButton.hidden = false + }) + fetch("{{ site.baseurl }}/maps/lrr23-map-style.json").then( res => res.json()).then( style => { - var map = new maplibregl.Map({ + let map = new maplibregl.Map({ container: 'map-container', // container id attributionControl: false, style: style, @@ -910,31 +969,15 @@

resolve(); }) }), - Promise.all([ - fetch("{{ site.baseurl }}/maps/link-light-rail.geojson"), - fetch("{{ site.baseurl }}/maps/lrr22.geojson"), - ]) - .then(responses => Promise.all(responses.map((res => res.json())))) - ]).then(([_, [railLines, relay]]) => { - - let [legs, exchanges] = processRelayGeoJSON(relay) - let exchangeNames = new Array(exchanges.length); - for (let exchange of exchanges.features) { - exchangeNames[exchange.properties.id] = exchange.properties.name - } + railData, + processedRouteData + ]).then(([_, railLines, [legs, exchanges, exchangeNames]]) => { let legsData = legs.features createLegDetailsTable(document.getElementById("leg-details-table"), legsData, exchanges.features) const relayBounds = legsData.reduce((bounds, leg) => leg.geometry.coordinates.reduce((bounds, coord) => { return bounds.extend(coord); }, bounds), new maplibregl.LngLatBounds(legsData[0].geometry.coordinates[0], legsData[0].geometry.coordinates[0])); - const downloadRouteButton = document.getElementById("download-route-gpx-btn") - downloadRouteButton.addEventListener("click", event => { - let gpxString = relayToGPX("Race Condition Running Light Rail Relay 2023", "Light Rail Relay 2023", legs.features, exchanges.features, 2023) - download(gpxString, "application/gpx+xml", `light-rail-relay-2023.gpx`) - event.preventDefault() - }) - downloadRouteButton.hidden = false map.fitBounds(relayBounds, { padding: 32 }); @@ -957,7 +1000,7 @@

map.addSource('route', { 'type': 'geojson', 'data': railLines - }); + }) map.addLayer({ 'id': 'route', @@ -1138,13 +1181,105 @@

) let startTime = new Date("Sept 30, 2023 8:30:00") - fetch('{{ site.baseurl }}/maps/lrr23-schedule.json') + /*fetch('{{ site.baseurl }}/maps/lrr23-schedule.json') .then(res => res.json()) .then(schedule => { createScheduleTable("#schedule-table", schedule, startTime) - }) + })*/ createCountdown(startTime.getTime(), "countdown-ended") + + processedRouteData.then( ([legs, exchanges, exchangeNames]) => { + document.getElementById("leg-calculator").style.display = "" + let valuesSlider = document.getElementById('leg-calculator-slider'); + let cumulativeDistances = [0] + let cumulativeAscents = [0] + let cumulativeDescents = [0] + for (let i = 1; i < legs.features.length + 1; i++) { + let leg = legs.features[i - 1] + cumulativeDistances.push(cumulativeDistances[i - 1] + leg.properties.distance_mi) + cumulativeAscents.push(cumulativeAscents[i - 1] + leg.properties.ascent_ft) + cumulativeDescents.push(cumulativeDescents[i - 1] + leg.properties.descent_ft) + } + + function bisectLeft(arr, value, lo=0, hi=arr.length) { + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (arr[mid] < value) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; + } + + let formatToStationNumber = { + to: function(value) { + return bisectLeft(cumulativeDistances, value); + }, + from: function (value) { + return cumulativeDistances[Math.round(value)]; + } + }; + + let steppedRange = {} + for (let i = 0; i < cumulativeDistances.length - 1; i++) { + let percentage = cumulativeDistances[i] / cumulativeDistances[cumulativeDistances.length - 1] + steppedRange[`${percentage * 100}%`] = cumulativeDistances[i] + } + steppedRange["min"] = 0 + steppedRange["max"] = cumulativeDistances[cumulativeDistances.length - 1] + let slider = noUiSlider.create(valuesSlider, { + start: ["4", "12"], + range: steppedRange, + margin: .5, + tooltips: true, + snap: true, + connect: true, + format: formatToStationNumber, + pips: { + mode: 'range', + density: 50, + format: formatToStationNumber + } + }); + + + let leftValue = 4 + let rightValue = 12 + // The display values can be used to control the slider + slider.set(['4', '12']); + slider.on('update', function (values, handle, unencoded) { + let oldLeft = leftValue + let oldRight = rightValue + if (handle === 0) { + leftValue = values[handle] + } else { + rightValue = values[handle] + } + if (leftValue === rightValue) { + if (handle === 0) { + leftValue = oldLeft + slider.set([leftValue, rightValue]) + } else { + rightValue = oldRight + slider.set([leftValue, rightValue]) + } + return + } + let distance = cumulativeDistances[rightValue] - cumulativeDistances[leftValue] + let ascent = cumulativeAscents[rightValue] - cumulativeAscents[leftValue] + let descent = cumulativeDescents[rightValue] - cumulativeDescents[leftValue] + let legName = `${exchangeNames[leftValue]} to ${exchangeNames[rightValue]}` + // "values" has the "to" function from "format" applied + // "unencoded" contains the raw numerical slider values + let legDesc = `
${legName}
${distance.toFixed(2)}mi ↑${ascent.toFixed(0)}ft ↓${descent.toFixed(0)}ft
` + document.getElementById("leg-calculator-description").innerHTML = legDesc + }); + }) + + // Tooltips for sign-up deadlines const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))