From 2e403610ca9d3883014814fbc5147016e45619e5 Mon Sep 17 00:00:00 2001 From: najajan Date: Sat, 20 Sep 2025 16:43:51 +0200 Subject: [PATCH] use runway and gate names but fails to generate route --- server/api/service/tools/taxiroute.get.ts | 357 ++++++++++++---------- 1 file changed, 199 insertions(+), 158 deletions(-) diff --git a/server/api/service/tools/taxiroute.get.ts b/server/api/service/tools/taxiroute.get.ts index 7b7d66c..5792d49 100644 --- a/server/api/service/tools/taxiroute.get.ts +++ b/server/api/service/tools/taxiroute.get.ts @@ -1,180 +1,221 @@ import { defineEventHandler, getQuery } from 'h3' import https from 'node:https' -export default defineEventHandler(async (event) => { - const q = getQuery(event) - const oLat = Number(q.origin_lat), oLon = Number(q.origin_lng) - const dLat = Number(q.dest_lat), dLon = Number(q.dest_lng) - const radius = Number(q.radius ?? 2000) +type LatLon = { lat:number; lon:number } +type NodeRec = { id:number; lat:number; lon:number } +type WayRec = { id:number; nodes:number[]; tags?:Record } - const endpoint = 'https://overpass-api.de/api/interpreter' - const overpassQ = ` +const endpoint = 'https://overpass-api.de/api/interpreter' +const toRad = (d:number)=>d*Math.PI/180 +const haversine = (a:LatLon,b:LatLon)=>{ + const R=6371000,dLat=toRad(b.lat-a.lat),dLon=toRad(b.lon-a.lon),la1=toRad(a.lat),la2=toRad(b.lat) + const s=Math.sin(dLat/2)**2+Math.cos(la1)*Math.cos(la2)*Math.sin(dLon/2)**2 + return 2*R*Math.asin(Math.sqrt(s)) +} + +function parseTarget(s:string){ + const t=s?.trim(); if(!t) return null as const + if(t.startsWith('coord:')) return {kind:'coord', value:t.slice(6).trim()} as const + if(t.startsWith('threshold:')) return {kind:'threshold', value:t.slice(10).trim()} as const + if(t.startsWith('gate:')) return {kind:'gate', value:t.slice(5).trim()} as const + if(t.startsWith('stand:')) return {kind:'stand', value:t.slice(6).trim()} as const + return null as const +} + +function selectorFor(kind:'threshold'|'gate'|'stand', ref:string, alias:string){ + if(kind==='gate') return `node(area.a)["aeroway"="gate"]["ref"="${ref}"]->.${alias};` + if(kind==='stand') return `node(area.a)["aeroway"="parking_position"]["ref"="${ref}"]->.${alias};` + // Threshold robust (versch. Tagging-Schemata) + return ` + ( + node(area.a)["runway"="threshold"]["ref"="${ref}"]; + node(area.a)["aeroway"="runway"]["runway"="threshold"]["ref"="${ref}"]; + node(area.a)["aeroway"="threshold"]["ref"="${ref}"]; + )->.${alias};` +} + +function buildOverpassQuery(airport:string, o:any, d:any, includeRunways=true){ + const net = includeRunways ? '^(taxiway|runway)$' : '^taxiway$' + const selO = o && o.kind!=='coord' ? selectorFor(o.kind, o.value, 'OSEL') : '' + const selD = d && d.kind!=='coord' ? selectorFor(d.kind, d.value, 'DSEL') : '' + return ` [out:json][timeout:90]; -( - way["aeroway"="taxiway"](around:${radius},${oLat},${oLon}); - way["aeroway"="taxiway"](around:${radius},${dLat},${dLon}); -); -(._;>;); -out body; - ` +// Aerodrome → Area +rel["aeroway"="aerodrome"]["icao"="${airport}"]; +map_to_area->.a; - // --- utils --- - const toRad = (d:number)=>d*Math.PI/180 - function haversine(a:{lat:number,lon:number}, b:{lat:number,lon:number}){ - const R=6371000 - const dLat=toRad(b.lat-a.lat), dLon=toRad(b.lon-a.lon) - const la1=toRad(a.lat), la2=toRad(b.lat) - const s=Math.sin(dLat/2)**2 + Math.cos(la1)*Math.cos(la2)*Math.sin(dLon/2)**2 - return 2*R*Math.asin(Math.sqrt(s)) - } - function fetchOverpass(query: string) { - return new Promise((resolve, reject) => { - const req = https.request(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }, res => { - let data = '' - res.on('data', d => (data += d)) - res.on('end', () => { - if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0,200)}`)) - resolve(JSON.parse(data)) - }) - }) - req.on('error', reject) - req.write('data=' + encodeURIComponent(query)) - req.end() - }) - } +// Netz laden +way(area.a)["aeroway"~"${net}"]->.w; +( .w; >; ); out body; - // --- build graph from OSM --- - const osm = await fetchOverpass(overpassQ) +// Features laden (optional) +${selO} +${selD} +${(selO||selD) ? '( .OSEL; .DSEL; ); out body;' : ''} +`.trim() +} - const nodes = new Map() - const ways: Array = [] - for (const el of osm.elements) { - if (el.type === 'node') nodes.set(el.id, { id: el.id, lat: el.lat, lon: el.lon }) - else if (el.type === 'way') ways.push(el) - } - - type Edge = { u:number, v:number, w:number, way_id:number, name:string|null } - const edges: Edge[] = [] - for (const w of ways) { - const name: string | null = w.tags?.name || w.tags?.ref || null - for (let i=0;i((resolve, reject) => { + const req = https.request(endpoint, { + method:'POST', + headers:{'Content-Type':'application/x-www-form-urlencoded'} + }, res => { + let data=''; res.on('data', d => data += d) + res.on('end', () => { + if(res.statusCode!==200){ + return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0,300)}`)) } - } + try{ resolve(JSON.parse(data)) } catch{ reject(new Error('Bad JSON from Overpass')) } + }) + }) + req.on('error', reject) + req.write('data=' + encodeURIComponent(query)) + req.end() + }) +} - // --- adjacency + quick edge lookup --- - const adj = new Map>() - const edgeName = new Map() - for (const e of edges){ - if (!adj.has(e.u)) adj.set(e.u, []) - adj.get(e.u)!.push({ to:e.v, w:e.w }) - edgeName.set(`${e.u}->${e.v}`, { name:e.name, way_id:e.way_id }) - } +export default defineEventHandler(async (event)=>{ + const q=getQuery(event) + const airport=String(q.airport||'EDDF').toUpperCase() + const originRaw=String(q.origin||'').trim() + const destRaw =String(q.dest||'').trim() + const includeRunways = String(q.include_runways||'1')!=='0' - // --- nearest node to a coordinate --- - function nearestNode(lat:number, lon:number){ - let bestId:number|undefined, bestD=Infinity - for (const n of nodes.values()){ - const d = haversine({lat,lon},{lat:n.lat,lon:n.lon}) - if (d() + const ways:WayRec[]=[] + for(const el of osm.elements){ + if(el.type==='node') nodes.set(el.id, {id:el.id, lat:el.lat, lon:el.lon}) + else if(el.type==='way') ways.push({id:el.id, nodes:el.nodes, tags:el.tags}) + } + + type Edge = {u:number; v:number; w:number; way_id:number; name:string|null} + const edges:Edge[]=[] + for(const w of ways){ + const name = w.tags?.ref || w.tags?.name || null + for(let i=0;i>() + const edgeMeta = new Map() + for(const e of edges){ + if(!adj.has(e.u)) adj.set(e.u,[]) + adj.get(e.u)!.push({to:e.v,w:e.w}) + edgeMeta.set(`${e.u}->${e.v}`, {name:e.name, way_id:e.way_id}) + } + + function nearestNode(lat:number, lon:number){ + let best:NodeRec|undefined, dBest=Infinity + for(const n of nodes.values()){ + const d=haversine({lat,lon},{lat:n.lat,lon:n.lon}) + if(d){ + if(!t) return null + if(t.kind==='coord'){ + const [latS,lonS]=t.value.split(',').map(x=>x.trim()) + const lat=Number(latS), lon=Number(lonS); if(!Number.isFinite(lat)||!Number.isFinite(lon)) return null + const attach=nearestNode(lat,lon); if(!attach) return null + return {point:{lat,lon}, attach} + } + const hits=(osm.elements as any[]).filter(e=>{ + if(e.type!=='node') return false + const refOk = e.tags?.ref===t.value + if(t.kind==='gate') return refOk && e.tags?.aeroway==='gate' + if(t.kind==='stand') return refOk && e.tags?.aeroway==='parking_position' + // threshold: mehrere Taggingvarianten + return refOk && (e.tags?.runway==='threshold' || e.tags?.aeroway==='threshold' || + (e.tags?.aeroway==='runway' && e.tags?.runway==='threshold')) + }) + if(!hits.length) return null + const p={lat:hits[0].lat, lon:hits[0].lon} + const attach=nearestNode(p.lat,p.lon); if(!attach) return null + return {point:p, attach} + } + + const oRes=resolvePoint(oT) + const dRes=resolvePoint(dT) + if(!oRes || !dRes){ + return { airport, error:'feature_not_found_or_attach_failed', origin_query:originRaw, dest_query:destRaw } + } + + function dijkstra(src: number, dst: number) { + const dist = new Map() + const prev = new Map() + const done = new Set() + const pq: Array<{ id: number; d: number }> = [] + const push = (id: number, d: number) => { pq.push({ id, d }); pq.sort((a, b) => a.d - b.d) } + + for (const id of nodes.keys()) dist.set(id, Infinity) + dist.set(src, 0); push(src, 0) + + while (pq.length) { + const { id: u } = pq.shift()! + if (done.has(u)) continue + done.add(u) + if (u === dst) break + + const nb = adj.get(u) + if (!nb) continue + + for (const { to: v, w: cost } of nb) { + const alt = dist.get(u)! + cost + if (alt < dist.get(v)!) { + dist.set(v, alt) + prev.set(v, u) + push(v, alt) } - if (bestId===undefined) return null - const nn = nodes.get(bestId)! - return { node_id: bestId, lat: nn.lat, lon: nn.lon, distance_m: bestD } + } } - const startAttach = nearestNode(oLat,oLon) - const endAttach = nearestNode(dLat,dLon) - - if (!startAttach || !endAttach) { - return { error: 'no_nodes_in_area', origin:{lat:oLat,lon:oLon}, dest:{lat:dLat,lon:dLon} } + if (!prev.has(dst) && src !== dst) return null + const path: number[] = [] + let u = dst + path.push(u) + while (u !== src) { + const p = prev.get(u) + if (p === undefined) break + u = p + path.push(u) } + path.reverse() + return { path, total_m: dist.get(dst)! } + } - // --- dijkstra shortest path (meters) --- - function dijkstra(src:number, dst:number){ - const dist = new Map() - const prev = new Map() - const visited = new Set() - const pq: Array<{id:number, d:number}> = [] - const push = (id:number, d:number)=>{ - pq.push({id,d}) - // simple binary heap-free: insertion sort-ish - pq.sort((a,b)=>a.d-b.d) - } - for (const id of nodes.keys()) dist.set(id, Infinity) - dist.set(src, 0); push(src,0) + const sp=dijkstra(oRes.attach.node_id, dRes.attach.node_id) - while (pq.length){ - const {id:u} = pq.shift()! - if (visited.has(u)) continue - visited.add(u) - if (u===dst) break - const lst = adj.get(u); if (!lst) continue - for (const {to:v, w} of lst){ - const alt = dist.get(u)! + w - if (alt < dist.get(v)!){ - dist.set(v, alt) - prev.set(v, u) - push(v, alt) - } - } - } - - if (!prev.has(dst) && src!==dst) return null - const path:number[] = [] - let u = dst - path.push(u) - while (u !== src){ - const p = prev.get(u) - if (p===undefined){ break } - u = p - path.push(u) - } - path.reverse() - const total_m = dist.get(dst)! - return { path, total_m } + const names:string[]=[] + if(sp && sp.path.length>1){ + for(let i=0;i${v}`)||edgeMeta.get(`${v}->${u}`) + const nm=(m?.name||'').trim() + if(nm && (names.length===0 || names[names.length-1]!==nm)) names.push(nm) } + } - const sp = dijkstra(startAttach.node_id, endAttach.node_id) - if (!sp) { - return { - origin: { lat:oLat, lon:oLon }, - dest: { lat:dLat, lon:dLon }, - start_attach: startAttach, - end_attach: endAttach, - route: null, - names: [] - } - } - - // names along path (consecutive compressed, drop null/unnamed) - const namesSeq: string[] = [] - for (let i=0;i${v}`) || edgeName.get(`${v}->${u}`) - const nm = meta?.name?.trim() - if (nm && (namesSeq.length===0 || namesSeq[namesSeq.length-1]!==nm)) namesSeq.push(nm) - } - - return { - origin: { lat:oLat, lon:oLon }, - dest: { lat:dLat, lon:dLon }, - start_attach: startAttach, - end_attach: endAttach, - route: { - node_ids: sp.path, - total_distance_m: sp.total_m - }, - names: namesSeq - } + return { + airport, + origin:{ query:originRaw, point:oRes.point, attach:oRes.attach }, + dest: { query:destRaw, point:dRes.point, attach:dRes.attach }, + route: sp ? { node_ids:sp.path, total_distance_m:sp.total_m } : null, + names + } })