mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge remote-tracking branch 'origin/main' into upstream/geocode-place-region
# Conflicts: # docs/CURRENT-STATE.md # src/keylessGeocoder.test.mjs
This commit is contained in:
commit
c13c2fc1f0
16 files changed
+300
-14
No files matched your search
+20
-5
@@ -206,12 +206,27 @@ export function photonSearchUrl(
|
||||
url.searchParams.set('limit', String(limit));
|
||||
|
||||
const corners = String(bias ?? '').split('|');
|
||||
if (corners.length === 2) {
|
||||
const [swLat, swLng] = corners[0].split(',').map(Number);
|
||||
const [neLat, neLng] = corners[1].split(',').map(Number);
|
||||
if ([swLat, swLng, neLat, neLng].every(Number.isFinite)) {
|
||||
const tokens = corners.flatMap((corner) => corner.split(','));
|
||||
if (
|
||||
corners.length === 2 &&
|
||||
corners.every((corner) => corner.split(',').length === 2) &&
|
||||
tokens.every((token) => token.trim() !== '')
|
||||
) {
|
||||
const [swLat, swLng, neLat, neLng] = tokens.map(Number);
|
||||
if (
|
||||
[swLat, swLng, neLat, neLng].every(Number.isFinite) &&
|
||||
Math.abs(swLat) <= 90 &&
|
||||
Math.abs(neLat) <= 90 &&
|
||||
swLat <= neLat &&
|
||||
Math.abs(swLng) <= 180 &&
|
||||
Math.abs(neLng) <= 180
|
||||
) {
|
||||
const midpoint = (swLng + neLng + (swLng > neLng ? 360 : 0)) / 2;
|
||||
url.searchParams.set('lat', String((swLat + neLat) / 2));
|
||||
url.searchParams.set('lon', String((swLng + neLng) / 2));
|
||||
url.searchParams.set(
|
||||
'lon',
|
||||
String(midpoint > 180 ? midpoint - 360 : midpoint),
|
||||
);
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
|
||||
@@ -583,3 +583,14 @@ test('a nearby place whose own name has a comma still wins when nothing matches
|
||||
assert.match(result.label, /Denver/);
|
||||
assert.equal(fetchImpl.urls.length, 2, 'the region checks ran first');
|
||||
});
|
||||
|
||||
|
||||
test('wrapped view bias stays near the antimeridian and malformed coordinates are omitted', () => {
|
||||
for (const [bias, expected] of [['-20,170|-10,-171', 179.5], ['50,175|60,-165', -175], ['0,180|10,-180', 180], ['0,-180|10,180', 0]]) {
|
||||
const params = new URL(photonSearchUrl('island', { bias })).searchParams;
|
||||
assert.equal(Number(params.get('lon')), expected);
|
||||
}
|
||||
for (const bias of ['-91,0|0,1', '0,181|1,0', '10,0|0,1', ',|,', '0,1,2|3,4', '0,1|Infinity,2']) {
|
||||
assert.equal(new URL(photonSearchUrl('island', { bias })).searchParams.has('lon'), false);
|
||||
}
|
||||
});
|
||||
@@ -166,6 +166,8 @@ export function nominatimViewboxFromBounds(bounds) {
|
||||
const neLat = nominatimLatitude(match[3]);
|
||||
const neLng = nominatimLongitude(match[4]);
|
||||
if ([swLat, swLng, neLat, neLng].some((value) => value === null)) return null;
|
||||
if (swLat >= neLat) return null;
|
||||
// One Nominatim viewbox cannot express a wrapped interval. Omit this soft
|
||||
// preference rather than biasing the search toward the opposite hemisphere.
|
||||
if (swLat >= neLat || swLng > neLng) return null;
|
||||
return `${swLng},${neLat},${neLng},${swLat}`;
|
||||
}
|
||||
@@ -173,3 +173,12 @@ test('a viewport hint is sent only when every edge is a real coordinate', () =>
|
||||
`${JSON.stringify(bounds)} must not become a viewbox`,
|
||||
);
|
||||
});
|
||||
|
||||
test('wrapped viewports omit the Nominatim bias without rejecting the search', () => {
|
||||
assert.equal(nominatimViewboxFromBounds('-20,170|-10,-171'), null);
|
||||
assert.equal(nominatimViewboxFromBounds('50,175|60,-165'), null);
|
||||
assert.equal(
|
||||
nominatimViewboxFromBounds('-20,170|-10,180'),
|
||||
'170,-10,180,-20',
|
||||
);
|
||||
});
|
||||
@@ -307,10 +307,20 @@ export function createGroundSnap({ groundFloor }) {
|
||||
}
|
||||
const now = Date.now();
|
||||
const entry = entries.get(icao);
|
||||
if (entry && now < entry.nextRetryMs) return heldSnapM(entry, surfacePos); // backoff in force
|
||||
// Backoff in force, unless it ends further off than any backoff can:
|
||||
// a retry stamped before the clock stepped back would otherwise wait for
|
||||
// the clock to catch up.
|
||||
if (
|
||||
entry &&
|
||||
now < entry.nextRetryMs &&
|
||||
entry.nextRetryMs - now <= RETRY_MAX_MS
|
||||
)
|
||||
return heldSnapM(entry, surfacePos);
|
||||
// Per-window budget: deny WITHOUT a retry stamp so the overflow simply
|
||||
// tries again next tick instead of waiting out a backoff it didn't earn.
|
||||
if (now - windowStartMs > SAMPLE_WINDOW_MS) {
|
||||
// A clock that stepped back also opens a new window, or none would open
|
||||
// until it caught up.
|
||||
if (now < windowStartMs || now - windowStartMs > SAMPLE_WINDOW_MS) {
|
||||
windowStartMs = now;
|
||||
windowCount = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createGroundSnap } from './groundSnap.js';
|
||||
|
||||
const groundFloor = {
|
||||
cachedMeshFloor: () => null,
|
||||
meshFloorPreferred: () => false,
|
||||
reportValidatedMeshFloorCell: () => {},
|
||||
};
|
||||
|
||||
/** A viewer whose ground is a loaded 3D tileset, counting height samples. */
|
||||
function makeViewer() {
|
||||
const calls = { samples: 0 };
|
||||
const tileset = { show: true, tilesLoaded: true };
|
||||
return {
|
||||
calls,
|
||||
scene: {
|
||||
primitives: { length: 1, get: () => tileset },
|
||||
sampleHeight() {
|
||||
calls.samples += 1;
|
||||
return 1650;
|
||||
},
|
||||
globe: { getHeight: () => 1600 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const atDenverAirport = Cesium.Cartesian3.fromDegrees(-104.6737, 39.8561, 0);
|
||||
|
||||
test('a clock that steps back opens a new sample window instead of denying samples until it catches up', (t) => {
|
||||
t.mock.timers.enable({ apis: ['Date'], now: 1_000_000 });
|
||||
const viewer = makeViewer();
|
||||
const snap = createGroundSnap({ groundFloor });
|
||||
const icaos = ['a00001', 'a00002', 'a00003', 'a00004', 'a00005', 'a00006'];
|
||||
for (const icao of icaos) snap.heightFor(viewer, icao, atDenverAirport);
|
||||
assert.equal(viewer.calls.samples, 4, 'four samples per 250 ms window');
|
||||
t.mock.timers.setTime(1_000_000 - 60_000);
|
||||
for (const icao of icaos) snap.heightFor(viewer, icao, atDenverAirport);
|
||||
assert.equal(
|
||||
viewer.calls.samples,
|
||||
6,
|
||||
'the two denied aircraft sample in the new window',
|
||||
);
|
||||
});
|
||||
|
||||
test('a retry backoff stamped before the clock stepped back does not wait for it to catch up', (t) => {
|
||||
t.mock.timers.enable({ apis: ['Date'], now: 10_000_000 });
|
||||
const viewer = makeViewer();
|
||||
const tileset = viewer.scene.primitives.get(0);
|
||||
const snap = createGroundSnap({ groundFloor });
|
||||
// Tiles still streaming: a miss, backed off for 2 s.
|
||||
tileset.tilesLoaded = false;
|
||||
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), null);
|
||||
tileset.tilesLoaded = true;
|
||||
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), null);
|
||||
assert.equal(viewer.calls.samples, 0, 'the backoff holds');
|
||||
// The clock steps back an hour: the stamp is an hour off, past any backoff.
|
||||
t.mock.timers.setTime(10_000_000 - 3_600_000);
|
||||
assert.equal(snap.heightFor(viewer, 'a00001', atDenverAirport), 1650);
|
||||
assert.equal(viewer.calls.samples, 1);
|
||||
});
|
||||
@@ -98,6 +98,13 @@ function dockCapacity(raw) {
|
||||
return capacity - geofenced;
|
||||
}
|
||||
|
||||
/** Read a numeric coordinate without treating blank text or other JSON types as zero. */
|
||||
function coordinateNumber(value) {
|
||||
if (typeof value !== 'number' && (typeof value !== 'string' || !value.trim()))
|
||||
return NaN;
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GBFS station_information payload into a Map of station metadata.
|
||||
* Handles field-name variations across different GBFS providers
|
||||
@@ -113,9 +120,16 @@ export function parseStationInformation(payload) {
|
||||
for (const raw of stations) {
|
||||
// Accept both GBFS (station_id) and alternate (id) field names
|
||||
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
|
||||
const lat = Number(raw?.lat ?? raw?.latitude);
|
||||
const lon = Number(raw?.lon ?? raw?.longitude);
|
||||
if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||
const lat = coordinateNumber(raw?.lat ?? raw?.latitude);
|
||||
const lon = coordinateNumber(raw?.lon ?? raw?.longitude);
|
||||
if (
|
||||
!stationId ||
|
||||
!Number.isFinite(lat) ||
|
||||
!Number.isFinite(lon) ||
|
||||
Math.abs(lat) > 90 ||
|
||||
Math.abs(lon) > 180
|
||||
)
|
||||
continue;
|
||||
|
||||
stationMap.set(stationId, {
|
||||
stationId,
|
||||
|
||||
@@ -19,6 +19,73 @@ const parseCapacity = (fields) =>
|
||||
parseStationInformation({ data: { stations: [station(fields)] } }).get('s1')
|
||||
.capacity;
|
||||
|
||||
test('station information skips missing, blank and malformed coordinates', () => {
|
||||
for (const field of ['lat', 'lon', 'latitude', 'longitude']) {
|
||||
for (const value of [
|
||||
undefined,
|
||||
null,
|
||||
'',
|
||||
' \t ',
|
||||
false,
|
||||
true,
|
||||
[],
|
||||
[0],
|
||||
{},
|
||||
'bad',
|
||||
NaN,
|
||||
Infinity,
|
||||
]) {
|
||||
const raw = { station_id: 'bad', lat: 0, lon: 0 };
|
||||
if (field === 'latitude') delete raw.lat;
|
||||
if (field === 'longitude') delete raw.lon;
|
||||
raw[field] = value;
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [raw, station({ station_id: 'good' })] },
|
||||
});
|
||||
assert.deepEqual(
|
||||
[...parsed.keys()],
|
||||
['good'],
|
||||
`${field} = ${JSON.stringify(value)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('station information skips coordinates outside latitude and longitude bounds', () => {
|
||||
for (const fields of [
|
||||
{ lat: 90.001 },
|
||||
{ lat: -90.001 },
|
||||
{ lon: 180.001 },
|
||||
{ lon: -180.001 },
|
||||
{ lat: undefined, latitude: '91' },
|
||||
{ lon: undefined, longitude: '-181' },
|
||||
]) {
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [station(fields)] },
|
||||
});
|
||||
assert.equal(parsed.size, 0, JSON.stringify(fields));
|
||||
}
|
||||
});
|
||||
|
||||
test('station information preserves zero, boundary coordinates and numeric aliases', () => {
|
||||
for (const [fields, expected] of [
|
||||
[{ lat: 0, lon: 0 }, [0, 0]],
|
||||
[{ lat: ' 0 ', lon: '0' }, [0, 0]],
|
||||
[{ lat: 90, lon: 180 }, [90, 180]],
|
||||
[{ lat: '-90', lon: '-180' }, [-90, -180]],
|
||||
[
|
||||
{ lat: null, lon: undefined, latitude: '30.2681', longitude: '-97.7469' },
|
||||
[30.2681, -97.7469],
|
||||
],
|
||||
]) {
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [station(fields)] },
|
||||
}).get('s1');
|
||||
assert.ok(parsed, JSON.stringify(fields));
|
||||
assert.deepEqual([parsed.lat, parsed.lon], expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('capacity counts docks, not the geofenced area PBSC adds to it', () => {
|
||||
// Bilbao Bizi, BiciMAD and Dbizi as published on 2026-10-17.
|
||||
assert.equal(parseCapacity({ capacity: 839, geofenced_capacity: 800 }), 39);
|
||||
|
||||
@@ -171,6 +171,46 @@ test('bike share reports uncovered areas and failed systems honestly', async ()
|
||||
);
|
||||
});
|
||||
|
||||
test('bike share excludes blank coordinates without losing stations on the prime meridian', async () => {
|
||||
const stations = [
|
||||
{ station_id: 'blank', lat: 51.5, lon: ' ' },
|
||||
{ station_id: 'zero', lat: 51.5, lon: 0 },
|
||||
];
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
bikeshare: {
|
||||
systems: [system('fixture', 'Fixture Bikes', 51.5, 0)],
|
||||
async getStations(url) {
|
||||
return {
|
||||
data: {
|
||||
stations: url.endsWith('station_information.json')
|
||||
? stations
|
||||
: stations.map(({ station_id }) => ({
|
||||
station_id,
|
||||
num_bikes_available: 4,
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('get_bike_share', {
|
||||
area: { lat: 51.5, lon: 0, radius_km: 2 },
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.data.rows.map((row) => row.station_id),
|
||||
['zero'],
|
||||
);
|
||||
assert.equal(result.data.total, 1);
|
||||
assert.equal(result.data.rows[0].lon, 0);
|
||||
assert.equal(
|
||||
result.summary,
|
||||
'1 bike-share station in 2 km around 51.500, 0.000 with 4 bikes available.',
|
||||
);
|
||||
});
|
||||
|
||||
const feed = {
|
||||
id: 'capmetro',
|
||||
name: 'CapMetro',
|
||||
|
||||
+13
-1
@@ -159,18 +159,30 @@ export class VisualEffects {
|
||||
this.requestRender('bloom');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sharpen runs only when it is on and its slider is above 0, as the style
|
||||
* stages do: the pass costs the same at any amount (~1.5 ms GPU per frame
|
||||
* on the Arc, 2026-10-24), and `0.1 + value * 2` still sharpened at 0.
|
||||
*/
|
||||
syncSharpenEnabled() {
|
||||
if (this.stopped || !this.sharpenStage) return;
|
||||
this.sharpenStage.enabled =
|
||||
this.sharpenEnabled && this.sharpenIntensity > 0.001;
|
||||
}
|
||||
|
||||
applySharpenIntensity(value) {
|
||||
if (this.stopped) return;
|
||||
this.sharpenIntensity = value;
|
||||
if (this.sharpenStage)
|
||||
this.sharpenStage.uniforms.amount = 0.1 + value * 2.0;
|
||||
this.syncSharpenEnabled();
|
||||
this.requestRender('sharpen');
|
||||
}
|
||||
|
||||
setSharpenEnabled(enabled) {
|
||||
if (this.stopped) return;
|
||||
this.sharpenEnabled = !!enabled;
|
||||
if (this.sharpenStage) this.sharpenStage.enabled = this.sharpenEnabled;
|
||||
this.syncSharpenEnabled();
|
||||
this.requestRender('sharpen');
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,24 @@ test('sharpen uses the existing uniform scale and keeps values while disabled',
|
||||
f.effects.destroy();
|
||||
});
|
||||
|
||||
test('sharpen at 0 runs no pass, and comes back with the slider', () => {
|
||||
const f = fixture();
|
||||
f.effects.initPostProcess(0.49);
|
||||
f.effects.setSharpenEnabled(true);
|
||||
assert.equal(f.effects.sharpenStage.enabled, true);
|
||||
f.effects.applySharpenIntensity(0);
|
||||
assert.equal(
|
||||
f.effects.sharpenStage.enabled,
|
||||
false,
|
||||
'no residual sharpen at 0',
|
||||
);
|
||||
f.effects.setSharpenEnabled(true);
|
||||
assert.equal(f.effects.sharpenStage.enabled, false, 'on at 0 is still off');
|
||||
f.effects.applySharpenIntensity(0.2);
|
||||
assert.equal(f.effects.sharpenStage.enabled, true);
|
||||
f.effects.destroy();
|
||||
});
|
||||
|
||||
test('stop revokes pending work immediately but retains stages until final destruction', () => {
|
||||
const f = fixture();
|
||||
f.effects.initStyles();
|
||||
|
||||
Reference in new issue
Block a user