- site vitrine + fingerprint test + zmap + z-panel - remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs) - suppression du token DuckDNS (plus utilisé)
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
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
|
|
// <x>_<y>.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<string, { x: number; y: number; count: number }> | null = null
|
|
const cells = new Map<string, { off: Int32Array; data: Buffer }>()
|
|
|
|
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<string, { x: number; y: number; count: number }> | 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<string, { x: number; y: number; indices: number[] }>()
|
|
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
|
|
}
|