mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
style: format terrain traffic fire and bike-share modules
This commit is contained in:
1 parent
f4054469d5
commit
a2d6461b52
13 files changed
+638
-213
No files matched your search
+30
-7
@@ -17,7 +17,14 @@
|
||||
*/
|
||||
|
||||
/** Header fields that must all be present for a payload to count as FIRMS CSV. */
|
||||
const REQUIRED_HEADER_FIELDS = ['latitude', 'longitude', 'acq_date', 'acq_time', 'confidence', 'frp'];
|
||||
const REQUIRED_HEADER_FIELDS = [
|
||||
'latitude',
|
||||
'longitude',
|
||||
'acq_date',
|
||||
'acq_time',
|
||||
'confidence',
|
||||
'frp',
|
||||
];
|
||||
|
||||
const HOUR_MS = 3600_000;
|
||||
/** Trailing window size for {@link filterTrailing24h}. */
|
||||
@@ -36,8 +43,10 @@ export function isLikelyCsv(text) {
|
||||
if (typeof text !== 'string') return false;
|
||||
const trimmed = text.trimStart();
|
||||
if (!trimmed || trimmed[0] === '<') return false;
|
||||
const headerLine = trimmed.slice(0, trimmed.indexOf('\n') === -1 ? undefined : trimmed.indexOf('\n'))
|
||||
.trim().toLowerCase();
|
||||
const headerLine = trimmed
|
||||
.slice(0, trimmed.indexOf('\n') === -1 ? undefined : trimmed.indexOf('\n'))
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const fields = headerLine.split(',').map((f) => f.trim());
|
||||
return REQUIRED_HEADER_FIELDS.every((required) => fields.includes(required));
|
||||
}
|
||||
@@ -62,8 +71,13 @@ export function parseFirmsCsv(text) {
|
||||
// Locate the header (first non-empty line) and build a column index so the
|
||||
// parser survives column reordering across FIRMS product versions.
|
||||
let headerIndex = 0;
|
||||
while (headerIndex < lines.length && !lines[headerIndex].trim()) headerIndex += 1;
|
||||
const header = lines[headerIndex].trim().toLowerCase().split(',').map((f) => f.trim());
|
||||
while (headerIndex < lines.length && !lines[headerIndex].trim())
|
||||
headerIndex += 1;
|
||||
const header = lines[headerIndex]
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(',')
|
||||
.map((f) => f.trim());
|
||||
const col = new Map(header.map((name, i) => [name, i]));
|
||||
const iLat = col.get('latitude');
|
||||
const iLon = col.get('longitude');
|
||||
@@ -114,7 +128,8 @@ export function parseFirmsCsv(text) {
|
||||
* @returns {number} Epoch ms, or NaN when unparseable.
|
||||
*/
|
||||
export function acquisitionMsUtc(acqDate, acqTime) {
|
||||
if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate)) return NaN;
|
||||
if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate))
|
||||
return NaN;
|
||||
const timeText = String(acqTime ?? '').trim();
|
||||
if (!/^\d{1,4}$/.test(timeText)) return NaN;
|
||||
const hhmm = timeText.padStart(4, '0');
|
||||
@@ -123,7 +138,15 @@ export function acquisitionMsUtc(acqDate, acqTime) {
|
||||
const day = Number(acqDate.slice(8, 10));
|
||||
const hours = Number(hhmm.slice(0, 2));
|
||||
const minutes = Number(hhmm.slice(2, 4));
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31 || hours > 23 || minutes > 59) return NaN;
|
||||
if (
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
day < 1 ||
|
||||
day > 31 ||
|
||||
hours > 23 ||
|
||||
minutes > 59
|
||||
)
|
||||
return NaN;
|
||||
return Date.UTC(year, month - 1, day, hours, minutes);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURE = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'firms-viirs-noaa20-sample.csv'),
|
||||
'utf8'
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const HEADER = 'latitude,longitude,bright_ti4,scan,track,acq_date,acq_time,satellite,instrument,confidence,version,bright_ti5,frp,daynight';
|
||||
const HEADER =
|
||||
'latitude,longitude,bright_ti4,scan,track,acq_date,acq_time,satellite,instrument,confidence,version,bright_ti5,frp,daynight';
|
||||
|
||||
test('fixture parses to 45 records with finite lat/lon/frp', () => {
|
||||
const records = parseFirmsCsv(FIXTURE);
|
||||
@@ -48,15 +49,24 @@ test('fixture first row maps every field, categorical confidence preserved raw',
|
||||
});
|
||||
|
||||
test('acquisitionMsUtc: "1006" = 10:06 UTC', () => {
|
||||
assert.equal(acquisitionMsUtc('2026-07-16', '1006'), Date.UTC(2026, 6, 16, 10, 6));
|
||||
assert.equal(
|
||||
acquisitionMsUtc('2026-07-16', '1006'),
|
||||
Date.UTC(2026, 6, 16, 10, 6),
|
||||
);
|
||||
});
|
||||
|
||||
test('acquisitionMsUtc: non-zero-padded "45" = 00:45 UTC', () => {
|
||||
assert.equal(acquisitionMsUtc('2026-07-16', '45'), Date.UTC(2026, 6, 16, 0, 45));
|
||||
assert.equal(
|
||||
acquisitionMsUtc('2026-07-16', '45'),
|
||||
Date.UTC(2026, 6, 16, 0, 45),
|
||||
);
|
||||
});
|
||||
|
||||
test('acquisitionMsUtc: "0" = midnight UTC; garbage → NaN', () => {
|
||||
assert.equal(acquisitionMsUtc('2026-07-16', '0'), Date.UTC(2026, 6, 16, 0, 0));
|
||||
assert.equal(
|
||||
acquisitionMsUtc('2026-07-16', '0'),
|
||||
Date.UTC(2026, 6, 16, 0, 0),
|
||||
);
|
||||
assert.ok(Number.isNaN(acquisitionMsUtc('not-a-date', '1006')));
|
||||
assert.ok(Number.isNaN(acquisitionMsUtc('2026-07-16', 'xx')));
|
||||
});
|
||||
@@ -118,7 +128,12 @@ test('filterTrailing24h: window is [now − 24 h, now + 2 h] inclusive', () => {
|
||||
const kept = filterTrailing24h(records, now);
|
||||
assert.deepEqual(
|
||||
kept.map((r) => `${r.acqDate} ${r.acqTime}`),
|
||||
['2026-07-15 1200', '2026-07-15 1201', '2026-07-16 1130', '2026-07-16 1400']
|
||||
[
|
||||
'2026-07-15 1200',
|
||||
'2026-07-15 1201',
|
||||
'2026-07-16 1130',
|
||||
'2026-07-16 1400',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ const GBFS_ALLOWED_HOSTS = new Set([
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAllowedGbfsHost(hostname) {
|
||||
const host = String(hostname || '').trim().toLowerCase();
|
||||
const host = String(hostname || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!host) return false;
|
||||
if (GBFS_ALLOWED_HOSTS.has(host)) return true;
|
||||
return host.endsWith('.publicbikesystem.net');
|
||||
|
||||
@@ -24,7 +24,10 @@ function defaultSleep(ms) {
|
||||
export function parseTerrainPoints(raw) {
|
||||
const text = String(raw || '').trim();
|
||||
if (!text) return null;
|
||||
const pairs = text.split(';').map((part) => part.trim()).filter(Boolean);
|
||||
const pairs = text
|
||||
.split(';')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
if (pairs.length === 0) return null;
|
||||
const points = [];
|
||||
for (const pair of pairs) {
|
||||
@@ -83,16 +86,19 @@ export function terrainRetryAfterMs(value, nowMs = Date.now()) {
|
||||
* @param {object} [options]
|
||||
* @returns {Promise<Array<object>>}
|
||||
*/
|
||||
export async function fetchTerrainChunkWithRetry(points, {
|
||||
fetchImpl = globalThis.fetch,
|
||||
sleep = defaultSleep,
|
||||
random = Math.random,
|
||||
now = Date.now,
|
||||
makeSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs),
|
||||
attemptTimeoutMs = 30_000,
|
||||
retryBudgetMs = TERRAIN_RETRY_BUDGET_MS,
|
||||
maxAttempts = TERRAIN_MAX_ATTEMPTS,
|
||||
} = {}) {
|
||||
export async function fetchTerrainChunkWithRetry(
|
||||
points,
|
||||
{
|
||||
fetchImpl = globalThis.fetch,
|
||||
sleep = defaultSleep,
|
||||
random = Math.random,
|
||||
now = Date.now,
|
||||
makeSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs),
|
||||
attemptTimeoutMs = 30_000,
|
||||
retryBudgetMs = TERRAIN_RETRY_BUDGET_MS,
|
||||
maxAttempts = TERRAIN_MAX_ATTEMPTS,
|
||||
} = {},
|
||||
) {
|
||||
const pointsParam = points.map(terrainPointKey).join(';');
|
||||
const url = `https://terrain.reearth.land/heights.json?points=${encodeURIComponent(pointsParam)}`;
|
||||
let retryStartedAt = null;
|
||||
@@ -117,7 +123,9 @@ export async function fetchTerrainChunkWithRetry(points, {
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!Array.isArray(json?.results)) {
|
||||
const error = new Error('malformed upstream response (no results array)');
|
||||
const error = new Error(
|
||||
'malformed upstream response (no results array)',
|
||||
);
|
||||
error.retryable = false;
|
||||
throw error;
|
||||
}
|
||||
@@ -131,14 +139,18 @@ export async function fetchTerrainChunkWithRetry(points, {
|
||||
if (remaining <= 0) break;
|
||||
|
||||
const retryAfterMs = terrainRetryAfterMs(error?.retryAfter, now());
|
||||
const backoffMs = Math.round(350 * (2 ** attempt) * (0.75 + random() * 0.5));
|
||||
const backoffMs = Math.round(
|
||||
350 * 2 ** attempt * (0.75 + random() * 0.5),
|
||||
);
|
||||
const requestedDelay = retryAfterMs ?? backoffMs;
|
||||
const delayMs = Math.min(requestedDelay, remaining);
|
||||
if (delayMs > 0) await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('terrain heights upstream retry budget exhausted');
|
||||
throw (
|
||||
lastError || new Error('terrain heights upstream retry budget exhausted')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +183,12 @@ export async function resolveTerrainHeightRequest({
|
||||
const missing = [];
|
||||
for (const [key, point] of unique) {
|
||||
const entry = cache.get(key);
|
||||
if (entry && validTerrainResult(entry.result) && requestStartedAt - entry.at < ttlMs) continue;
|
||||
if (
|
||||
entry &&
|
||||
validTerrainResult(entry.result) &&
|
||||
requestStartedAt - entry.at < ttlMs
|
||||
)
|
||||
continue;
|
||||
missing.push({ key, point });
|
||||
}
|
||||
|
||||
@@ -180,7 +197,8 @@ export async function resolveTerrainHeightRequest({
|
||||
if (missing.length > 0) {
|
||||
try {
|
||||
const fetched = await fetchMissing(missing.map((item) => item.point));
|
||||
if (!Array.isArray(fetched)) throw new Error('malformed upstream response (no results array)');
|
||||
if (!Array.isArray(fetched))
|
||||
throw new Error('malformed upstream response (no results array)');
|
||||
const fetchedAt = now();
|
||||
for (let i = 0; i < missing.length; i += 1) {
|
||||
const result = fetched[i];
|
||||
@@ -188,8 +206,13 @@ export async function resolveTerrainHeightRequest({
|
||||
cache.set(missing[i].key, { at: fetchedAt, result });
|
||||
cacheChanged = true;
|
||||
}
|
||||
if (fetched.length !== missing.length || missing.some((_, i) => !validTerrainResult(fetched[i]))) {
|
||||
upstreamError = new Error('upstream omitted one or more terrain heights');
|
||||
if (
|
||||
fetched.length !== missing.length ||
|
||||
missing.some((_, i) => !validTerrainResult(fetched[i]))
|
||||
) {
|
||||
upstreamError = new Error(
|
||||
'upstream omitted one or more terrain heights',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
upstreamError = error;
|
||||
@@ -203,7 +226,10 @@ export async function resolveTerrainHeightRequest({
|
||||
if (results.some((result) => result == null)) {
|
||||
return {
|
||||
status: 502,
|
||||
body: { error: 'terrain heights fetch failed and no cache available for every point' },
|
||||
body: {
|
||||
error:
|
||||
'terrain heights fetch failed and no cache available for every point',
|
||||
},
|
||||
cacheChanged,
|
||||
upstreamError,
|
||||
};
|
||||
|
||||
@@ -18,7 +18,13 @@ test('per-point cache reconstruction preserves exact request order with mixed hi
|
||||
[terrainPointKey([10, 1]), { at: now, result: result('cached-a', 101) }],
|
||||
[terrainPointKey([30, 3]), { at: now, result: result('cached-c', 303) }],
|
||||
]);
|
||||
const points = [[30, 3], [20, 2], [10, 1], [40, 4], [20, 2]];
|
||||
const points = [
|
||||
[30, 3],
|
||||
[20, 2],
|
||||
[10, 1],
|
||||
[40, 4],
|
||||
[20, 2],
|
||||
];
|
||||
let fetchedPoints = null;
|
||||
|
||||
const response = await resolveTerrainHeightRequest({
|
||||
@@ -33,11 +39,18 @@ test('per-point cache reconstruction preserves exact request order with mixed hi
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(fetchedPoints, [[20, 2], [40, 4]], 'only unique cache misses go upstream');
|
||||
assert.deepEqual(
|
||||
fetchedPoints,
|
||||
[
|
||||
[20, 2],
|
||||
[40, 4],
|
||||
],
|
||||
'only unique cache misses go upstream',
|
||||
);
|
||||
assert.deepEqual(
|
||||
response.body.results.map((item) => item.id),
|
||||
['cached-c', 'fresh-b', 'cached-a', 'fresh-d', 'fresh-b'],
|
||||
'each output position must map back to its own input point'
|
||||
'each output position must map back to its own input point',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,7 +72,9 @@ test('429 retry honors Retry-After before the next attempt', async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 429,
|
||||
headers: { get: (name) => name.toLowerCase() === 'retry-after' ? '3' : null },
|
||||
headers: {
|
||||
get: (name) => (name.toLowerCase() === 'retry-after' ? '3' : null),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -83,21 +98,28 @@ test('permanently failing upstream exhausts retries and returns 502 without fake
|
||||
cache: new Map(),
|
||||
ttlMs: 10_000,
|
||||
now: () => clock,
|
||||
fetchMissing: (missing) => fetchTerrainChunkWithRetry(missing, {
|
||||
now: () => clock,
|
||||
random: () => 0.5,
|
||||
makeSignal: () => undefined,
|
||||
sleep: async (ms) => { clock += ms; },
|
||||
fetchImpl: async () => {
|
||||
attempts += 1;
|
||||
return { ok: false, status: 503, headers: { get: () => null } };
|
||||
},
|
||||
}),
|
||||
fetchMissing: (missing) =>
|
||||
fetchTerrainChunkWithRetry(missing, {
|
||||
now: () => clock,
|
||||
random: () => 0.5,
|
||||
makeSignal: () => undefined,
|
||||
sleep: async (ms) => {
|
||||
clock += ms;
|
||||
},
|
||||
fetchImpl: async () => {
|
||||
attempts += 1;
|
||||
return { ok: false, status: 503, headers: { get: () => null } };
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(attempts, 4, 'one initial attempt plus three retries');
|
||||
assert.equal(response.status, 502);
|
||||
assert.equal('results' in response.body, false, 'failure body must not fabricate positional heights');
|
||||
assert.equal(
|
||||
'results' in response.body,
|
||||
false,
|
||||
'failure body must not fabricate positional heights',
|
||||
);
|
||||
});
|
||||
|
||||
test('stale per-point entries serve through a failed refresh only when every point is real', async () => {
|
||||
@@ -106,12 +128,20 @@ test('stale per-point entries serve through a failed refresh only when every poi
|
||||
[terrainPointKey([2, 2]), { at: 0, result: result('stale-b', 22) }],
|
||||
]);
|
||||
const response = await resolveTerrainHeightRequest({
|
||||
points: [[2, 2], [1, 1]],
|
||||
points: [
|
||||
[2, 2],
|
||||
[1, 1],
|
||||
],
|
||||
cache,
|
||||
ttlMs: 100,
|
||||
now: () => 1000,
|
||||
fetchMissing: async () => { throw new Error('offline'); },
|
||||
fetchMissing: async () => {
|
||||
throw new Error('offline');
|
||||
},
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(response.body.results.map((item) => item.id), ['stale-b', 'stale-a']);
|
||||
assert.deepEqual(
|
||||
response.body.results.map((item) => item.id),
|
||||
['stale-b', 'stale-a'],
|
||||
);
|
||||
});
|
||||
+14
-8
@@ -29,7 +29,8 @@ const MERCATOR_LAT_LIMIT = 85.05112878;
|
||||
* @returns {boolean} True when the coordinate is a fetchable tile.
|
||||
*/
|
||||
export function isValidTileCoord(z, x, y) {
|
||||
if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y)) return false;
|
||||
if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y))
|
||||
return false;
|
||||
if (z < MIN_TILE_ZOOM || z > MAX_TILE_ZOOM) return false;
|
||||
const n = 2 ** z;
|
||||
return x >= 0 && x < n && y >= 0 && y < n;
|
||||
@@ -47,11 +48,14 @@ export function isValidTileCoord(z, x, y) {
|
||||
*/
|
||||
export function lonLatToTile(lon, lat, z) {
|
||||
const n = 2 ** z;
|
||||
const clampedLat = Math.max(-MERCATOR_LAT_LIMIT, Math.min(MERCATOR_LAT_LIMIT, lat));
|
||||
const clampedLat = Math.max(
|
||||
-MERCATOR_LAT_LIMIT,
|
||||
Math.min(MERCATOR_LAT_LIMIT, lat),
|
||||
);
|
||||
const latRad = (clampedLat * Math.PI) / 180;
|
||||
const x = Math.floor(((lon + 180) / 360) * n);
|
||||
const y = Math.floor(
|
||||
((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n
|
||||
((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n,
|
||||
);
|
||||
return {
|
||||
x: Math.max(0, Math.min(n - 1, x)),
|
||||
@@ -70,7 +74,8 @@ export function lonLatToTile(lon, lat, z) {
|
||||
export function tileToBBox(z, x, y) {
|
||||
const n = 2 ** z;
|
||||
const lonAt = (col) => (col / n) * 360 - 180;
|
||||
const latAt = (row) => (Math.atan(Math.sinh(Math.PI * (1 - (2 * row) / n))) * 180) / Math.PI;
|
||||
const latAt = (row) =>
|
||||
(Math.atan(Math.sinh(Math.PI * (1 - (2 * row) / n))) * 180) / Math.PI;
|
||||
return {
|
||||
west: lonAt(x),
|
||||
east: lonAt(x + 1),
|
||||
@@ -132,10 +137,11 @@ export function utcDayKey(epochMs = Date.now()) {
|
||||
* @returns {{date:string, count:number}} Valid state for `dayKey`.
|
||||
*/
|
||||
export function normalizeBudget(state, dayKey) {
|
||||
const valid = Boolean(state)
|
||||
&& state.date === dayKey
|
||||
&& Number.isFinite(state.count)
|
||||
&& state.count >= 0;
|
||||
const valid =
|
||||
Boolean(state) &&
|
||||
state.date === dayKey &&
|
||||
Number.isFinite(state.count) &&
|
||||
state.count >= 0;
|
||||
return valid ? state : { date: dayKey, count: 0 };
|
||||
}
|
||||
|
||||
|
||||
@@ -53,22 +53,39 @@ test('lonLatToTile: downtown Austin @ z12 -> x935 y1686 (matches fixture)', () =
|
||||
|
||||
test('tileToBBox: fixture tile bbox contains downtown Austin', () => {
|
||||
const bbox = tileToBBox(12, 935, 1686);
|
||||
assert.ok(bbox.west <= AUSTIN.lon && AUSTIN.lon <= bbox.east, `lon outside [${bbox.west}, ${bbox.east}]`);
|
||||
assert.ok(bbox.south <= AUSTIN.lat && AUSTIN.lat <= bbox.north, `lat outside [${bbox.south}, ${bbox.north}]`);
|
||||
assert.ok(
|
||||
bbox.west <= AUSTIN.lon && AUSTIN.lon <= bbox.east,
|
||||
`lon outside [${bbox.west}, ${bbox.east}]`,
|
||||
);
|
||||
assert.ok(
|
||||
bbox.south <= AUSTIN.lat && AUSTIN.lat <= bbox.north,
|
||||
`lat outside [${bbox.south}, ${bbox.north}]`,
|
||||
);
|
||||
assert.ok(bbox.north > bbox.south && bbox.east > bbox.west);
|
||||
});
|
||||
|
||||
test('tileToBBox <-> lonLatToTile roundtrip at the bbox center', () => {
|
||||
const bbox = tileToBBox(12, 935, 1686);
|
||||
const center = { lon: (bbox.west + bbox.east) / 2, lat: (bbox.south + bbox.north) / 2 };
|
||||
const center = {
|
||||
lon: (bbox.west + bbox.east) / 2,
|
||||
lat: (bbox.south + bbox.north) / 2,
|
||||
};
|
||||
const t = lonLatToTile(center.lon, center.lat, 12);
|
||||
assert.deepEqual({ x: t.x, y: t.y }, { x: 935, y: 1686 });
|
||||
});
|
||||
|
||||
test('lonLatToTile clamps poles and antimeridian into valid range', () => {
|
||||
for (const [lon, lat] of [[-180, 89.9], [180, -89.9], [179.9999, 0]]) {
|
||||
for (const [lon, lat] of [
|
||||
[-180, 89.9],
|
||||
[180, -89.9],
|
||||
[179.9999, 0],
|
||||
]) {
|
||||
const t = lonLatToTile(lon, lat, 12);
|
||||
assert.equal(isValidTileCoord(12, t.x, t.y), true, `invalid tile for ${lon},${lat}: ${t.x},${t.y}`);
|
||||
assert.equal(
|
||||
isValidTileCoord(12, t.x, t.y),
|
||||
true,
|
||||
`invalid tile for ${lon},${lat}: ${t.x},${t.y}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,14 +93,19 @@ test('lonLatToTile clamps poles and antimeridian into valid range', () => {
|
||||
|
||||
test('tilesForBounds: clamped Austin viewport covers the fixture tile', () => {
|
||||
const bounds = {
|
||||
south: AUSTIN.lat - 0.025, north: AUSTIN.lat + 0.025,
|
||||
west: AUSTIN.lon - 0.025, east: AUSTIN.lon + 0.025,
|
||||
south: AUSTIN.lat - 0.025,
|
||||
north: AUSTIN.lat + 0.025,
|
||||
west: AUSTIN.lon - 0.025,
|
||||
east: AUSTIN.lon + 0.025,
|
||||
};
|
||||
const tiles = tilesForBounds(bounds, 12);
|
||||
assert.ok(tiles.length >= 1 && tiles.length <= 4, `tile count ${tiles.length}`);
|
||||
assert.ok(
|
||||
tiles.length >= 1 && tiles.length <= 4,
|
||||
`tile count ${tiles.length}`,
|
||||
);
|
||||
assert.ok(
|
||||
tiles.some((t) => t.z === 12 && t.x === 935 && t.y === 1686),
|
||||
`fixture tile missing from ${JSON.stringify(tiles)}`
|
||||
`fixture tile missing from ${JSON.stringify(tiles)}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,7 +124,7 @@ test('tilesForBounds: bounds straddling a tile edge return both tiles', () => {
|
||||
});
|
||||
|
||||
test('tilesForBounds: default zoom is 12 and every tile is valid', () => {
|
||||
const bounds = { south: 51.49, north: 51.52, west: -0.14, east: -0.10 }; // London
|
||||
const bounds = { south: 51.49, north: 51.52, west: -0.14, east: -0.1 }; // London
|
||||
const tiles = tilesForBounds(bounds);
|
||||
assert.ok(tiles.length >= 1);
|
||||
for (const t of tiles) {
|
||||
@@ -112,7 +134,11 @@ test('tilesForBounds: default zoom is 12 and every tile is valid', () => {
|
||||
});
|
||||
|
||||
test('tilesForBounds: runaway bounds are truncated by the safety cap', () => {
|
||||
const tiles = tilesForBounds({ south: -60, north: 60, west: -170, east: 170 }, 12, { maxTiles: 16 });
|
||||
const tiles = tilesForBounds(
|
||||
{ south: -60, north: 60, west: -170, east: 170 },
|
||||
12,
|
||||
{ maxTiles: 16 },
|
||||
);
|
||||
assert.ok(tiles.length <= 16, `got ${tiles.length}`);
|
||||
});
|
||||
|
||||
@@ -129,15 +155,30 @@ test('normalizeBudget: same-day state passes through untouched', () => {
|
||||
});
|
||||
|
||||
test('normalizeBudget: day rollover resets the counter', () => {
|
||||
const rolled = normalizeBudget({ date: '2026-07-16', count: 39999 }, '2026-07-17');
|
||||
const rolled = normalizeBudget(
|
||||
{ date: '2026-07-16', count: 39999 },
|
||||
'2026-07-17',
|
||||
);
|
||||
assert.deepEqual(rolled, { date: '2026-07-17', count: 0 });
|
||||
});
|
||||
|
||||
test('normalizeBudget: missing/corrupt state starts fresh', () => {
|
||||
assert.deepEqual(normalizeBudget(null, '2026-07-16'), { date: '2026-07-16', count: 0 });
|
||||
assert.deepEqual(normalizeBudget({ date: '2026-07-16', count: NaN }, '2026-07-16'), { date: '2026-07-16', count: 0 });
|
||||
assert.deepEqual(normalizeBudget({ count: 5 }, '2026-07-16'), { date: '2026-07-16', count: 0 });
|
||||
assert.deepEqual(normalizeBudget({ date: '2026-07-16', count: -3 }, '2026-07-16'), { date: '2026-07-16', count: 0 });
|
||||
assert.deepEqual(normalizeBudget(null, '2026-07-16'), {
|
||||
date: '2026-07-16',
|
||||
count: 0,
|
||||
});
|
||||
assert.deepEqual(
|
||||
normalizeBudget({ date: '2026-07-16', count: NaN }, '2026-07-16'),
|
||||
{ date: '2026-07-16', count: 0 },
|
||||
);
|
||||
assert.deepEqual(normalizeBudget({ count: 5 }, '2026-07-16'), {
|
||||
date: '2026-07-16',
|
||||
count: 0,
|
||||
});
|
||||
assert.deepEqual(
|
||||
normalizeBudget({ date: '2026-07-16', count: -3 }, '2026-07-16'),
|
||||
{ date: '2026-07-16', count: 0 },
|
||||
);
|
||||
});
|
||||
|
||||
test('isOverBudget: at or above the limit is over, below is not', () => {
|
||||
|
||||
Reference in new issue
Block a user