From 4ab5e3dfafdd1ec60c034d4b7e3caaf523619258 Mon Sep 17 00:00:00 2001
From: roxburghm
- Walking distances are worked out in advance from OpenStreetMap data using GraphHopper, then looked up on your - device. They're estimates: the council uses its own address points and routes. Results near the line are - marked too close to call. + Your walking distance comes from Google Maps walking directions to the school. It's an estimate: the + council uses its own address points and routes, so results within about 50 metres of the line are marked + too close to call.
@@ -64,7 +64,7 @@Exam results from the Department for Education. Ofsted grades from reports.ofsted.gov.uk. - Walking routes © OpenStreetMap contributors, + Walking distances from Google Maps. Walking areas on the map © OpenStreetMap contributors, calculated with GraphHopper.
@@ -73,7 +73,8 @@Your address is saved only in this browser so it's there next time. Google sees what you type in the address - box to suggest matches. Nothing else is sent anywhere. + box to suggest matches, and the location you pick, to work out walking distances to schools that use them. + Nothing is sent anywhere else.
diff --git a/src/components/SchoolDetail.vue b/src/components/SchoolDetail.vue index 8f09e14..1e4d891 100644 --- a/src/components/SchoolDetail.vue +++ b/src/components/SchoolDetail.vue @@ -131,7 +131,11 @@ export default { if (s.margin === null || s.lowerBound) { return `In ${this.year} your home was ${s.kind === 'in' ? 'inside' : 'outside'} the ${m(s.cutoff)} cut-off.`; } - return `In ${this.year} your home was ${m(Math.abs(s.margin))} ${s.margin >= 0 ? 'inside' : 'outside'} the ${m(s.cutoff)} cut-off.`; + const side = `${m(Math.abs(s.margin))} ${s.margin >= 0 ? 'inside' : 'outside'}`; + if (s.measure === 'walk') { + return `In ${this.year} your ${m(s.homeMiles)} walk was ${side} the ${m(s.cutoff)} walking cut-off.`; + } + return `In ${this.year} your home was ${side} the ${m(s.cutoff)} cut-off.`; }, p8() { const v = this.school.progress8; diff --git a/src/plugins/admission.js b/src/plugins/admission.js index 34a7086..56bd7b4 100644 --- a/src/plugins/admission.js +++ b/src/plugins/admission.js @@ -36,6 +36,9 @@ export function status(school, year, home) { const walk = walking.estimate(school, home); const exact = walking.insideCutoff(school, year, home); + // An exact walk from Google: compare it directly. A near miss stays "too + // close to call" because the council's own route may differ. + if (walk?.routed) return classify(cutoff, walk, how, straight); if (exact !== null && walk) { const result = classify(cutoff, walk, how, straight); if (result.kind === 'close') result.kind = exact ? 'in' : 'out'; diff --git a/src/plugins/assess.js b/src/plugins/assess.js index ad6cb09..931a2a3 100644 --- a/src/plugins/assess.js +++ b/src/plugins/assess.js @@ -22,6 +22,7 @@ function distance(school, home) { const straight = DistanceUtils.getDistanceInMiles(home, school.centre); if (admission.measure(school) !== 'walk') return {dist: straight, label: `${formatMiles(straight)} away`}; const walk = walking.estimate(school, home); + if (walk?.routed) return {dist: walk.low, label: `${formatMiles(walk.low)} walk`}; if (walk && !walk.beyond) { const mid = (walk.low + walk.high) / 2; return {dist: mid, label: `~${formatMiles(mid)} walk`}; diff --git a/src/plugins/walking.js b/src/plugins/walking.js index dd6a0e2..d291c6a 100644 --- a/src/plugins/walking.js +++ b/src/plugins/walking.js @@ -1,14 +1,18 @@ import {reactive} from 'vue'; -// Walking distances are precomputed in CI (scripts/walking-bands.mjs) as nested -// isochrone bands around each school, so a visitor's address never leaves the -// browser. Each file in /data/walk holds: +// Walking distance from home comes from Google Maps walking directions (one +// Distance Matrix request when an address is set). Walking *areas* for the map +// come from CI (scripts/walking-bands.mjs), and its bands are a fallback for the +// distance. Each file in /data/walk holds: // bands: [{metres, polygon}] ascending; polygon = GeoJSON Polygon coordinates // cutoffs: {year: {metres, polygon}} exact outline at that year's cut-off export const METRES_PER_MILE = 1609.344; const cache = reactive({}); +// Google walking distances: `${school}|${home}` → {miles}, null if Google +// couldn't route it, undefined while the request is out. +const routes = reactive({}); export function schoolKey(school) { return school.shortName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); @@ -46,11 +50,40 @@ export function inPolygon(point, polygon) { return inRing(point, outer) && !holes.some(hole => inRing(point, hole)); } -// Walking distance from home to school as a band in miles, e.g. {low: 0.9, high: 0.96}. -// `beyond` is set when home is further than the furthest band computed. +function routeKey(school, home) { + return `${schoolKey(school)}|${home.lat.toFixed(5)},${home.lng.toFixed(5)}`; +} + +// Ask Google for the walk from home to each school not already looked up for this home. +export function lookupWalks(schools, home) { + const google = window.google; + if (!home || !google?.maps?.DistanceMatrixService) return; + const todo = schools.filter(school => !(routeKey(school, home) in routes)); + if (!todo.length) return; + todo.forEach(school => routes[routeKey(school, home)] = undefined); + new google.maps.DistanceMatrixService().getDistanceMatrix({ + origins: [home], + destinations: todo.map(school => school.centre), + travelMode: google.maps.TravelMode.WALKING, + }, (response, status) => { + todo.forEach((school, i) => { + const element = status === 'OK' ? response.rows[0]?.elements[i] : null; + routes[routeKey(school, home)] = element?.status === 'OK' + ? {miles: element.distance.value / METRES_PER_MILE} + : null; + }); + }); +} + +// Walking distance from home to school as a band in miles. From Google it's +// exact ({low: 1.2, high: 1.2, routed: true}); from CI bands it's a range, with +// `beyond` set when home is further than the furthest band computed. export function estimate(school, home) { + if (!home) return null; + const route = routes[routeKey(school, home)]; + if (route) return {low: route.miles, high: route.miles, routed: true}; const walk = data(school); - if (!walk || !home) return null; + if (!walk) return null; let low = 0; for (const band of walk.bands) { if (inPolygon(home, band.polygon)) { @@ -74,4 +107,4 @@ export function cutoffOutline(school, year) { return data(school)?.cutoffs?.[year]?.polygon?.[0] || null; } -export default {schoolKey, load, data, estimate, insideCutoff, cutoffOutline, inPolygon, METRES_PER_MILE}; +export default {schoolKey, load, data, lookupWalks, estimate, insideCutoff, cutoffOutline, inPolygon, METRES_PER_MILE}; From e0b9b05b62849140c98b0b8f91f673a7d7c98992 Mon Sep 17 00:00:00 2001 From: roxburghm- Figures are approximate and for guidance only. Problems or corrections: + Every figure here is an estimate, for guidance only. Ealing Council’s admissions decisions are final. + Problems or corrections: matt@roxburghm.com
diff --git a/src/components/IntakeChart.vue b/src/components/IntakeChart.vue index bd171d0..f3afdc6 100644 --- a/src/components/IntakeChart.vue +++ b/src/components/IntakeChart.vue @@ -25,7 +25,7 @@ :rx="compact ? 1 : 3" :fill="`url(#${patternId})`" class="all-bar"/> -{{ yearLine }}
++ + Estimates only. These figures are worked out from past offers and our own distance + measurements. Ealing Council’s admissions decisions are final. +
+Note: {{ school.notes }}
{{ finePrint }}
- {{ sourceLine }} Issues: matt@roxburghm.com + {{ sourceLine }} Questions or corrections: matt@roxburghm.com · Build {{ buildDateTime }}
@@ -115,7 +115,7 @@{{ finePrint }}
- {{ sourceLine }} Issues: matt@roxburghm.com + {{ sourceLine }} Questions or corrections: matt@roxburghm.com · Build {{ buildDateTime }}