const ATTRIBUTION = '© OpenStreetMap contributors' /** Longest match first — Flock, Genetec, Leonardo, etc. */ const KNOWN_ALPR_MODELS = [ 'Falcon Flex', 'AutoVu CR-H2', 'AutoVu Sharp', 'UnicamVELOCITY3', 'Falcon', 'Sparrow', 'Raven', 'Condor', 'Wing', 'Pelican', ] const GENERIC_CAMERA_NAMES = new Set([ 'flock alpr camera', 'flock alpr cameera', 'flock camera', 'flock alpr', 'flock', 'flock camera', 'automatic license plate reader (alpr)', 'alpr camera', 'alpr', ]) const SKIP_EXTRA_TAGS = new Set([ 'surveillance:type', 'man_made', 'camera:direction', 'direction', 'camera:angle', 'surveillance:angle', 'manufacturer', 'brand', 'model', 'operator', 'name', 'ref', 'surveillance', 'camera:type', 'description', 'note', 'colour', 'color', 'manufacturer:wikidata', 'model:wikidata', 'operator:wikidata', ]) function escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** @param {string} value */ export function normalizeModelName(value) { const trimmed = value.trim() if (!trimmed) return null const lower = trimmed.toLowerCase() for (const model of KNOWN_ALPR_MODELS) { if (model.toLowerCase() === lower) return model } return trimmed.split(/\s+/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), ).join(' ') } /** @param {string} text */ function matchKnownModel(text) { for (const model of KNOWN_ALPR_MODELS) { const re = new RegExp(`\\b${escapeRegex(model)}\\b`, 'i') if (re.test(text)) return model } const flock = text.match(/\bFlock\s+([a-z][\w -]*)/i) if (flock) { const fragment = flock[1].trim() if (!fragment || /^safety$/i.test(fragment)) return null return matchKnownModel(fragment) } return null } /** @param {Record} tags */ export function inferModelFromTags(tags) { const t = tags ?? {} if (t.model?.trim()) return normalizeModelName(t.model) for (const key of ['name', 'description', 'note']) { const value = t[key] if (!value?.trim()) continue const model = matchKnownModel(value) if (model) return model } return null } /** @param {string | null | undefined} name */ export function isGenericCameraName(name) { if (!name?.trim()) return false const normalized = name.trim().toLowerCase() if (GENERIC_CAMERA_NAMES.has(normalized)) return true if (/^flock\s+alpr\b/i.test(name) && !matchKnownModel(name)) return true if (matchKnownModel(name) && /\bflock\b/i.test(name)) return true return false } /** @param {Record} tags */ export function displayNameFromTags(tags) { const name = tags?.name?.trim() if (!name || isGenericCameraName(name)) return null if (matchKnownModel(name)) return null return name } /** @param {Record} tags */ export function identifyingProperties(tags, manufacturer = null) { const t = tags ?? {} const mfr = manufacturer ?? t.manufacturer ?? t.brand ?? null const model = inferModelFromTags(t) const name = displayNameFromTags(t) /** @type {Record} */ const props = {} if (mfr) props.manufacturer = mfr if (model) props.model = model if (mfr && !model) props.modelUnknown = true if (t.brand && t.brand !== mfr) props.brand = t.brand if (t.operator) props.operator = t.operator if (name) props.name = name if (t.ref) props.ref = t.ref if (t.surveillance) props.surveillance = t.surveillance if (t['camera:type']) props.cameraType = t['camera:type'] if (t.description) props.description = t.description if (t.note) props.note = t.note const colour = t.colour ?? t.color if (colour) props.colour = colour if (t['manufacturer:wikidata']) props.manufacturerWikidata = t['manufacturer:wikidata'] if (t['model:wikidata']) props.modelWikidata = t['model:wikidata'] if (t['operator:wikidata']) props.operatorWikidata = t['operator:wikidata'] const extra = {} for (const [key, value] of Object.entries(t)) { if (SKIP_EXTRA_TAGS.has(key) || !value?.trim()) continue extra[key] = value } if (Object.keys(extra).length) props.tags = extra return props } /** @param {{ osmId: number, lat: number, lng: number, manufacturer: string | null, direction: number | null, fov: number | null, tags: Record }} camera */ export function cameraToFeature(camera) { return { type: 'Feature', id: camera.osmId, geometry: { type: 'Point', coordinates: [camera.lng, camera.lat] }, properties: { osmId: camera.osmId, direction: camera.direction, fov: camera.fov, ...identifyingProperties(camera.tags, camera.manufacturer), }, } } /** @param {ReturnType[]} features */ export function featureCollection(features, source) { return { type: 'FeatureCollection', features, attribution: ATTRIBUTION, source, } } export { ATTRIBUTION }