mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-07 10:05:50 +08:00
fix double taxiroutes between nodes, sanatise route for using less different named taxi routes
This commit is contained in:
@@ -1,31 +1,49 @@
|
||||
import { defineEventHandler, getQuery } from 'h3'
|
||||
import https from 'node:https'
|
||||
|
||||
type LatLon = { lat:number; lon:number }
|
||||
type NodeRec = { id:number; lat:number; lon:number }
|
||||
type WayRec = { id:number; nodes:number[]; tags?:Record<string,string> }
|
||||
/**
|
||||
* Taxi-Route API (EDDF o.ä.)
|
||||
* - Overpass: Taxiway/optional Runway Netz aus Aerodrome-Area
|
||||
* - Start/Ziel: coord:, threshold:, gate:, stand: → Punkt
|
||||
* - Snap auf nächsten Netz-Knoten
|
||||
* - Kürzeste Route (Dijkstra)
|
||||
* - Label-Segmente mit Distanzen
|
||||
* - A-B-A → A, falls Label-Only-Verbindung existiert (mit Distanz)
|
||||
*/
|
||||
|
||||
type LatLon = { lat: number; lon: number }
|
||||
type NodeRec = { id: number; lat: number; lon: number }
|
||||
type WayRec = { id: number; nodes: number[]; tags?: Record<string, string> }
|
||||
|
||||
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))
|
||||
|
||||
const toRad = (d: number) => d * Math.PI / 180
|
||||
const haversine = (a: LatLon, b: LatLon) => {
|
||||
const R = 6371000
|
||||
const dLat = toRad(b.lat - a.lat)
|
||||
const 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 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
|
||||
// Label normalisieren, z.B. " n 3 " → "N3"
|
||||
const normalizeLabel = (s?: string | null) => (s ?? '').trim().replace(/\s+/g, '').toUpperCase()
|
||||
|
||||
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. Taggingvarianten)
|
||||
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
|
||||
return `
|
||||
(
|
||||
node(area.a)["runway"="threshold"]["ref"="${ref}"];
|
||||
@@ -34,39 +52,37 @@ function selectorFor(kind:'threshold'|'gate'|'stand', ref:string, alias:string){
|
||||
)->.${alias};`
|
||||
}
|
||||
|
||||
function buildOverpassQuery(airport:string, o:any, d:any, includeRunways=true){
|
||||
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') : ''
|
||||
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];
|
||||
// Aerodrome → Area
|
||||
rel["aeroway"="aerodrome"]["icao"="${airport}"];
|
||||
map_to_area->.a;
|
||||
|
||||
// Netz (nur Linien)
|
||||
way(area.a)["aeroway"~"${net}"]->.w;
|
||||
( .w; >; ); out body;
|
||||
|
||||
// Features (Gate/Stand/Threshold)
|
||||
${selO}
|
||||
${selD}
|
||||
${(selO||selD) ? '( .OSEL; .DSEL; ); out body;' : ''}
|
||||
${(selO || selD) ? '( .OSEL; .DSEL; ); out body;' : ''}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
function fetchOverpass(query:string){
|
||||
function fetchOverpass(query: string) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
const req = https.request(endpoint, {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded'}
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}, res => {
|
||||
let data=''; res.on('data', d => data += d)
|
||||
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)}`))
|
||||
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')) }
|
||||
try { resolve(JSON.parse(data)) } catch { reject(new Error('Bad JSON from Overpass')) }
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
@@ -75,176 +91,366 @@ function fetchOverpass(query:string){
|
||||
})
|
||||
}
|
||||
|
||||
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||'0')!=='0'
|
||||
// ----- Util: Label-Parsing aus Way-Tags -----
|
||||
function parseLabelsFromTags(tags?: Record<string, string>): string[] {
|
||||
if (!tags) return []
|
||||
const raw: string[] = []
|
||||
if (tags.ref) raw.push(tags.ref)
|
||||
if (tags.name && tags.name !== tags.ref) raw.push(tags.name)
|
||||
const out = new Set<string>()
|
||||
for (const r of raw) {
|
||||
for (const t of r.split(/[;,\s/]+/)) {
|
||||
const lab = normalizeLabel(t)
|
||||
if (lab) out.add(lab)
|
||||
}
|
||||
}
|
||||
return Array.from(out)
|
||||
}
|
||||
|
||||
// Für Segmentwahl: „schönes“ Label bestimmen
|
||||
function pickCanonicalLabel(cands: Set<string>, prefer?: string): string {
|
||||
if (prefer && cands.has(prefer)) return prefer
|
||||
const arr = Array.from(cands)
|
||||
// Bevorzuge A, A1, A12 Muster
|
||||
const fmt = arr.filter(x => /^[A-Z]\d{0,2}$/.test(x))
|
||||
if (fmt.length) return fmt.sort()[0]
|
||||
return arr.sort()[0] ?? ''
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
const oT = parseTarget(originRaw)
|
||||
const dT = parseTarget(destRaw)
|
||||
|
||||
const osm = await fetchOverpass(buildOverpassQuery(airport, oT, dT, includeRunways))
|
||||
const overpassQ = buildOverpassQuery(airport, oT, dT, includeRunways)
|
||||
const osm = await fetchOverpass(overpassQ)
|
||||
|
||||
const nodes = new Map<number,NodeRec>()
|
||||
const ways:WayRec[]=[]
|
||||
const featureNodes: Array<{id:number,lat:number,lon:number,tags?:any}> = []
|
||||
for(const el of osm.elements){
|
||||
if(el.type==='node'){
|
||||
nodes.set(el.id, {id:el.id, lat:el.lat, lon:el.lon})
|
||||
if(el.tags?.aeroway || el.tags?.runway) featureNodes.push(el)
|
||||
} else if(el.type==='way'){
|
||||
ways.push({id:el.id, nodes:el.nodes, tags:el.tags})
|
||||
// --- Graph ---
|
||||
const nodes = new Map<number, NodeRec>()
|
||||
const ways: WayRec[] = []
|
||||
const featureNodes: Array<{ id: number, lat: number, lon: number, tags?: any }> = []
|
||||
|
||||
for (const el of osm.elements as any[]) {
|
||||
if (el.type === 'node') {
|
||||
nodes.set(el.id, { id: el.id, lat: el.lat, lon: el.lon })
|
||||
if (el.tags?.aeroway || el.tags?.runway) featureNodes.push(el)
|
||||
} 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[]=[]
|
||||
type Edge = { u: number; v: number; w: number; way_id: number }
|
||||
|
||||
const edges: Edge[] = []
|
||||
const wayNodeIds = new Set<number>()
|
||||
for(const w of ways){
|
||||
const name = w.tags?.ref || w.tags?.name || null
|
||||
for(let i=0;i<w.nodes.length-1;i++){
|
||||
const aId=w.nodes[i], bId=w.nodes[i+1]
|
||||
const a=nodes.get(aId), b=nodes.get(bId); if(!a||!b) continue
|
||||
|
||||
// Label-Speicher:
|
||||
// - edgeLabels: u->v → Menge Labels (vereint alle parallelen Ways)
|
||||
// - edgeLabelCost: Label → (u->v → minimaler w für Kanten mit diesem Label)
|
||||
const edgeLabels = new Map<string, Set<string>>()
|
||||
const edgeLabelCost = new Map<string, Map<string, number>>()
|
||||
|
||||
const addEdgeLabels = (u: number, v: number, labels: string[], w: number) => {
|
||||
const key = `${u}->${v}`
|
||||
if (!edgeLabels.has(key)) edgeLabels.set(key, new Set<string>())
|
||||
const set = edgeLabels.get(key)!
|
||||
for (const L of labels) set.add(L)
|
||||
|
||||
for (const L of labels) {
|
||||
if (!edgeLabelCost.has(L)) edgeLabelCost.set(L, new Map())
|
||||
const m = edgeLabelCost.get(L)!
|
||||
const prev = m.get(key)
|
||||
if (prev === undefined || w < prev) m.set(key, w)
|
||||
}
|
||||
}
|
||||
|
||||
for (const w of ways) {
|
||||
const labelsForWay = parseLabelsFromTags(w.tags)
|
||||
for (let i = 0; i < w.nodes.length - 1; i++) {
|
||||
const aId = w.nodes[i], bId = w.nodes[i + 1]
|
||||
const a = nodes.get(aId), b = nodes.get(bId)
|
||||
if (!a || !b) continue
|
||||
wayNodeIds.add(aId); wayNodeIds.add(bId)
|
||||
const dist=haversine({lat:a.lat,lon:a.lon},{lat:b.lat,lon:b.lon})
|
||||
edges.push({u:a.id,v:b.id,w:dist,way_id:w.id,name})
|
||||
edges.push({u:b.id,v:a.id,w:dist,way_id:w.id,name})
|
||||
const dist = haversine({ lat: a.lat, lon: a.lon }, { lat: b.lat, lon: b.lon })
|
||||
edges.push({ u: a.id, v: b.id, w: dist, way_id: w.id })
|
||||
edges.push({ u: b.id, v: a.id, w: dist, way_id: w.id })
|
||||
// Label-Mengen pro Richtung pflegen
|
||||
if (labelsForWay.length) {
|
||||
addEdgeLabels(a.id, b.id, labelsForWay, dist)
|
||||
addEdgeLabels(b.id, a.id, labelsForWay, dist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const adj = new Map<number, Array<{to:number,w:number}>>()
|
||||
const edgeMeta = new Map<string,{name:string|null, way_id:number}>()
|
||||
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})
|
||||
const adj = new Map<number, Array<{ to: number, w: number }>>()
|
||||
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 })
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function nearestNode(lat:number, lon:number, onlyNet=false){
|
||||
let best:NodeRec|undefined, dBest=Infinity
|
||||
for(const n of nodes.values()){
|
||||
if(onlyNet && !wayNodeIds.has(n.id)) continue
|
||||
const d=haversine({lat,lon},{lat:n.lat,lon:n.lon})
|
||||
if(d<dBest){ dBest=d; best=n }
|
||||
// --- Snap ---
|
||||
function nearestNode(lat: number, lon: number, onlyNet = false) {
|
||||
let best: NodeRec | undefined, dBest = Infinity
|
||||
for (const n of nodes.values()) {
|
||||
if (onlyNet && !wayNodeIds.has(n.id)) continue
|
||||
const d = haversine({ lat, lon }, { lat: n.lat, lon: n.lon })
|
||||
if (d < dBest) { dBest = d; best = n }
|
||||
}
|
||||
if(!best) return null
|
||||
return {node_id:best.id, lat:best.lat, lon:best.lon, distance_m:dBest}
|
||||
}
|
||||
function nearestCandidates(lat:number, lon:number, k=5, onlyNet=true){
|
||||
const arr: Array<{node_id:number, lat:number, lon:number, distance_m:number}> = []
|
||||
for(const n of nodes.values()){
|
||||
if(onlyNet && !wayNodeIds.has(n.id)) continue
|
||||
arr.push({node_id:n.id, lat:n.lat, lon:n.lon, distance_m:haversine({lat,lon},{lat:n.lat,lon:n.lon})})
|
||||
}
|
||||
arr.sort((a,b)=>a.distance_m-b.distance_m)
|
||||
return arr.slice(0,k)
|
||||
if (!best) return null
|
||||
return { node_id: best.id, lat: best.lat, lon: best.lon, distance_m: dBest }
|
||||
}
|
||||
|
||||
function resolvePoint(t:ReturnType<typeof parseTarget>){
|
||||
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 raw = nearestNode(lat,lon,false)
|
||||
const net = nearestNode(lat,lon,true)
|
||||
return {point:{lat,lon}, attach_raw:raw, attach_net:net}
|
||||
function nearestCandidates(lat: number, lon: number, k = 5, onlyNet = true) {
|
||||
const arr: Array<{ node_id: number, lat: number, lon: number, distance_m: number }> = []
|
||||
for (const n of nodes.values()) {
|
||||
if (onlyNet && !wayNodeIds.has(n.id)) continue
|
||||
arr.push({ node_id: n.id, lat: n.lat, lon: n.lon, distance_m: haversine({ lat, lon }, { lat: n.lat, lon: n.lon }) })
|
||||
}
|
||||
// Feature lookup aus Overpass-Antwort
|
||||
const hits = featureNodes.filter(e=>{
|
||||
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'
|
||||
return refOk && (e.tags?.runway==='threshold' || e.tags?.aeroway==='threshold' ||
|
||||
(e.tags?.aeroway==='runway' && e.tags?.runway==='threshold'))
|
||||
arr.sort((a, b) => a.distance_m - b.distance_m)
|
||||
return arr.slice(0, k)
|
||||
}
|
||||
|
||||
function resolvePoint(t: ReturnType<typeof parseTarget>) {
|
||||
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 raw = nearestNode(lat, lon, false)
|
||||
const net = nearestNode(lat, lon, true)
|
||||
return { point: { lat, lon }, attach_raw: raw, attach_net: net }
|
||||
}
|
||||
// Feature aus Overpass
|
||||
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
|
||||
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 raw = nearestNode(p.lat,p.lon,false)
|
||||
const net = nearestNode(p.lat,p.lon,true)
|
||||
return {point:p, attach_raw:raw, attach_net:net}
|
||||
if (!hits.length) return null
|
||||
const p = { lat: hits[0].lat, lon: hits[0].lon }
|
||||
const raw = nearestNode(p.lat, p.lon, false)
|
||||
const net = nearestNode(p.lat, p.lon, true)
|
||||
return { point: p, attach_raw: raw, attach_net: net }
|
||||
}
|
||||
|
||||
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 }
|
||||
const oTRes = resolvePoint(oT)
|
||||
const dTRes = resolvePoint(dT)
|
||||
if (!oTRes || !dTRes) {
|
||||
return { airport, error: 'feature_not_found_or_attach_failed', origin_query: originRaw, dest_query: destRaw }
|
||||
}
|
||||
|
||||
// Start/End an NETZ-Knoten binden (falls raw-Knoten nicht im Netz hängt)
|
||||
const oAttach = (oRes.attach_raw && wayNodeIds.has(oRes.attach_raw.node_id)) ? oRes.attach_raw : oRes.attach_net
|
||||
const dAttach = (dRes.attach_raw && wayNodeIds.has(dRes.attach_raw.node_id)) ? dRes.attach_raw : dRes.attach_net
|
||||
const oAttach = (oTRes.attach_raw && wayNodeIds.has(oTRes.attach_raw.node_id)) ? oTRes.attach_raw : oTRes.attach_net
|
||||
const dAttach = (dTRes.attach_raw && wayNodeIds.has(dTRes.attach_raw.node_id)) ? dTRes.attach_raw : dTRes.attach_net
|
||||
|
||||
function dijkstra(src:number, dst:number){
|
||||
const dist=new Map<number,number>(), prev=new Map<number,number>(), done=new Set<number>()
|
||||
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 wayNodeIds) dist.set(id,Infinity)
|
||||
if(!dist.has(src) || !dist.has(dst)) return null
|
||||
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
|
||||
// --- Dijkstra (Gesamtnetz) ---
|
||||
function dijkstra(src: number, dst: number) {
|
||||
const dist = new Map<number, number>()
|
||||
const prev = new Map<number, number>()
|
||||
const done = new Set<number>()
|
||||
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 wayNodeIds) dist.set(id, Infinity)
|
||||
if (!dist.has(src) || !dist.has(dst)) return null
|
||||
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(!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) }
|
||||
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)! }
|
||||
}
|
||||
|
||||
const sp = (oAttach && dAttach) ? dijkstra(oAttach.node_id, dAttach.node_id) : null
|
||||
|
||||
const names:string[]=[]
|
||||
if(sp && sp.path.length>1){
|
||||
for(let i=0;i<sp.path.length-1;i++){
|
||||
const u=sp.path[i], v=sp.path[i+1]
|
||||
const m=edgeMeta.get(`${u}->${v}`)||edgeMeta.get(`${v}->${u}`)
|
||||
const nm=(m?.name||'').trim()
|
||||
if(nm && (names.length===0 || names[names.length-1]!==nm)) names.push(nm)
|
||||
}
|
||||
// --- Segmente (contiguous je Label) ---
|
||||
type Seg = {
|
||||
name: string // gewählte Canonical-Anzeige
|
||||
label: string // normalisiert
|
||||
distance_m: number
|
||||
from: number; to: number
|
||||
start_idx: number; end_idx: number
|
||||
}
|
||||
|
||||
const segments: Seg[] = []
|
||||
if (sp && sp.path.length > 1) {
|
||||
let cur: Seg | null = null
|
||||
for (let i = 0; i < sp.path.length - 1; i++) {
|
||||
const u = sp.path[i], v = sp.path[i + 1]
|
||||
const key = `${u}->${v}`
|
||||
const rev = `${v}->${u}`
|
||||
const labelsSet: Set<string> = edgeLabels.get(key) || edgeLabels.get(rev) || new Set()
|
||||
const chosen = pickCanonicalLabel(labelsSet, cur?.label)
|
||||
const w = haversine(nodes.get(u)!, nodes.get(v)!)
|
||||
if (!chosen) {
|
||||
if (cur) { cur.end_idx = i + 1; cur.to = v }
|
||||
continue
|
||||
}
|
||||
if (!cur || cur.label !== chosen) {
|
||||
if (cur) segments.push(cur)
|
||||
cur = { name: chosen, label: chosen, distance_m: 0, from: u, to: v, start_idx: i, end_idx: i + 1 }
|
||||
} else {
|
||||
cur.to = v
|
||||
cur.end_idx = i + 1
|
||||
}
|
||||
cur.distance_m += w
|
||||
}
|
||||
if (cur) segments.push(cur)
|
||||
}
|
||||
|
||||
// --- Label-Dijkstra: nur Kanten, die Label tragen ---
|
||||
function dijkstraWithinLabel(label: string, starts: number[], targets: Set<number>) {
|
||||
const dist = new Map<number, number>()
|
||||
const done = new Set<number>()
|
||||
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 wayNodeIds) dist.set(id, Infinity)
|
||||
for (const s of starts) if (dist.has(s)) { dist.set(s, 0); push(s, 0) }
|
||||
|
||||
while (pq.length) {
|
||||
const { id: u } = pq.shift()!
|
||||
if (done.has(u)) continue
|
||||
done.add(u)
|
||||
if (targets.has(u)) return { reachable: true, distance_m: dist.get(u)! }
|
||||
|
||||
const nb = adj.get(u); if (!nb) continue
|
||||
for (const { to: v } of nb) {
|
||||
const key = `${u}->${v}`
|
||||
const m = edgeLabelCost.get(label)
|
||||
const cost = m?.get(key)
|
||||
if (cost === undefined) continue // diese Kante trägt das Label nicht
|
||||
const alt = dist.get(u)! + cost
|
||||
if (alt < dist.get(v)!) { dist.set(v, alt); push(v, alt) }
|
||||
}
|
||||
}
|
||||
return { reachable: false, distance_m: null as unknown as number }
|
||||
}
|
||||
|
||||
// --- A-B-A → A Prüfung & Sanitize ---
|
||||
type PatternCheck = {
|
||||
i: number,
|
||||
a: string, a_label: string,
|
||||
b: string, b_label: string,
|
||||
connectable: boolean,
|
||||
label_path_m: number | null,
|
||||
from_nodes: number[], to_nodes: number[]
|
||||
}
|
||||
const pattern_checks: PatternCheck[] = []
|
||||
|
||||
function sanitizeABA(input: Seg[]) {
|
||||
const out: Seg[] = []
|
||||
let i = 0
|
||||
while (i < input.length) {
|
||||
if (i + 2 < input.length) {
|
||||
const A1 = input[i], B = input[i + 1], A2 = input[i + 2]
|
||||
if (A1.label === A2.label && B.label !== A1.label) {
|
||||
const a1Nodes = sp!.path.slice(A1.start_idx, A1.end_idx + 1)
|
||||
const a2Nodes = sp!.path.slice(A2.start_idx, A2.end_idx + 1)
|
||||
const r = dijkstraWithinLabel(A1.label, a1Nodes, new Set(a2Nodes))
|
||||
pattern_checks.push({
|
||||
i,
|
||||
a: A1.name, a_label: A1.label,
|
||||
b: B.name, b_label: B.label,
|
||||
connectable: r.reachable,
|
||||
label_path_m: r.reachable ? Math.round(r.distance_m) : null,
|
||||
from_nodes: a1Nodes, to_nodes: a2Nodes
|
||||
})
|
||||
if (r.reachable) {
|
||||
out.push({
|
||||
name: A1.name,
|
||||
label: A1.label,
|
||||
distance_m: A1.distance_m + B.distance_m + A2.distance_m,
|
||||
from: A1.from,
|
||||
to: A2.to,
|
||||
start_idx: A1.start_idx,
|
||||
end_idx: A2.end_idx
|
||||
})
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(input[i]); i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const segments_sanitized = sanitizeABA(segments)
|
||||
|
||||
// Backwards-kompatibel + Distanzen
|
||||
const names = segments.map(s => s.name)
|
||||
const names_sanitized = segments_sanitized.map(s => s.name)
|
||||
const names_segments = segments.map(s => ({
|
||||
name: s.name, label: s.label, distance_m: Math.round(s.distance_m), from: s.from, to: s.to
|
||||
}))
|
||||
const names_segments_sanitized = segments_sanitized.map(s => ({
|
||||
name: s.name, label: s.label, distance_m: Math.round(s.distance_m), from: s.from, to: s.to
|
||||
}))
|
||||
|
||||
// Debug
|
||||
const deg0 = [...nodes.keys()].filter(id => !adj.has(id))
|
||||
const netDeg0 = [...wayNodeIds].filter(id => !adj.has(id))
|
||||
const originCandidates = nearestCandidates(oRes.point.lat, oRes.point.lon, 5, true)
|
||||
const destCandidates = nearestCandidates(dRes.point.lat, dRes.point.lon, 5, true)
|
||||
const originCandidates = nearestCandidates(oTRes.point.lat, oTRes.point.lon, 5, true)
|
||||
const destCandidates = nearestCandidates(dTRes.point.lat, dTRes.point.lon, 5, true)
|
||||
|
||||
return {
|
||||
airport,
|
||||
origin: {
|
||||
query: originRaw,
|
||||
point: oRes.point,
|
||||
attach_raw: oRes.attach_raw, // evtl. Feature-Knoten (kann "isolated" sein)
|
||||
attach_net: oAttach, // Snap auf Netz-Knoten
|
||||
net_candidates_top5: originCandidates // Debug
|
||||
point: oTRes.point,
|
||||
attach_raw: oTRes.attach_raw,
|
||||
attach_net: oAttach,
|
||||
net_candidates_top5: originCandidates
|
||||
},
|
||||
dest: {
|
||||
query: destRaw,
|
||||
point: dRes.point,
|
||||
attach_raw: dRes.attach_raw,
|
||||
point: dTRes.point,
|
||||
attach_raw: dTRes.attach_raw,
|
||||
attach_net: dAttach,
|
||||
net_candidates_top5: destCandidates
|
||||
},
|
||||
route: sp ? { node_ids: sp.path, total_distance_m: sp.total_m } : null,
|
||||
route: sp ? { node_ids: sp.path, total_distance_m: Math.round(sp.total_m) } : null,
|
||||
|
||||
names_segments,
|
||||
names_segments_sanitized,
|
||||
names,
|
||||
names_sanitized,
|
||||
pattern_checks,
|
||||
|
||||
debug: {
|
||||
overpass_bytes: JSON.stringify(osm).length,
|
||||
stats: {
|
||||
nodes_total: nodes.size,
|
||||
ways_total: ways.length,
|
||||
edges_total: edges.length,
|
||||
net_nodes: wayNodeIds.size,
|
||||
isolated_nodes_total: deg0.length,
|
||||
net_isolated_nodes: netDeg0.length
|
||||
},
|
||||
attach_ok: Boolean(sp),
|
||||
note: 'attach_raw kann außerhalb des Netzgraphs liegen; attach_net ist immer ein Way-Knoten.'
|
||||
net_nodes: wayNodeIds.size
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user