1(()=>{'use strict';const LEAFLET_CSS=['https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.css','https://unpkg.com/[email protected]/dist/leaflet.css','https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css',];const LEAFLET_JS=['https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.js','https://unpkg.com/[email protected]/dist/leaflet.js','https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js',];const AIR_SOURCES=['https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat','https://cdn.jsdelivr.net/gh/jpatokal/openflights@master/data/airports.dat','https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv','https://cdn.jsdelivr.net/gh/davidmegginson/ourairports-data@main/airports.csv',];const NOMINATIM_ENDPOINT='https://nominatim.openstreetmap.org/search';function loadStyleOnce(href,ms=8000){return new Promise((resolve,reject)=>{if([...document.styleSheets].some(s=>s.href&&s.href.includes('leaflet'))) 2return resolve();const l=document.createElement('link');l.rel='stylesheet';l.href=href;l.crossOrigin='anonymous';const t=setTimeout(()=>reject(new Error('style timeout '+href)),ms);l.onload=()=>{clearTimeout(t);resolve()};l.onerror=()=>{clearTimeout(t);reject(new Error('style error '+href))};document.head.appendChild(l)})} 3function loadScriptOnce(src,ms=8000){return new Promise((resolve,reject)=>{if(window.L)return resolve();const s=document.createElement('script');s.src=src;s.defer=!0;s.crossOrigin='anonymous';const t=setTimeout(()=>reject(new Error('script timeout '+src)),ms);s.onload=()=>{clearTimeout(t);resolve()};s.onerror=()=>{clearTimeout(t);reject(new Error('script error '+src))};document.head.appendChild(s)})} 4async function loadAny(urls,loader){for(const u of urls){try{await loader(u);return!0}catch(e){}} 5throw new Error('Alle Quellen fehlgeschlagen:\n'+urls.join('\n'))} 6const sleep=ms=>new Promise(r=>setTimeout(r,ms));const clean=v=>String(v||'').trim();const splitCSV=s=>clean(s).split(',').map(x=>x.trim()).filter(Boolean);function tryCoords(term){const m=String(term||'').trim().match(/^\s*(-?\d{1,3}(?:\.\d+)?)\s*[,;]\s*(-?\d{1,3}(?:\.\d+)?)\s*$/);if(!m)return null;const lat=+m[1],lon=+m[2];if(Math.abs(lat)>90||Math.abs(lon)>180)return null;return{lat,lon,name:term}} 7const LS_KEY_AIR='pk_air_iata_v5';const LS_KEY_CITY='pk_city_geo_v1';const LS_TTL_MS=1000*60*60*24*60;function lsRead(key){try{const obj=JSON.parse(localStorage.getItem(key)||'{}');if(!obj.t||(Date.now()-obj.t)>LS_TTL_MS)return{t:Date.now(),data:{}};return obj}catch(e){return{t:Date.now(),data:{}}}} 8function lsWrite(key,data){try{localStorage.setItem(key,JSON.stringify({t:Date.now(),data}))}catch(e){}} 9async function fetchText(url,ms=12000){const ctrl=new AbortController();const to=setTimeout(()=>ctrl.abort(),ms);try{const r=await fetch(url,{signal:ctrl.signal,mode:'cors',cache:'no-store'});if(!r.ok)throw new Error('HTTP '+r.status);return await r.text()}finally{clearTimeout(to)}} 10async function fetchJSON(url,ms=12000){const ctrl=new AbortController();const to=setTimeout(()=>ctrl.abort(),ms);try{const r=await fetch(url,{signal:ctrl.signal,mode:'cors',cache:'no-store',headers:{'Accept':'application/json','User-Agent':'preis-king-route/1.0 (+https://www.preis-king.com)'}});if(!r.ok)throw new Error('HTTP '+r.status);return await r.json()}finally{clearTimeout(to)}} 11function parseOpenFlights(text,wantedSet){const out={};const lines=text.split(/\r?\n/);for(const line of lines){if(!line)continue;const cells=[];let cur='',inQ=!1;for(let i=0;i<line.length;i++){const ch=line[i];if(ch==='"'){inQ=!inQ;continue} 12if(ch===','&&!inQ){cells.push(cur);cur='';continue} 13cur+=ch} 14cells.push(cur);if(cells.length<8)continue;const iata=clean(cells[4]);if(!iata||iata==='\\N')continue;if(wantedSet&&!wantedSet.has(iata))continue;const lat=parseFloat(cells[6]);const lon=parseFloat(cells[7]);if(!isFinite(lat)||!isFinite(lon))continue;out[iata]={lat,lon,name:cells[1]||iata,city:cells[2]||'',country:cells[3]||'',code:iata}} 15return out} 16function parseOurAirports(text,wantedSet){const out={};const lines=text.split(/\r?\n/);const hdr=lines.shift();if(!hdr)return out;const idx={};hdr.split(',').forEach((h,i)=>idx[h]=i);for(const line of lines){if(!line)continue;const cells=line.split(',');const iata=clean(cells[idx.iata_code]);if(!iata)continue;if(wantedSet&&!wantedSet.has(iata))continue;const lat=parseFloat(cells[idx.latitude_deg]);const lon=parseFloat(cells[idx.longitude_deg]);if(!isFinite(lat)||!isFinite(lon))continue;out[iata]={lat,lon,name:cells[idx.name]||iata,city:cells[idx.municipality]||'',country:cells[idx.iso_country]||'',code:iata}} 17return out} 18async function loadAirSubset(wanted){const store=lsRead(LS_KEY_AIR);const have=store.data||{};const missing=[...wanted].filter(c=>!have[c]);if(!missing.length)return have;for(const src of AIR_SOURCES){try{const txt=await fetchText(src);const parsed=src.endsWith('.csv')?parseOurAirports(txt,new Set(missing)):parseOpenFlights(txt,new Set(missing));Object.assign(have,parsed);if(missing.every(c=>have[c]))break}catch(e){}} 19const SEED={FRA:{lat:50.0379,lon:8.5622,name:'Frankfurt am Main',code:'FRA'},MUC:{lat:48.3538,lon:11.7861,name:'Munich',code:'MUC'},DUS:{lat:51.2895,lon:6.7668,name:'Düsseldorf',code:'DUS'},NUE:{lat:49.4987,lon:11.0669,name:'Nürnberg',code:'NUE'},LEJ:{lat:51.4239,lon:12.2367,name:'Leipzig/Halle',code:'LEJ'},HAM:{lat:53.6304,lon:9.9882,name:'Hamburg',code:'HAM'},BER:{lat:52.3628,lon:13.5038,name:'Berlin Brandenburg',code:'BER'},TFS:{lat:28.0445,lon:-16.5725,name:'Tenerife South',code:'TFS'}};for(const code of missing){if(!have[code]&&SEED[code])have[code]=SEED[code]} 20lsWrite(LS_KEY_AIR,have);return have} 21async function loadCitySubset(wanted){const store=lsRead(LS_KEY_CITY);const have=store.data||{};const missing=[...wanted].filter(n=>!have[n]);if(!missing.length)return have;for(const name of missing){try{const url=NOMINATIM_ENDPOINT+'?format=jsonv2&limit=1&q='+encodeURIComponent(name)+'&accept-language=de';const res=await fetchJSON(url);if(Array.isArray(res)&&res.length){const hit=res[0];const lat=parseFloat(hit.lat);const lon=parseFloat(hit.lon);if(isFinite(lat)&&isFinite(lon)){have[name]={lat,lon,name:hit.display_name||name,city:hit.name||name,country:(hit.address&&(hit.address.country||hit.address.country_code))||'',code:name}}} 22await sleep(200)}catch(e){}}
23lsWrite(LS_KEY_CITY,have);return have} 24function greatCircle(a,b,n=120){const toRad=x=>x*Math.PI/180,toDeg=x=>x*180/Math.PI;const Ï1=toRad(a.lat),λ1=toRad(a.lon),Ï2=toRad(b.lat),λ2=toRad(b.lon);const sinÎÏ=Math.sin((Ï2-Ï1)/2),sinÎλ=Math.sin((Ï2-Ï1)/2);const Î=2*Math.asin(Math.sqrt(sinÎÏ*sinÎÏ+Math.cos(Ï1)*Math.cos(Ï2)*sinÎλ*sinÎλ));if(!Î||!isFinite(Î))return[[a.lat,a.lon],[b.lat,b.lon]];const pts=[];for(let i=0;i<=n;i++){const f=i/n;const A=Math.sin((1-f)*Î)/Math.sin(Î);const B=Math.sin(f*Î)/Math.sin(Î);const x=A*Math.cos(Ï1)*Math.cos(λ1)+B*Math.cos(Ï2)*Math.cos(λ2);const y=A*Math.cos(Ï1)*Math.sin(λ1)+B*Math.cos(Ï2)*Math.sin(λ2);const z=A*Math.sin(Ï1)+B*Math.sin(Ï2);const Ïi=Math.atan2(z,Math.sqrt(x*x+y*y));const λi=Math.atan2(y,x);pts.push([toDeg(Ïi),toDeg(λi)])} 25return pts} 26function ensureHost(el){if(el.dataset.pkRouteReady==='1')return!1;el.dataset.pkRouteReady='1';if(!el.style.height)el.style.height='var(--pk-route-height, 480px)';el.style.position='relative';el.style.borderRadius='14px';el.style.overflow='hidden';return!0} 27function buildMap(el,dark){const map=L.map(el,{zoomControl:!0,attributionControl:!0});const url=dark==='1'?'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png':'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';const attr=dark==='1'?'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © CARTO':'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';L.tileLayer(url,{attribution:attr,maxZoom:12}).addTo(map);return map} 28async function renderOne(el){if(!ensureHost(el))return;await loadAny(LEAFLET_CSS,loadStyleOnce);await loadAny(LEAFLET_JS,loadScriptOnce);if(!window.L){el.textContent='Leaflet konnte nicht geladen werden.';return} 29await sleep(10);const mode=(el.getAttribute('data-mode')||'flight').toLowerCase();const dark=el.getAttribute('data-dark')||'0';const starts=splitCSV(el.getAttribute('data-start'));const stops=splitCSV(el.getAttribute('data-stops'));const ziel=clean(el.getAttribute('data-ziel'));const rawTerms=[...starts,...stops,ziel].filter(Boolean);let db={};if(mode==='ship'){const namesNeeded=new Set(rawTerms.filter(x=>!tryCoords(x)));db=await loadCitySubset(namesNeeded)}else{const codesNeeded=new Set(rawTerms.filter(x=>!tryCoords(x)).map(x=>x.toUpperCase()));db=await loadAirSubset(codesNeeded)} 30const resolve=(term)=>{const c=tryCoords(term);if(c)return{...c,code:undefined};if(!term)return null;if(mode==='ship'){const key=term;return db[key]||null}else{const code=String(term||'').trim().toUpperCase();return db[code]||null}};const startObjs=starts.map(resolve).filter(Boolean);const stopObjs=stops.map(resolve).filter(Boolean);const zielObj=resolve(ziel);if(!startObjs.length||!zielObj){const label=mode==='ship'?'â ï¸ Orte konnten nicht aufgelöst werden.':'â ï¸ Flughäfen konnten nicht aufgelöst werden.';el.innerHTML='<div style="position:absolute;inset:0;display:grid;place-items:center;font:600 14px/1.3 system-ui">'+label+'</div>';return} 31const map=buildMap(el,dark);const all=[];const addMarker=(p,label)=>{const m=L.marker([p.lat,p.lon],{interactive:!1}).addTo(map);m.bindTooltip(label,{permanent:!0,direction:'top',className:'pk-label'});all.push([p.lat,p.lon])};addMarker(zielObj,zielObj.code||zielObj.name||'Ziel');const addLeg=(a,b,label)=>{const pts=greatCircle(a,b,120);L.polyline(pts,{weight:3,opacity:0.9}).addTo(map);addMarker(a,label||a.code||a.name||(mode==='ship'?'Hafen':'Start'));all.push([b.lat,b.lon])};if(stopObjs.length){for(const s of startObjs){let prev=s;for(let i=0;i<stopObjs.length;i++){const st=stopObjs[i];addLeg(prev,st,prev.code);prev=st} 32addLeg(prev,zielObj,prev.code)}}else{for(const s of startObjs)addLeg(s,zielObj,s.code)} 33try{map.fitBounds(all,{padding:[28,28]})}catch(e){} 34if(!document.getElementById('pk-route-css')){const st=document.createElement('style');st.id='pk-route-css';st.textContent=` 35 .leaflet-tooltip.pk-label{ 36 background:#111;border:0;border-radius:12px;color:#fff; 37 padding:4px 8px;font:600 12px/1.1 Inter,system-ui,Arial; 38 box-shadow:0 1px 4px rgba(0,0,0,.25); 39 }`;document.head.appendChild(st)}} 40function boot(){document.querySelectorAll('.pk-route').forEach(renderOne)} 41if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',boot)}else{boot()} 42new MutationObserver(boot).observe(document.documentElement,{childList:!0,subtree:!0})})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.