mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
406 lines
14 KiB
JavaScript
406 lines
14 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
ALLOCATION_ELASTIC,
|
|
ALLOCATION_WEIGHTED,
|
|
LabelArbiter,
|
|
} from './labelArbiter.js';
|
|
|
|
// Compact rows retain the full identity/corner/rect contract:
|
|
// key@corner@x@y@w@h (fade rows append @alpha@selected).
|
|
const GOLDEN = {
|
|
elasticTie12: {
|
|
selected: 'firms:000,firms:008,firms:010,vessels:003,vessels:007,vessels:009',
|
|
placements: 'firms:000@NE@-229@-190@38@16|firms:008@NE@451@-65@38@16|firms:010@NE@153@-304@38@16|vessels:003@NE@94@-74@38@16|vessels:007@NE@-418@-73@38@16|vessels:009@NE@-210@180@38@16',
|
|
fades: [],
|
|
selectedCount: 6,
|
|
quotas: 'firms:3,vessels:3',
|
|
labelsByLayer: 'firms:3,vessels:3',
|
|
queue: [2, 6],
|
|
spatialCellCount: 27,
|
|
},
|
|
weightedFew24: {
|
|
selected: 'cctv:003,cctv:009,cctv:015,cctv:018,firms:010,firms:016,vessels:008,vessels:011,vessels:020',
|
|
placements: 'cctv:003@NE@-163@-259@38@16|cctv:009@NE@188@-51@38@16|cctv:015@NE@-230@100@38@16|cctv:018@NE@-431@-109@38@16|firms:010@NE@-199@-290@38@16|firms:016@NE@232@232@38@16|vessels:008@NE@52@-209@38@16|vessels:011@NE@-68@-54@38@16|vessels:020@NE@-31@199@38@16',
|
|
fades: [],
|
|
selectedCount: 9,
|
|
quotas: 'cctv:4,firms:2,vessels:3',
|
|
labelsByLayer: 'cctv:4,firms:2,vessels:3',
|
|
queue: [3, 9],
|
|
spatialCellCount: 28,
|
|
},
|
|
duplicateKeys48: {
|
|
selected: 'a-blocker:fixed,z-duplicates:shared',
|
|
placements: 'a-blocker:fixed@only@0@0@20@20|z-duplicates:shared@free@300@300@20@20',
|
|
fades: [],
|
|
selectedCount: 2,
|
|
quotas: 'a-blocker:1,z-duplicates:1',
|
|
labelsByLayer: 'a-blocker:1,z-duplicates:1',
|
|
queue: [2, 2],
|
|
spatialCellCount: 5,
|
|
},
|
|
stable96: [
|
|
{
|
|
selected: 'flights:003,flights:009,flights:033,flights:057,flights:087,satellites:055,satellites:067,satellites:073,satellites:079,satellites:085,vessels:017,vessels:029,vessels:035,vessels:083,vessels:095',
|
|
placements: 'flights:003@NE@-214@22@38@16|flights:009@NE@190@-267@38@16|flights:033@NE@-437@237@38@16|flights:057@NE@43@241@38@16|flights:087@NE@384@3@38@16|satellites:055@NE@-343@-236@38@16|satellites:067@NE@420@187@38@16|satellites:073@NE@-380@40@38@16|satellites:079@NE@256@32@38@16|satellites:085@NE@-80@207@38@16|vessels:017@NE@-212@-132@38@16|vessels:029@NE@12@-14@38@16|vessels:035@NE@130@-106@38@16|vessels:083@NE@-43@-289@38@16|vessels:095@NE@439@-262@38@16',
|
|
fades: [],
|
|
selectedCount: 15,
|
|
quotas: 'flights:5,satellites:5,vessels:5',
|
|
labelsByLayer: 'flights:5,satellites:5,vessels:5',
|
|
queue: [3, 15],
|
|
spatialCellCount: 52,
|
|
},
|
|
{
|
|
selected: 'flights:003,flights:009,flights:033,flights:057,flights:087,satellites:055,satellites:067,satellites:073,satellites:079,satellites:085,vessels:017,vessels:029,vessels:035,vessels:083,vessels:095',
|
|
placements: 'flights:003@NE@-214@22@38@16|flights:009@NE@190@-267@38@16|flights:033@NE@-437@237@38@16|flights:057@NE@43@241@38@16|flights:087@NE@384@3@38@16|satellites:055@NE@-343@-236@38@16|satellites:067@NE@420@187@38@16|satellites:073@NE@-380@40@38@16|satellites:079@NE@256@32@38@16|satellites:085@NE@-80@207@38@16|vessels:017@NE@-212@-132@38@16|vessels:029@NE@12@-14@38@16|vessels:035@NE@130@-106@38@16|vessels:083@NE@-43@-289@38@16|vessels:095@NE@439@-262@38@16',
|
|
fades: [],
|
|
selectedCount: 15,
|
|
quotas: 'flights:5,satellites:5,vessels:5',
|
|
labelsByLayer: 'flights:5,satellites:5,vessels:5',
|
|
queue: [0, 0],
|
|
spatialCellCount: 52,
|
|
},
|
|
],
|
|
clustered200: {
|
|
selected: 'firms:000,firms:001',
|
|
placements: 'firms:000@above@70@66@60@20|firms:001@below@70@114@60@20',
|
|
fades: [],
|
|
selectedCount: 2,
|
|
quotas: 'firms:200',
|
|
labelsByLayer: 'firms:2',
|
|
queue: [2, 4],
|
|
spatialCellCount: 9,
|
|
},
|
|
packedCells400: {
|
|
selected: 'air:000,air:003,air:051,air:066,air:171,air:210,air:330,air:351,ground:013,ground:124,ground:157,ground:379,sea:146,sea:230,sea:281,sea:347,sea:383,sea:386',
|
|
placements: 'air:000@packed@-639@-127@10@10|air:003@packed@-607@-639@10@10|air:051@NE@-4288@546@38@16|air:066@NE@-245@-2390@38@16|air:171@NE@5493@2134@38@16|air:210@NE@2943@2389@38@16|air:330@NE@3289@-1454@38@16|air:351@NE@5423@-3072@38@16|ground:013@NE@2371@-2822@38@16|ground:124@NE@-20@3579@38@16|ground:157@NE@-1642@-3390@38@16|ground:379@NE@5196@-1036@38@16|sea:146@NE@-3559@-3258@38@16|sea:230@NE@-1938@-1868@38@16|sea:281@NE@5212@-1783@38@16|sea:347@NE@-2185@2564@38@16|sea:383@NE@2177@-1527@38@16|sea:386@NE@4250@1117@38@16',
|
|
fades: [],
|
|
selectedCount: 18,
|
|
quotas: 'air:8,ground:4,sea:6',
|
|
labelsByLayer: 'air:8,ground:4,sea:6',
|
|
queue: [3, 18],
|
|
spatialCellCount: 55,
|
|
},
|
|
fadeSequence12: [
|
|
{
|
|
selected: 'tracks:000,tracks:005,tracks:007,tracks:009',
|
|
placements: 'tracks:000@NE@-51@-134@38@16|tracks:005@NE@-179@-256@38@16|tracks:007@NE@347@200@38@16|tracks:009@NE@446@22@38@16',
|
|
fades: [
|
|
'7000:',
|
|
'7075:tracks:000@NE@-51@-134@38@[email protected]@true|tracks:005@NE@-179@-256@38@[email protected]@true|tracks:007@NE@347@200@38@[email protected]@true|tracks:009@NE@446@22@38@[email protected]@true',
|
|
'7150:tracks:000@NE@-51@-134@38@16@1@true|tracks:005@NE@-179@-256@38@16@1@true|tracks:007@NE@347@200@38@16@1@true|tracks:009@NE@446@22@38@16@1@true',
|
|
],
|
|
selectedCount: 4,
|
|
quotas: 'tracks:4',
|
|
labelsByLayer: 'tracks:4',
|
|
queue: [1, 4],
|
|
spatialCellCount: 15,
|
|
},
|
|
{
|
|
selected: 'tracks:007,tracks:009',
|
|
placements: 'tracks:007@NE@347@200@38@16|tracks:009@NE@446@22@38@16',
|
|
fades: [
|
|
'7200:tracks:000@NE@-51@-134@38@16@1@false|tracks:005@NE@-179@-256@38@16@1@false|tracks:007@NE@347@200@38@16@1@true|tracks:009@NE@446@22@38@16@1@true',
|
|
'7275:tracks:000@NE@-51@-134@38@[email protected]@false|tracks:005@NE@-179@-256@38@[email protected]@false|tracks:007@NE@347@200@38@16@1@true|tracks:009@NE@446@22@38@16@1@true',
|
|
'7350:tracks:000@NE@-51@-134@38@[email protected]@false|tracks:005@NE@-179@-256@38@[email protected]@false|tracks:007@NE@347@200@38@16@1@true|tracks:009@NE@446@22@38@16@1@true',
|
|
'7501:tracks:007@NE@347@200@38@16@1@true|tracks:009@NE@446@22@38@16@1@true',
|
|
],
|
|
selectedCount: 2,
|
|
quotas: 'tracks:2',
|
|
labelsByLayer: 'tracks:2',
|
|
queue: [1, 2],
|
|
spatialCellCount: 15,
|
|
},
|
|
],
|
|
};
|
|
|
|
function seededRandom(seed) {
|
|
let state = seed >>> 0;
|
|
return () => {
|
|
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
return state / 0x100000000;
|
|
};
|
|
}
|
|
|
|
function rect(x, y, w = 38, h = 16) {
|
|
return { x, y, w, h };
|
|
}
|
|
|
|
function placementsFor(x, y, shape = 'full') {
|
|
if (shape === 'verticalOnly') {
|
|
return [
|
|
{ corner: 'above', rect: rect(x - 30, y - 34, 60, 20) },
|
|
{ corner: 'below', rect: rect(x - 30, y + 14, 60, 20) },
|
|
];
|
|
}
|
|
if (shape === 'few') {
|
|
return [
|
|
{ corner: 'NE', rect: rect(x + 8, y - 24) },
|
|
{ corner: 'SW', rect: rect(x - 46, y + 8) },
|
|
];
|
|
}
|
|
return [
|
|
{ corner: 'NE', rect: rect(x + 8, y - 24) },
|
|
{ corner: 'NW', rect: rect(x - 46, y - 24) },
|
|
{ corner: 'SE', rect: rect(x + 8, y + 8) },
|
|
{ corner: 'SW', rect: rect(x - 46, y + 8) },
|
|
];
|
|
}
|
|
|
|
function makeCohort(count, {
|
|
seed,
|
|
layers,
|
|
shape = 'full',
|
|
tieHeavy = false,
|
|
spreadX = 900,
|
|
spreadY = 560,
|
|
} = {}) {
|
|
const random = seededRandom(seed);
|
|
return Array.from({ length: count }, (_, index) => {
|
|
const x = Math.floor(random() * spreadX) - Math.floor(spreadX / 2);
|
|
const y = Math.floor(random() * spreadY) - Math.floor(spreadY / 2);
|
|
const layerId = layers[index % layers.length];
|
|
return {
|
|
key: `${layerId}:${String(index).padStart(3, '0')}`,
|
|
layerId,
|
|
sourceId: index,
|
|
priority: tieHeavy ? index % 2 : Math.floor(random() * 7),
|
|
centerDistance: tieHeavy ? 100 : Math.hypot(x, y),
|
|
keyholeAlpha: tieHeavy ? 1 : 0.55 + Math.floor(random() * 4) * 0.1,
|
|
screenX: x,
|
|
screenY: y,
|
|
placements: placementsFor(x, y, shape),
|
|
};
|
|
});
|
|
}
|
|
|
|
function roundedAlpha(value) {
|
|
return Number(value.toFixed(6));
|
|
}
|
|
|
|
function snapshotSolve(arbiter, candidates, options, fadeTimes = []) {
|
|
const diagnostics = arbiter.solve(candidates, options);
|
|
const selected = Array.from(arbiter.selectedKeys).sort();
|
|
const placements = selected.map((key) => {
|
|
const state = arbiter.states.get(key);
|
|
const { x, y, w, h } = state.lastPlacement.rect;
|
|
return [key, state.corner, x, y, w, h];
|
|
});
|
|
const current = new Map(candidates.map((candidate) => [candidate.key, candidate]));
|
|
const fades = fadeTimes.map((now) => [
|
|
now,
|
|
...arbiter.renderEntries(current, now)
|
|
.map((entry) => {
|
|
const { x, y, w, h } = entry.placement.rect;
|
|
return [
|
|
entry.candidate.key,
|
|
entry.placement.corner,
|
|
x,
|
|
y,
|
|
w,
|
|
h,
|
|
roundedAlpha(entry.temporalAlpha),
|
|
entry.selected,
|
|
];
|
|
})
|
|
.sort(([a], [b]) => a.localeCompare(b)),
|
|
]);
|
|
return {
|
|
selected: selected.join(','),
|
|
placements: placements.map((row) => row.join('@')).join('|'),
|
|
fades: fades.map(([now, ...entries]) => `${now}:${entries.map((row) => row.join('@')).join('|')}`),
|
|
selectedCount: diagnostics.selectedCount,
|
|
quotas: Object.entries(diagnostics.quotas).map((row) => row.join(':')).join(','),
|
|
labelsByLayer: Object.entries(diagnostics.labelsByLayer).map((row) => row.join(':')).join(','),
|
|
queue: [diagnostics.spatialQueueBuildCount, diagnostics.spatialQueueNextCount],
|
|
spatialCellCount: arbiter._spatial.cells.size,
|
|
};
|
|
}
|
|
|
|
function buildGoldenBattery() {
|
|
const elasticTie12 = makeCohort(12, {
|
|
seed: 0x12e1a57,
|
|
layers: ['firms', 'vessels'],
|
|
tieHeavy: true,
|
|
});
|
|
const elasticArbiter = new LabelArbiter();
|
|
|
|
const weightedFew24 = makeCohort(24, {
|
|
seed: 0x24fe771,
|
|
layers: ['cctv', 'firms', 'vessels'],
|
|
shape: 'few',
|
|
});
|
|
const weightedArbiter = new LabelArbiter();
|
|
|
|
const duplicateTail = makeCohort(45, {
|
|
seed: 0x48d0b1e,
|
|
layers: ['z-duplicates'],
|
|
shape: 'verticalOnly',
|
|
}).map((candidate, index) => ({
|
|
...candidate,
|
|
key: `z-duplicates:tail-${String(index).padStart(2, '0')}`,
|
|
priority: 0,
|
|
}));
|
|
const duplicateCandidates = [
|
|
{
|
|
key: 'a-blocker:fixed',
|
|
layerId: 'a-blocker',
|
|
sourceId: 'blocker',
|
|
priority: 100,
|
|
centerDistance: 0,
|
|
keyholeAlpha: 1,
|
|
screenX: 0,
|
|
screenY: 0,
|
|
placements: [{ corner: 'only', rect: rect(0, 0, 20, 20) }],
|
|
},
|
|
{
|
|
key: 'z-duplicates:shared',
|
|
layerId: 'z-duplicates',
|
|
sourceId: 'blocked-copy',
|
|
priority: 99,
|
|
centerDistance: 1,
|
|
keyholeAlpha: 1,
|
|
screenX: 4,
|
|
screenY: 4,
|
|
placements: [{ corner: 'blocked', rect: rect(0, 0, 20, 20) }],
|
|
},
|
|
{
|
|
key: 'z-duplicates:shared',
|
|
layerId: 'z-duplicates',
|
|
sourceId: 'free-copy',
|
|
priority: 99,
|
|
centerDistance: 1,
|
|
keyholeAlpha: 1,
|
|
screenX: 300,
|
|
screenY: 300,
|
|
placements: [{ corner: 'free', rect: rect(300, 300, 20, 20) }],
|
|
},
|
|
...duplicateTail,
|
|
];
|
|
const duplicateArbiter = new LabelArbiter();
|
|
|
|
const stable96 = makeCohort(96, {
|
|
seed: 0x96c0ffee,
|
|
layers: ['flights', 'satellites', 'vessels'],
|
|
shape: 'few',
|
|
tieHeavy: true,
|
|
});
|
|
const stableArbiter = new LabelArbiter();
|
|
|
|
const clustered200 = makeCohort(200, {
|
|
seed: 0x200f1e1d,
|
|
layers: ['firms'],
|
|
shape: 'verticalOnly',
|
|
}).map((candidate, index) => ({
|
|
...candidate,
|
|
priority: 200 - index,
|
|
centerDistance: index,
|
|
screenX: 100,
|
|
screenY: 100,
|
|
placements: placementsFor(100, 100, 'verticalOnly'),
|
|
}));
|
|
const clusteredArbiter = new LabelArbiter();
|
|
|
|
const packed400 = makeCohort(400, {
|
|
seed: 0x400b17c5,
|
|
layers: ['air', 'ground', 'sea'],
|
|
shape: 'few',
|
|
spreadX: 12000,
|
|
spreadY: 8000,
|
|
});
|
|
const packedCell = (candidate, cellX, cellY, priority) => ({
|
|
...candidate,
|
|
priority,
|
|
keyholeAlpha: 1,
|
|
centerDistance: 0,
|
|
screenX: cellX * 32 + 1,
|
|
screenY: cellY * 32 + 1,
|
|
placements: [{
|
|
corner: 'packed',
|
|
rect: rect(cellX * 32 + 1, cellY * 32 + 1, 10, 10),
|
|
}],
|
|
});
|
|
packed400[0] = packedCell(packed400[0], -20, -4, 1000);
|
|
packed400[3] = packedCell(packed400[3], -19, -20, 999);
|
|
const packedArbiter = new LabelArbiter();
|
|
|
|
const fade12 = makeCohort(12, {
|
|
seed: 0xfade0012,
|
|
layers: ['tracks'],
|
|
shape: 'few',
|
|
});
|
|
const fadeArbiter = new LabelArbiter();
|
|
|
|
return {
|
|
elasticTie12: snapshotSolve(elasticArbiter, elasticTie12, {
|
|
capacity: 6,
|
|
strategy: ALLOCATION_ELASTIC,
|
|
now: 1000,
|
|
preserveIncumbents: false,
|
|
}),
|
|
weightedFew24: snapshotSolve(weightedArbiter, weightedFew24, {
|
|
capacity: 9,
|
|
strategy: ALLOCATION_WEIGHTED,
|
|
layerWeights: { cctv: 2, firms: 0.75, vessels: 1.25 },
|
|
now: 2000,
|
|
preserveIncumbents: false,
|
|
}),
|
|
duplicateKeys48: snapshotSolve(duplicateArbiter, duplicateCandidates, {
|
|
capacity: 2,
|
|
strategy: ALLOCATION_ELASTIC,
|
|
demandByLayer: { 'a-blocker': 1, 'z-duplicates': 47 },
|
|
now: 3000,
|
|
preserveIncumbents: false,
|
|
}),
|
|
stable96: [
|
|
snapshotSolve(stableArbiter, stable96, {
|
|
capacity: 15,
|
|
strategy: ALLOCATION_WEIGHTED,
|
|
now: 4000,
|
|
preserveIncumbents: true,
|
|
}),
|
|
snapshotSolve(stableArbiter, stable96.slice().reverse(), {
|
|
capacity: 15,
|
|
strategy: ALLOCATION_WEIGHTED,
|
|
now: 4300,
|
|
preserveIncumbents: true,
|
|
}),
|
|
],
|
|
clustered200: snapshotSolve(clusteredArbiter, clustered200, {
|
|
capacity: 200,
|
|
strategy: ALLOCATION_ELASTIC,
|
|
now: 5000,
|
|
preserveIncumbents: false,
|
|
}),
|
|
packedCells400: snapshotSolve(packedArbiter, packed400, {
|
|
capacity: 18,
|
|
strategy: ALLOCATION_WEIGHTED,
|
|
layerWeights: { air: 1.5, ground: 0.75, sea: 1 },
|
|
now: 6000,
|
|
preserveIncumbents: false,
|
|
}),
|
|
fadeSequence12: [
|
|
snapshotSolve(fadeArbiter, fade12, {
|
|
capacity: 4,
|
|
strategy: ALLOCATION_ELASTIC,
|
|
now: 7000,
|
|
preserveIncumbents: false,
|
|
}, [7000, 7075, 7150]),
|
|
snapshotSolve(fadeArbiter, fade12.slice(4), {
|
|
capacity: 2,
|
|
strategy: ALLOCATION_ELASTIC,
|
|
now: 7200,
|
|
preserveIncumbents: false,
|
|
}, [7200, 7275, 7350, 7501]),
|
|
],
|
|
};
|
|
}
|
|
|
|
if (process.env.GEV_PRINT_ARBITER_GOLDEN === '1') {
|
|
process.stdout.write(`${JSON.stringify(buildGoldenBattery(), null, 2)}\n`);
|
|
} else {
|
|
test('seeded arbiter scenario battery matches the current golden behavior', () => {
|
|
assert.deepEqual(buildGoldenBattery(), GOLDEN);
|
|
});
|
|
}
|