Add ADS-B, AIS, and ALPR map layers with live CoT streaming.
Ingest aircraft and vessel tracks via OSINT feeds and tactical CoT, expose viewport-filtered SSE to the map, and add an OSM ALPR layer with tiled caching and performant marker sync.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
parseOverpassElement,
|
||||
parseBbox,
|
||||
tileKeysForBbox,
|
||||
getAlprCameras,
|
||||
markTilesFetched,
|
||||
} from '../../server/utils/alpr.js'
|
||||
import { cameraToFeature, identifyingProperties, inferModelFromTags, isGenericCameraName } from '../../server/utils/alprGeo.js'
|
||||
import { getDb, setDbPathForTest, closeDb } from '../../server/utils/db.js'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
describe('alpr utils', () => {
|
||||
describe('parseOverpassElement', () => {
|
||||
it('parses a valid OSM node', () => {
|
||||
const out = parseOverpassElement({
|
||||
type: 'node',
|
||||
id: 123,
|
||||
lat: 33.75,
|
||||
lon: -84.39,
|
||||
tags: { 'manufacturer': 'Flock Safety', 'camera:direction': '90' },
|
||||
})
|
||||
expect(out?.osmId).toBe(123)
|
||||
expect(out?.manufacturer).toBe('Flock Safety')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cameraToFeature', () => {
|
||||
it('returns GeoJSON Point feature with identifying tags', () => {
|
||||
const feature = cameraToFeature({
|
||||
osmId: 1,
|
||||
lat: 33.5,
|
||||
lng: -84.5,
|
||||
manufacturer: 'Flock Safety',
|
||||
direction: 90,
|
||||
fov: 45,
|
||||
tags: {
|
||||
'model': 'Falcon',
|
||||
'operator': 'City PD',
|
||||
'ref': 'CAM-12',
|
||||
'operator:wikidata': 'Q123',
|
||||
'fixme': 'verify mount',
|
||||
},
|
||||
})
|
||||
expect(feature.properties.manufacturer).toBe('Flock Safety')
|
||||
expect(feature.properties.model).toBe('Falcon')
|
||||
expect(feature.properties.operator).toBe('City PD')
|
||||
expect(feature.properties.ref).toBe('CAM-12')
|
||||
expect(feature.properties.operatorWikidata).toBe('Q123')
|
||||
expect(feature.properties.tags).toEqual({ fixme: 'verify mount' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifyingProperties', () => {
|
||||
it('omits brand when same as manufacturer', () => {
|
||||
const props = identifyingProperties({ manufacturer: 'Flock Safety', brand: 'Flock Safety', model: 'Falcon' })
|
||||
expect(props.manufacturer).toBe('Flock Safety')
|
||||
expect(props.model).toBe('Falcon')
|
||||
expect(props.brand).toBeUndefined()
|
||||
})
|
||||
|
||||
it('infers model from lowercase name tag', () => {
|
||||
const props = identifyingProperties({
|
||||
manufacturer: 'Flock Safety',
|
||||
name: 'falcon',
|
||||
})
|
||||
expect(props.model).toBe('Falcon')
|
||||
expect(props.name).toBeUndefined()
|
||||
expect(props.modelUnknown).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops generic camera names and flags unknown model', () => {
|
||||
const props = identifyingProperties({
|
||||
manufacturer: 'Flock Safety',
|
||||
name: 'Flock ALPR camera',
|
||||
})
|
||||
expect(props.name).toBeUndefined()
|
||||
expect(props.model).toBeUndefined()
|
||||
expect(props.modelUnknown).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inferModelFromTags', () => {
|
||||
it('reads model tag and mis-tagged Flock Falcon name', () => {
|
||||
expect(inferModelFromTags({ model: 'Sparrow' })).toBe('Sparrow')
|
||||
expect(inferModelFromTags({ name: 'Flock Falcon' })).toBe('Falcon')
|
||||
expect(isGenericCameraName('Flock ALPR camera')).toBe(true)
|
||||
expect(isGenericCameraName('Peachtree & 5th')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseBbox', () => {
|
||||
it('parses valid bbox', () => {
|
||||
expect(parseBbox({ south: 33.0, west: -85.0, north: 33.4, east: -84.6 })).toEqual({
|
||||
south: 33.0,
|
||||
west: -85.0,
|
||||
north: 33.4,
|
||||
east: -84.6,
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects oversized bbox', () => {
|
||||
expect(() => parseBbox({ south: 0, west: 0, north: 2, east: 1 })).toThrow(/bbox exceeds/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cache and fetch', () => {
|
||||
let dbPath
|
||||
|
||||
beforeEach(async () => {
|
||||
dbPath = join(tmpdir(), `kestrelos-alpr-${randomUUID()}.db`)
|
||||
setDbPathForTest(dbPath)
|
||||
await getDb()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeDb()
|
||||
setDbPathForTest(null)
|
||||
})
|
||||
|
||||
it('returns GeoJSON FeatureCollection from cache', async () => {
|
||||
const db = await getDb()
|
||||
const bbox = { south: 33.4, west: -85.0, north: 33.6, east: -84.0 }
|
||||
await db.run(
|
||||
`INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[99, 33.5, -84.5, 'Acme', 180, '{}', new Date().toISOString()],
|
||||
)
|
||||
await markTilesFetched(db, tileKeysForBbox(bbox))
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
const result = await getAlprCameras(db, bbox)
|
||||
expect(result.type).toBe('FeatureCollection')
|
||||
expect(result.features).toHaveLength(1)
|
||||
expect(result.source).toBe('cache')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('falls back to cache when Overpass fails', async () => {
|
||||
const db = await getDb()
|
||||
await db.run(
|
||||
`INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[42, 33.5, -84.5, null, null, '{}', new Date().toISOString()],
|
||||
)
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
|
||||
const result = await getAlprCameras(db, { south: 33.4, west: -85.0, north: 33.6, east: -84.0 })
|
||||
expect(result.type).toBe('FeatureCollection')
|
||||
expect(result.features).toHaveLength(1)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user