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:
riricdev 2026-09-06 13:03:25 +02:00
commit 8e7ec41f18
104 changed files with 19256 additions and 0 deletions

163
src/maxspeed-api.ts Normal file
View 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
}