import { readFileSync, existsSync } from "node:fs" import { gunzipSync } from "node:zlib" // ============================================================================ // API publique "maxspeed" — jeu de données OSM des limitations de vitesse. // // Données : grille de cellules 0.5° x 0.5° couvrant l'Europe (produite par // data/maxspeed/build_maxspeed.py à partir du planet OSM). Chaque fichier // _.bin.gz = blob gzip : // int32 count ; int32 offsets[32*32] ; puis count points de // (lonq uint16, latq uint16, speed uint8) triés par sous-cellule 128. // // Ce module est autonome (indépendant de ZMap). Lecture lazy par cellule avec // cache LRU borné. // ============================================================================ const CELL = 0.5 const SUB = 32 const MAX_DIST = 40.0 // mètres: distance maximale route->point pour un "hit" const DEFAULT_DIR = process.env.ZMAP_MAXSPEED_DIR || "" let GRID_DIR = DEFAULT_DIR let index: Record | null = null const cells = new Map() export function setMaxspeedDir(dir: string) { GRID_DIR = dir index = null cells.clear() } export function gridDir(): string { return GRID_DIR } export function meta() { const idx = loadIndex() if (!idx) return null let points = 0 let files = 0 for (const k in idx) { files++ points += idx[k]!.count } return { cell: CELL, sub: SUB, stepM: 40.0, points, files, dir: GRID_DIR, } } function loadIndex(): Record | null { if (!GRID_DIR) return null if (!index) { const f = `${GRID_DIR}/index.json` if (!existsSync(f)) return null try { index = JSON.parse(readFileSync(f, "utf8")) } catch { return null } } return index } function loadCell(x: number, y: number): { off: Int32Array; data: Buffer } | null { const key = `${x}_${y}` const hit = cells.get(key) if (hit) return hit const f = `${GRID_DIR}/${key}.bin.gz` if (!existsSync(f)) return null try { const raw = gunzipSync(readFileSync(f)) const off = new Int32Array(raw.buffer, raw.byteOffset + 4, SUB * SUB) const data = raw.subarray(4 + SUB * SUB * 4) const c = { off, data } cells.set(key, c) if (cells.size > 64) cells.delete(cells.keys().next().value!) return c } catch { return null } } // Recherche la vitesse au coordonnée donnée. Retour km/h, ou null si aucun // point de route dans les 40 m. Coordonnées (lon, lat). export function snapPoint(lon: number, lat: number): number | null { const idx = loadIndex() if (!idx) return null const x = Math.floor(lon / CELL) const y = Math.floor(lat / CELL) const cell = loadCell(x, y) if (!cell) return null return snapInCell(cell, x, y, lat, lon) } function snapInCell(cell: { off: Int32Array; data: Buffer }, cx: number, cy: number, lat: number, lon: number): number | null { const lonq = Math.round(((lon - cx * CELL) / CELL) * 65535) const latq = Math.round(((lat - cy * CELL) / CELL) * 65535) if (lonq < 0 || lonq > 65535 || latq < 0 || latq > 65535) return null const sx = Math.min(SUB - 1, Math.floor((lonq * SUB) / 65536)) const sy = Math.min(SUB - 1, Math.floor((latq * SUB) / 65536)) let best: number | null = null let bestD2 = Infinity const cosLat = Math.cos((lat * Math.PI) / 180) for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { const sxx = sx + dx const syy = sy + dy if (sxx < 0 || syy < 0 || sxx >= SUB || syy >= SUB) continue const sub = sxx + syy * SUB const start = cell.off[sub]! const end = sub + 1 < SUB * SUB ? cell.off[sub + 1]! : cell.data.length / 5 for (let i = start; i < end; i++) { const base = i * 5 const plon = cx * CELL + (cell.data.readUInt16LE(base) / 65535) * CELL const plat = cy * CELL + (cell.data.readUInt16LE(base + 2) / 65535) * CELL const dlonp = (lon - plon) * 111320 * cosLat const dlatp = (lat - plat) * 110540 const d2 = dlonp * dlonp + dlatp * dlatp if (d2 < bestD2) { bestD2 = d2 best = cell.data.readUInt8(base + 4) } } } } if (bestD2 > MAX_DIST * MAX_DIST) return null return best } // Snap sur un tracé (liste de [lon, lat]). Retour un tableau de vitesses, // null quand aucun point de route proche. export function snapRoute(coords: [number, number][]): (number | null)[] { const out: (number | null)[] = new Array(coords.length).fill(null) const idx = loadIndex() if (!idx) return out const byCell = new Map() for (let i = 0; i < coords.length; i++) { const [lon, lat] = coords[i]! const x = Math.floor(lon / CELL) const y = Math.floor(lat / CELL) const k = `${x}_${y}` let e = byCell.get(k) if (!e) { e = { x, y, indices: [] } byCell.set(k, e) } e.indices.push(i) } for (const { x, y, indices } of byCell.values()) { const cell = loadCell(x, y) if (!cell) continue for (const i of indices) { const [lon, lat] = coords[i]! out[i] = snapInCell(cell, x, y, lat, lon) } } return out }