Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 0 additions & 46 deletions js/directions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,6 @@
// about buildings, and it answers null rather than throwing. Null is the
// caller's signal to keep the routed number it already has, which is the whole
// point -- the graph is the floor this sits on, not the thing it replaces.
//
// docs/research/walking-routes-115.md is why the ranking half is opt-in and not
// the default: measured against OSU's own service, Google disagreed with it more
// than the bundled graph does, so a live matrix is not obviously more accurate.
// It is here because #115's successor asks for steps, and a matrix is nearly
// free once the script is loaded.

// Google's own ceiling on destinations per DistanceMatrix request.
export const MATRIX_MAX = 25;

// Long enough for a cold script fetch on campus wifi, short enough that a
// student does not watch a spinner instead of reading the walk they already
Expand Down Expand Up @@ -149,42 +140,5 @@ export function createDirections({ key, consent = () => false, maps = loadMaps,
return null;
}
},

// Walking seconds to many buildings in one request, for ranking. Returns a
// Map of code to seconds, or null.
//
// Buildings and not rooms: 425 rooms share 50 buildings, and the ranking
// only ever needs the building. Over MATRIX_MAX destinations it returns
// null rather than paging, because a half-filled matrix would sort one list
// on two models -- the failure mode #115 names.
async matrix(origin, buildings) {
const at = generation;
const codes = Object.keys(buildings ?? {});
if (!codes.length || codes.length > MATRIX_MAX) return null;
try {
const g = await withTimeout(ready(), TIMEOUT_MS);
const service = new g.DistanceMatrixService();
const res = await withTimeout(
service.getDistanceMatrix({
origins: [{ lat: origin.lat, lng: origin.lon }],
destinations: codes.map((c) => ({ lat: buildings[c].lat, lng: buildings[c].lon })),
travelMode: 'WALKING',
}),
TIMEOUT_MS,
);
if (at !== generation) return null;
const row = res?.rows?.[0]?.elements;
if (!row || row.length !== codes.length) return null;
const out = new Map();
row.forEach((el, i) => {
if (el?.status === 'OK' && Number.isFinite(el.duration?.value)) out.set(codes[i], el.duration.value);
});
// All or nothing, for the same reason the graph's fallback is per origin
// and never per building.
return out.size === codes.length ? out : null;
} catch {
return null;
}
},
};
}
1 change: 0 additions & 1 deletion js/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export function daysBetween(fromIso, toIso) {
// which a Spring index carrying 200 rooms walks straight through, so the app
// checks per term digit instead.
const ROOM_FLOOR = { 2: 400, 4: 100, 8: 400 };
export const TERM_NAMES = { 2: 'Spring', 4: 'Summer', 8: 'Autumn' };

export function floorFor(term) {
return ROOM_FLOOR[Number(String(term ?? '').slice(-1))] ?? 150;
Expand Down
67 changes: 6 additions & 61 deletions scripts/test/directions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
// What this file is really guarding is the FALLBACK. js/directions.js exists to
// add steps the bundled graph cannot produce, and the graph stays the floor
// underneath it: every failure -- no key, no consent, offline, quota, timeout,
// a stale answer, a half-filled matrix -- has to come back as null, because
// null is what makes js/app.js keep the routed minutes it already had. A
// provider that threw, or that returned a partial matrix, would either break
// the screen or sort one list on two models, which is the failure #115 names.
// a stale answer -- has to come back as null, because null is what makes
// js/app.js keep the routed minutes it already had. A provider that threw
// would break the screen instead of falling back to it.
//
// The consent tests are the other half. This app sent no position anywhere
// before this module, and the rule is that no coordinate leaves the device
Expand All @@ -16,11 +15,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';

import { MATRIX_MAX, createDirections, plainText } from '../../js/directions.js';
import { createDirections, plainText } from '../../js/directions.js';

// A stand-in for google.maps, counting what it was asked.
const fakeMaps = ({ leg, elements, fail, hang } = {}) => {
const calls = { route: 0, matrix: 0, origins: [] };
const fakeMaps = ({ leg, fail, hang } = {}) => {
const calls = { route: 0, origins: [] };
const g = {
TravelMode: { WALKING: 'WALKING' },
DirectionsService: class {
Expand All @@ -32,15 +31,6 @@ const fakeMaps = ({ leg, elements, fail, hang } = {}) => {
return Promise.resolve({ routes: [{ legs: [leg] }] });
}
},
DistanceMatrixService: class {
getDistanceMatrix(req) {
calls.matrix += 1;
calls.origins.push(req.origins[0]);
if (fail) return Promise.reject(new Error('quota'));
if (hang) return new Promise(() => {});
return Promise.resolve({ rows: [{ elements }] });
}
},
};
return { maps: () => Promise.resolve(g), calls };
};
Expand Down Expand Up @@ -84,9 +74,7 @@ test('without consent no coordinate is sent and the answer is null', async () =>
const { maps, calls } = fakeMaps({ leg: LEG });
const d = createDirections({ key: 'k', consent: no, maps });
assert.equal(await d.steps(ORIGIN, BUILDING), null);
assert.equal(await d.matrix(ORIGIN, { 106: BUILDING }), null);
assert.equal(calls.route, 0);
assert.equal(calls.matrix, 0);
assert.deepEqual(calls.origins, []);
});

Expand Down Expand Up @@ -123,7 +111,6 @@ test('a rejected request is null rather than a throw', async () => {
const { maps } = fakeMaps({ fail: true });
const d = createDirections({ key: 'k', consent: yes, maps });
assert.equal(await d.steps(ORIGIN, BUILDING), null);
assert.equal(await d.matrix(ORIGIN, { 106: BUILDING }), null);
});

// --- the stale answer, which is the one that would corrupt a screen
Expand All @@ -145,48 +132,6 @@ test('invalidate counts up so a caller can tell generations apart', () => {
assert.equal(d.generation, 2);
});

// --- the matrix, all or nothing

test('a full row becomes seconds per building code', async () => {
const { maps } = fakeMaps({
elements: [
{ status: 'OK', duration: { value: 200 } },
{ status: 'OK', duration: { value: 310 } },
],
});
const d = createDirections({ key: 'k', consent: yes, maps });
const m = await d.matrix(ORIGIN, { 106: BUILDING, 110: BUILDING });
assert.equal(m.get('106'), 200);
assert.equal(m.get('110'), 310);
assert.equal(m.size, 2);
});

test('one unreachable destination voids the whole matrix', async () => {
// Half a matrix would rank part of one list on Google and part on the graph.
const { maps } = fakeMaps({
elements: [{ status: 'OK', duration: { value: 200 } }, { status: 'ZERO_RESULTS' }],
});
const d = createDirections({ key: 'k', consent: yes, maps });
assert.equal(await d.matrix(ORIGIN, { 106: BUILDING, 110: BUILDING }), null);
});

test('more destinations than Google allows is null and is not sent', async () => {
const { maps, calls } = fakeMaps({ elements: [] });
const d = createDirections({ key: 'k', consent: yes, maps });
const many = {};
for (let i = 0; i <= MATRIX_MAX; i += 1) many[String(i)] = BUILDING;
assert.equal(Object.keys(many).length, MATRIX_MAX + 1);
assert.equal(await d.matrix(ORIGIN, many), null);
assert.equal(calls.matrix, 0);
});

test('no buildings asks nothing', async () => {
const { maps, calls } = fakeMaps({ elements: [] });
const d = createDirections({ key: 'k', consent: yes, maps });
assert.equal(await d.matrix(ORIGIN, {}), null);
assert.equal(calls.matrix, 0);
});

// --- offline

test('offline answers null without asking', async () => {
Expand Down
2 changes: 1 addition & 1 deletion sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
// placeholder is __BUILD_ID__, and a committed sw.js still carrying it means the
// stamp did not run. scripts/test/sw.test.mjs fails on exactly that. Spelled out
// rather than built from CACHE_PREFIX, because the stamper rewrites this line.
const SHELL_CACHE = 'vacant-shell-45985c7';
const SHELL_CACHE = 'vacant-shell-fbc1218';
const DATA_CACHE = 'vacant-data-v1';

// CacheStorage is per origin, not per path, and enesyilmazcode.github.io also
Expand Down
Loading