chore: checkpoint initial — Zektyc dataset, avant humanisation
- site vitrine + fingerprint test + zmap + z-panel - remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs) - suppression du token DuckDNS (plus utilisé)
This commit is contained in:
commit
8e7ec41f18
104 changed files with 19256 additions and 0 deletions
163
src/maxspeed-api.ts
Normal file
163
src/maxspeed-api.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue