From 6fe19c0ccb0552038b5406813fac1eda0dbe100f Mon Sep 17 00:00:00 2001 From: najajan Date: Sat, 20 Sep 2025 15:41:03 +0200 Subject: [PATCH] fix taxiroute nearest taxiway --- server/api/service/tools/taxiroute.get.ts | 248 ++++++++++------------ 1 file changed, 107 insertions(+), 141 deletions(-) diff --git a/server/api/service/tools/taxiroute.get.ts b/server/api/service/tools/taxiroute.get.ts index 8f75260..0d77a0e 100644 --- a/server/api/service/tools/taxiroute.get.ts +++ b/server/api/service/tools/taxiroute.get.ts @@ -1,12 +1,11 @@ -// call GET: http://localhost:3000/api/service/tools/taxiroute?icao=EDDF&origin_lat=50.046377&origin_lng=8.558253&dest_lat=50.04609&dest_lng=8.57114 - import { defineEventHandler, getQuery } from 'h3' type Node = { id: number; lat: number; lon: number } type Edge = { from: number; to: number; len: number; ref: string; wayId?: number } type Way = { id: number; geometry: Array<{ lat: number; lon: number }>; label: string } +type Segment = { aId: number; bId: number; a: Node; b: Node; ref: string; wayId: number } -const meters = (a:{lat:number;lon:number}, b:{lat:number;lon:number})=>{ +const m = (a:{lat:number;lon:number}, b:{lat:number;lon:number})=>{ const lat0=(a.lat+b.lat)*0.5, kx=Math.cos(lat0*Math.PI/180)*111320, ky=110540 const dx=(b.lon-a.lon)*kx, dy=(b.lat-a.lat)*ky return Math.hypot(dx,dy) @@ -19,211 +18,178 @@ const overpassAround = (lat:number, lon:number, radius:number) => ` ); out tags geom; ` +const buildWays = (els:any[]): Way[] => + (els||[]) + .filter((el:any)=>el.type==='way' && el.geometry?.length>=2) + .map((el:any)=>({ id: el.id, geometry: el.geometry, label: (el.tags?.ref || el.tags?.name || `TWY-${el.id}`) })) -function buildWays(osm:any[]): Way[] { - return (osm||[]) - .filter((el:any)=>el.type==='way' && el.geometry && el.geometry.length>=2) - .map((el:any)=>({ - id: el.id, - geometry: el.geometry, - label: (el.tags?.ref || el.tags?.name || `TWY-${el.id}`) as string - })) -} - -function buildGraph(ways: Way[]) { - const nodes=new Map(), adj=new Map(), nodeRefs=new Map() +function buildGraph(ways: Way[]){ + const nodes=new Map(), adj=new Map(), segments: Segment[]=[] let nid=1 - const push=(e:Edge)=>{ if(!adj.has(e.from)) adj.set(e.from,[]); adj.get(e.from)!.push(e); if(e.ref){ if(!nodeRefs.has(e.from)) nodeRefs.set(e.from,[]); if(!nodeRefs.has(e.to)) nodeRefs.set(e.to,[]); nodeRefs.get(e.from)!.push(e.ref); nodeRefs.get(e.to)!.push(e.ref) } } - for (const w of ways){ - let prev:number|undefined - for (const p of w.geometry){ + const push=(e:Edge)=>{ if(!adj.has(e.from)) adj.set(e.from,[]); adj.get(e.from)!.push(e) } + for(const w of ways){ + let prevId:number|undefined, prev:Node|undefined + for(const p of w.geometry){ const id=nid++, n:Node={id,lat:p.lat,lon:p.lon}; nodes.set(id,n) - if(prev!==undefined){ - const a=nodes.get(prev)!, b=n, len=meters(a,b) - push({from:a.id,to:b.id,len,ref:w.label,wayId:w.id}) - push({from:b.id,to:a.id,len,ref:w.label,wayId:w.id}) + if(prevId && prev){ + const len=m(prev,n) + push({from:prevId,to:id,len,ref:w.label,wayId:w.id}) + push({from:id,to:prevId,len,ref:w.label,wayId:w.id}) + segments.push({aId:prevId,bId:id,a:prev,b:n,ref:w.label,wayId:w.id}) } - prev=id + prevId=id; prev=n } } - return {nodes,adj,nodeRefs,nid} + return { nodes, adj, segments, nextId: Math.max(...nodes.keys())+1 } } function connectClose(nodes:Map, adj:Map, maxDist:number){ if(maxDist<=0) return - const arr=Array.from(nodes.values()), N=arr.length - if(N>8000) return + const arr=Array.from(nodes.values()); const N=arr.length; if(N>9000) return const push=(e:Edge)=>{ if(!adj.has(e.from)) adj.set(e.from,[]); adj.get(e.from)!.push(e) } - for(let i=0;i0 && d<=maxDist){ - push({from:a.id,to:b.id,len:d,ref:'stitch'}) - push({from:b.id,to:a.id,len:d,ref:'stitch'}) - } - } + for(let i=0;i0 && d<=maxDist){ push({from:a.id,to:b.id,len:d,ref:'stitch'}); push({from:b.id,to:a.id,len:d,ref:'stitch'}) } } } -function projectPointToSegment(p:{lat:number;lon:number}, a:{lat:number;lon:number}, b:{lat:number;lon:number}){ +function proj(p:{lat:number;lon:number}, a:Node, b:Node){ const lat0=(a.lat+b.lat)*0.5, kx=Math.cos(lat0*Math.PI/180)*111320, ky=110540 - const ax=0, ay=0 const bx=(b.lon-a.lon)*kx, by=(b.lat-a.lat)*ky const px=(p.lon-a.lon)*kx, py=(p.lat-a.lat)*ky const seg2=bx*bx+by*by - let t = seg2===0?0: (px*bx+py*by)/seg2 - t = Math.max(0, Math.min(1,t)) + let t= seg2===0?0:(px*bx+py*by)/seg2; t=Math.max(0,Math.min(1,t)) const qx=bx*t, qy=by*t - const qlon=a.lon + qx/kx, qlat=a.lat + qy/ky - const dist=Math.hypot(px-qx, py-qy) - return { t, q:{lat:qlat, lon:qlon}, dist } + return { q:{lat:a.lat+qy/ky, lon:a.lon+qx/kx}, dist:Math.hypot(px-qx,py-qy) } } -function snapPointAsVirtualNode(p:{lat:number;lon:number}, nodes:Map, adj:Map, nextId:number){ - let best:any=null - const edges:Edge[]=[] - for (const from of adj.keys()){ - for (const e of adj.get(from)!){ - if (e.from!==from) continue - const A=nodes.get(e.from)!, B=nodes.get(e.to)! - const pr=projectPointToSegment(p, A, B) - if(!best || pr.dist { - for(let i=list.length-1;i>=0;i--){ - const x=list[i]; if(x.from===oldFrom && x.to===oldTo){ list.splice(i,1) } - } - } - replFrom(adj.get(A.id)!, A.id, B.id) - replFrom(adj.get(B.id)!, B.id, A.id) - const push=(ed:Edge)=>{ if(!adj.has(ed.from)) adj.set(ed.from,[]); adj.get(ed.from)!.push(ed) } - push({from:A.id,to:mid.id,len:lenAM,ref:e.ref,wayId:e.wayId}) - push({from:mid.id,to:A.id,len:lenAM,ref:e.ref,wayId:e.wayId}) - push({from:mid.id,to:B.id,len:lenMB,ref:e.ref,wayId:e.wayId}) - push({from:B.id,to:mid.id,len:lenMB,ref:e.ref,wayId:e.wayId}) - push({from:n.id,to:mid.id,len:lenPN,ref:'snap'}) - push({from:mid.id,to:n.id,len:lenPN,ref:'snap'}) - return { virtualId: id, attachId: mid.id, attachLabel: e.ref, attachDist: Math.round(lenPN), nextId: mid.id+1 } - } else { - return { virtualId: id, attachId: null, attachLabel: 'snap', attachDist: null, nextId: id+1 } +function snapToOSMSegment(p:{lat:number;lon:number}, g:{nodes:Map,adj:Map,segments:Segment[],nextId:number}){ + let best:null|{seg:Segment,q:{lat:number;lon:number},dist:number}=null + for(const s of g.segments){ const pr=proj(p,s.a,s.b); if(!best||pr.dist{ if(!g.adj.has(e.from)) g.adj.set(e.from,[]); g.adj.get(e.from)!.push(e) } + const remove=(from:number,to:number)=>{ + const L=g.adj.get(from); if(!L) return + for(let i=L.length-1;i>=0;i--) if(L[i].from===from && L[i].to===to && L[i].ref!=='stitch' && L[i].ref!=='snap') L.splice(i,1) } + const {aId,bId,a,b,ref,wayId}=best.seg + remove(aId,bId); remove(bId,aId) + const lenAM=m(a,mid), lenMB=m(mid,b), lenPM=m(virt,mid) + push({from:aId,to:idMid,len:lenAM,ref,wayId}) + push({from:idMid,to:aId,len:lenAM,ref,wayId}) + push({from:idMid,to:bId,len:lenMB,ref,wayId}) + push({from:bId,to:idMid,len:lenMB,ref,wayId}) + push({from:idVirt,to:idMid,len:lenPM,ref:'snap'}) + push({from:idMid,to:idVirt,len:lenPM,ref:'snap'}) + const idx=g.segments.findIndex(s=>s.aId===aId && s.bId===bId) + if(idx>=0) g.segments.splice(idx,1,{aId, bId:idMid, a, b:mid, ref, wayId},{aId:idMid, bId, a:mid, b, ref, wayId}) + return { virtualId:idVirt, midId:idMid, attachLabel:ref, attachDist:Math.round(lenPM), attachCoord:{lat:mid.lat,lon:mid.lon} } } -function dijkstra(start:number, goal:number, adj:Map) { - const dist=new Map(), prev=new Map(), pq:Array<[number,number]>=[] - const push=(d:number,n:number)=>{ pq.push([d,n]); pq.sort((a,b)=>a[0]-b[0]) } - dist.set(start,0); push(0,start) - while(pq.length){ - const [d,u]=pq.shift()! +function astar(start:number, goal:number, adj:Map, nodes:Map){ + const h=(x:number)=> m(nodes.get(x)!, nodes.get(goal)!) + const open: Array<[number,number]> = [] + const g=new Map(), came=new Map() + const push=(f:number,n:number)=>{ open.push([f,n]); open.sort((a,b)=>a[0]-b[0]) } + g.set(start,0); push(h(start), start) + const seen=new Set() + while(open.length){ + const [,u]=open.shift()! if(u===goal) break - if(d!==dist.get(u)) continue + if(seen.has(u)) continue; seen.add(u) for(const e of (adj.get(u)||[])){ - const v=e.to, nd=d+e.len - if(nd < (dist.get(v) ?? Infinity)){ - dist.set(v, nd); prev.set(v,{node:u,edge:e}); push(nd,v) + const v=e.to, tentative=(g.get(u)??Infinity)+e.len + if(tentative < (g.get(v)??Infinity)){ + g.set(v,tentative); came.set(v,{node:u,edge:e}); push(tentative + h(v), v) } } } - if(!prev.has(goal)) return null - const nodes=[goal], edges:Edge[]=[] - for(let cur=goal; cur!==start; ){ const p=prev.get(cur)!; edges.push(p.edge); nodes.push(p.node); cur=p.node } - nodes.reverse(); edges.reverse() - return { nodes, edges, distance: dist.get(goal)! } + if(!came.has(goal)) return null + const nodesPath=[goal], edges:Edge[]=[] + for(let cur=goal; cur!==start; ){ const p=came.get(cur)!; edges.push(p.edge); nodesPath.push(p.node); cur=p.node } + nodesPath.reverse(); edges.reverse() + return { nodes: nodesPath, edges, distance: Math.round(g.get(goal)!) } } 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=Math.max(1000,Math.min(15000,Number(q.radius)||6000)) - const connectInit=Math.max(0,Math.min(40,Number(q.connect)||12)) - const connectMax=Math.max(connectInit, Math.min(80, Number(q.connect_max)||40)) + const radius=Math.max(1200,Math.min(15000,Number(q.radius)||7000)) + let connect=Math.max(0,Math.min(50,Number(q.connect)||12)) + const connectMax=Math.max(connect, Math.min(80, Number(q.connect_max)||48)) + const connectStep=Math.max(4, Math.min(20, Number(q.connect_step)||8)) - if(!isFinite(oLat)||!isFinite(oLon)||!isFinite(dLat)||!isFinite(dLon)){ + if(!isFinite(oLat)||!isFinite(oLon)||!isFinite(dLat)||!isFinite(dLon)) return { ok:false, message:'params missing', distance_straight_m:null } - } - const straight=Math.round(meters({lat:oLat,lon:oLon},{lat:dLat,lon:dLon})) + const straight=Math.round(m({lat:oLat,lon:oLon},{lat:dLat,lon:dLon})) - const req1 = fetch('https://overpass-api.de/api/interpreter',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'data='+encodeURIComponent(overpassAround(oLat,oLon,radius))}) - const req2 = fetch('https://overpass-api.de/api/interpreter',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'data='+encodeURIComponent(overpassAround(dLat,dLon,radius))}) - const [r1,r2]=await Promise.all([req1,req2]) + const [r1,r2]=await Promise.all([ + fetch('https://overpass-api.de/api/interpreter',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'data='+encodeURIComponent(overpassAround(oLat,oLon,radius))}), + fetch('https://overpass-api.de/api/interpreter',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'data='+encodeURIComponent(overpassAround(dLat,dLon,radius))}) + ]) if(!r1.ok||!r2.ok){ - const txt1= r1.ok? '' : await r1.text().catch(()=>''), txt2= r2.ok? '' : await r2.text().catch(()=> '') - return { ok:false, message:`overpass ${r1.status}/${r2.status}`, distance_straight_m: straight, error: (txt1||txt2) } - } - const j1=await r1.json(), j2=await r2.json() - const ways = [...buildWays(j1.elements), ...buildWays(j2.elements)] - if(!ways.length){ - return { ok:false, message:'no taxiways found', distance_straight_m: straight, nodes_with_distance: [] } + const t1=r1.ok?'':await r1.text().catch(()=>''), t2=r2.ok?'':await r2.text().catch(()=>'') + + return { ok:false, message:`overpass ${r1.status}/${r2.status}`, distance_straight_m: straight, error: (t1||t2) } } + const ways=[...buildWays((await r1.json()).elements), ...buildWays((await r2.json()).elements)] + if(!ways.length) return { ok:false, message:'no taxiways found', distance_straight_m: straight, nodes_with_distance: [] } - const {nodes,adj,nodeRefs,nid}=buildGraph(ways) - let nextId=nid + const g=buildGraph(ways) + const s=snapToOSMSegment({lat:oLat,lon:oLon}, g) + const d=snapToOSMSegment({lat:dLat,lon:dLon}, g) - let startInfo = snapPointAsVirtualNode({lat:oLat,lon:oLon}, nodes, adj, nextId); nextId=startInfo.nextId - let destInfo = snapPointAsVirtualNode({lat:dLat,lon:dLon}, nodes, adj, nextId); nextId=destInfo.nextId - - let connect=connectInit, path=null + let path=null while(connect<=connectMax && !path){ - connectClose(nodes, adj, connect) - path=dijkstra(startInfo.virtualId, destInfo.virtualId, adj) - if(!path) connect += 6 + connectClose(g.nodes, g.adj, connect) + path=astar(s.virtualId!, d.virtualId!, g.adj, g.nodes) + if(!path) connect += connectStep } + + const nodes_with_distance = Array.from(g.nodes.values()).map(n=>({ + id:n.id, lat:n.lat, lon:n.lon, + label: (g.adj.get(n.id)?.[0]?.ref) || 'stitch/snap', + d_from_start_m: Math.round(m({lat:oLat,lon:oLon}, n)), + d_from_dest_m: Math.round(m({lat:dLat,lon:dLon}, n)), + })) + if(!path){ - const nodes_with_distance = Array.from(nodes.values()).map(n=>({ - id:n.id, lat:n.lat, lon:n.lon, - label:(nodeRefs.get(n.id)||[]).sort((a,b)=>0).length? (nodeRefs.get(n.id)![0]) : 'stitch/snap', - d_from_start_m: Math.round(meters({lat:oLat,lon:oLon}, n)), - d_from_dest_m: Math.round(meters({lat:dLat,lon:dLon}, n)), - })) return { ok:false, - message:'no path found (after segment-snap/adaptive-connect)', + message:'no path found (after segment-snap/A*)', distance_straight_m: straight, chosen: { - start_attach: { attach_node: startInfo.attachId, label: startInfo.attachLabel, distance_m: startInfo.attachDist }, - dest_attach: { attach_node: destInfo.attachId, label: destInfo.attachLabel, distance_m: destInfo.attachDist } + start_attach: { label: s.attachLabel, distance_m: s.attachDist, coord: s.attachCoord }, + dest_attach: { label: d.attachLabel, distance_m: d.attachDist, coord: d.attachCoord } }, nodes_with_distance } } - const coords = path.nodes.map(id=>{ const n=nodes.get(id)!; return [n.lon,n.lat] as [number,number] }) + const coords = path.nodes.map(id=>{ const n=g.nodes.get(id)!; return [n.lon,n.lat] as [number,number] }) const steps: Array<{ref:string; length_m:number}> = [] - let i=0 - while(i (idx===0?'Taxi':'Continue on') + ` ${s.ref} for ${s.length_m} m`) - const nodes_with_distance = Array.from(nodes.values()).map(n=>({ - id:n.id, lat:n.lat, lon:n.lon, - label:( (nodeRefs.get(n.id)||[])[0] ) || 'stitch/snap', - d_from_start_m: Math.round(meters({lat:oLat,lon:oLon}, n)), - d_from_dest_m: Math.round(meters({lat:dLat,lon:dLon}, n)), - })) return { ok:true, message:'route found', distance_straight_m: straight, chosen: { - start_attach: { attach_node: startInfo.attachId, label: startInfo.attachLabel, distance_m: startInfo.attachDist }, - dest_attach: { attach_node: destInfo.attachId, label: destInfo.attachLabel, distance_m: destInfo.attachDist } + start_attach: { label: s.attachLabel, distance_m: s.attachDist, coord: s.attachCoord }, + dest_attach: { label: d.attachLabel, distance_m: d.attachDist, coord: d.attachCoord } }, - distance_m: Math.round(path.distance), + distance_m: path.distance, steps, instructions, nodes_with_distance, - path: { type:'Feature', properties:{ distance_m: Math.round(path.distance) }, geometry:{ type:'LineString', coordinates: coords } } + path: { type:'Feature', properties:{ distance_m: path.distance }, geometry:{ type:'LineString', coordinates: coords } } } })