Pin that an ALPR camera on a tile edge stays one record with its tagged azimuth

This commit is contained in:
German Khokhlov committed 2026-11-03 19:45:59 +04:00
1 parent d86047fe8f
commit c45ec634ee
1 file changed
+131
+131
View File
@@ -0,0 +1,131 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { PbfWriter } from 'pbf';
import * as geo from '../../geo/index.js';
import { tilesForBounds, tileToBBox } from '../../data/tomtomTiles.js';
import { alprDetailZoom, createAlprTileSource } from './source.js';
import { validateAlprSnapshot } from './model.js';
import { directionWedgePositions } from './visuals.js';
import { decodeAlprTile } from './tileRecords.js';
import { DIRECTION_CONE_HALF_ANGLE_DEG } from './policy.js';
const EXTENT = 4096;
const box = { south: 30.25, north: 30.3, west: -97.8, east: -97.7 };
/** Encode one `cameras` layer holding point features (MVT 2.1). */
function encodeCameraTile(points) {
const keys = [];
const values = [];
const index = (list, value) => {
const at = list.indexOf(value);
return at === -1 ? list.push(value) - 1 : at;
};
const zigzag = (n) => (n << 1) ^ (n >> 31);
const features = points.map(({ x, y, properties }) => {
const tags = [];
for (const [key, value] of Object.entries(properties))
tags.push(index(keys, key), index(values, String(value)));
return { tags, geometry: [9, zigzag(x), zigzag(y)] };
});
const pbf = new PbfWriter();
pbf.writeMessage(3, (_layer, out) => {
out.writeVarintField(15, 2);
out.writeStringField(1, 'cameras');
for (const feature of features)
out.writeMessage(2, (_f, w) => {
w.writePackedVarint(2, feature.tags);
w.writeVarintField(3, 1);
w.writePackedVarint(4, feature.geometry);
});
for (const key of keys) out.writeStringField(3, key);
for (const value of values)
out.writeMessage(4, (_v, w) => w.writeStringField(1, value));
out.writeVarintField(5, EXTENT);
});
return pbf.finish();
}
const metadata = (country) => ({
tiles: [
`https://tiles.dontgetflocked.com/cameras-${country}-hourly/{z}/{x}/{y}.mvt`,
],
bounds: country === 'ca' ? [-124, 42, -63, 54] : [-160, 17, -64, 59],
});
/** Initial great-circle bearing from one Cartesian to another, in degrees. */
function bearingDeg(from, to) {
const a = geo.cartographic.fromCartesian(from);
const b = geo.cartographic.fromCartesian(to);
const dLon = b.longitude - a.longitude;
const y = Math.sin(dLon) * Math.cos(b.latitude);
const x =
Math.cos(a.latitude) * Math.sin(b.latitude) -
Math.sin(a.latitude) * Math.cos(b.latitude) * Math.cos(dLon);
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
}
test('edge:alpr-cameras: a camera on a tile edge is one record with its own azimuth', async () => {
const zoom = alprDetailZoom(box);
const tiles = tilesForBounds(box, zoom, { maxTiles: 17 });
// Two horizontally adjacent tiles of one row share the camera's meridian.
const west = tiles.find((tile) =>
tiles.some((other) => other.x === tile.x + 1 && other.y === tile.y),
);
const east = { x: west.x + 1, y: west.y };
const edgeLon = tileToBBox(zoom, west.x, west.y).east;
assert.equal(edgeLon, tileToBBox(zoom, east.x, east.y).west);
const camera = {
osmType: 'node',
osmId: 424242,
direction: 135,
brand: 'Fixture',
};
const bytes = new Map([
[
`${west.x}/${west.y}`,
encodeCameraTile([{ x: EXTENT, y: 2048, properties: camera }]),
],
[
`${east.x}/${east.y}`,
encodeCameraTile([{ x: 0, y: 2048, properties: camera }]),
],
]);
// Both tiles really carry the camera, at the same longitude.
const decoded = [west, east].map(
({ x, y }) => decodeAlprTile(bytes.get(`${x}/${y}`), zoom, x, y)[0],
);
assert.deepEqual(
decoded.map((r) => r.id),
['alpr:424242', 'alpr:424242'],
);
assert.equal(decoded[0].longitude, decoded[1].longitude);
const source = createAlprTileSource({
tileFetchImpl: async (url) => {
if (url.endsWith('.json'))
return Response.json(metadata(url.includes('-ca-') ? 'ca' : 'us'));
const [, x, y] = url.match(/\/\d+\/(\d+)\/(\d+)\.mvt$/);
return new Response(bytes.get(`${x}/${y}`) ?? encodeCameraTile([]));
},
});
const snapshot = validateAlprSnapshot(await source.fetch(box));
const matches = snapshot.records.filter((r) => r.id === 'alpr:424242');
assert.equal(matches.length, 1);
assert.equal(snapshot.records.length, 1);
const [record] = matches;
assert.ok(Math.abs(record.longitude - edgeLon) < 1e-9);
assert.equal(record.directionDeg, 135);
// The bearing wedge is centred on the tagged azimuth.
const [apex, left, right] = directionWedgePositions(record);
const leftDeg = bearingDeg(apex, left);
const rightDeg = bearingDeg(apex, right);
assert.ok(Math.abs(leftDeg - (135 - DIRECTION_CONE_HALF_ANGLE_DEG)) < 0.01);
assert.ok(Math.abs(rightDeg - (135 + DIRECTION_CONE_HALF_ANGLE_DEG)) < 0.01);
// A repeat read (the other engine's scene) keeps the same identity.
const again = await source.fetch(box);
assert.deepEqual(
again.records.map((r) => [r.id, r.latitude, r.longitude, r.directionDeg]),
[[record.id, record.latitude, record.longitude, record.directionDeg]],
);
});