iobroker.jetframe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +357 -0
- package/admin/SF-Pro.ttf +0 -0
- package/admin/admin.d.ts +65 -0
- package/admin/frame.html +982 -0
- package/admin/frame.html.bak-aircraft-card-real-row-20260518-1608 +1236 -0
- package/admin/frame.html.bak-aircraft-card-structure-20260518-1517 +1236 -0
- package/admin/frame.html.bak-aircraft-logo-id-fix-20260518-1639 +1239 -0
- package/admin/frame.html.bak-shortcut-test +1236 -0
- package/admin/frame.html.bak-tablet-class-20260518-1729 +1239 -0
- package/admin/heatmap.html +216 -0
- package/admin/index.html +268 -0
- package/admin/index_m.html +1749 -0
- package/admin/jetframe.css +1260 -0
- package/admin/jetframe.css.bak-airbus-landscape-fix +4630 -0
- package/admin/jetframe.css.bak-aircraft-card-clean-equal-20260518-1438 +4899 -0
- package/admin/jetframe.css.bak-aircraft-card-real-row-20260518-1608 +4814 -0
- package/admin/jetframe.css.bak-aircraft-card-row-left-20260518-1525 +4604 -0
- package/admin/jetframe.css.bak-aircraft-card-slim-equal-20260518-1446 +4647 -0
- package/admin/jetframe.css.bak-aircraft-card-structure-20260518-1517 +4646 -0
- package/admin/jetframe.css.bak-aircraft-inline-final-20260518-1527 +4654 -0
- package/admin/jetframe.css.bak-aircraft-row-compact-fix-20260518-1639 +4763 -0
- package/admin/jetframe.css.bak-before-aircrafttype-purge +4818 -0
- package/admin/jetframe.css.bak-before-cleanup +4670 -0
- package/admin/jetframe.css.bak-before-remove-tablet-only-20260518-1711 +4896 -0
- package/admin/jetframe.css.bak-before-tablet-layout-rework-20260518-1650 +4914 -0
- package/admin/jetframe.css.bak-clean-duplicate-fonts-20260518-1340 +4975 -0
- package/admin/jetframe.css.bak-clean-old-index-fix-20260518-1937 +5167 -0
- package/admin/jetframe.css.bak-hardleft-airbus +4751 -0
- package/admin/jetframe.css.bak-index-iphone-landscape-20260518-1931 +5030 -0
- package/admin/jetframe.css.bak-index-landscape-final-20260518-1941 +5167 -0
- package/admin/jetframe.css.bak-index-landscape-real-20260518-1936 +5186 -0
- package/admin/jetframe.css.bak-landscape-compact-jumbo-bold-20260518-1343 +4802 -0
- package/admin/jetframe.css.bak-logo-align-final +4551 -0
- package/admin/jetframe.css.bak-logo-final2 +4551 -0
- package/admin/jetframe.css.bak-narrowbody-font-fix +4992 -0
- package/admin/jetframe.css.bak-nuke-airbus-align +4790 -0
- package/admin/jetframe.css.bak-pill-balance-20260518-1603 +4773 -0
- package/admin/jetframe.css.bak-pill-balance-fix +4910 -0
- package/admin/jetframe.css.bak-radar-fix-fonts +4710 -0
- package/admin/jetframe.css.bak-shortcut-test +4899 -0
- package/admin/jetframe.css.bak-smaller-aircraft-card-fonts-20260518-1345 +4897 -0
- package/admin/jetframe.css.bak-tablet-fix-real-20260518-1748 +4945 -0
- package/admin/jetframe.css.bak-tablet-fullscreen-fix-20260518-1804 +4972 -0
- package/admin/jetframe.css.bak-tablet-landscape-layout-20260518-1645 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-final-20260518-1839 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-v3-20260518-1729 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-v4-20260518-1801 +4957 -0
- package/admin/jetframe.css.bak-tablet-layout-v5-20260518-1843 +4970 -0
- package/admin/jetframe.css.bak-tablet-layout-v6-20260518-1848 +4958 -0
- package/admin/jetframe.css.bak-tablet-layout-v7-20260518-1909 +4985 -0
- package/admin/jetframe.css.bak-tablet-only-landscape-v2-20260518-1707 +4802 -0
- package/admin/jetframe.css.bak-tablet-pages-final-20260519-1857 +5188 -0
- package/admin/jetframe.css.bak-tablet-pages-final-20260519-1859 +5347 -0
- package/admin/jetframe.css.bak-tablet-pages-v2-20260519-190807 +5349 -0
- package/admin/jetframe.css.bak-typography-align-final +4818 -0
- package/admin/jetframe.png +0 -0
- package/admin/manifest.webmanifest +15 -0
- package/admin/src/app.tsx +58 -0
- package/admin/src/components/settings.tsx +97 -0
- package/admin/src/i18n/de.json +11 -0
- package/admin/src/i18n/en.json +11 -0
- package/admin/src/i18n/es.json +11 -0
- package/admin/src/i18n/fr.json +11 -0
- package/admin/src/i18n/i18n.d.ts +28 -0
- package/admin/src/i18n/it.json +11 -0
- package/admin/src/i18n/nl.json +11 -0
- package/admin/src/i18n/pl.json +11 -0
- package/admin/src/i18n/pt.json +11 -0
- package/admin/src/i18n/ru.json +11 -0
- package/admin/src/i18n/uk.json +11 -0
- package/admin/src/i18n/zh-cn.json +11 -0
- package/admin/src/index.tsx +25 -0
- package/admin/stats.html +228 -0
- package/admin/style.css +32 -0
- package/admin/tsconfig.json +11 -0
- package/admin/words.js +46 -0
- package/build/lib/adsb.js +218 -0
- package/build/lib/adsb.js.map +7 -0
- package/build/lib/airportNamesDe.js +131 -0
- package/build/lib/airportNamesDe.js.map +7 -0
- package/build/lib/airports.js +281 -0
- package/build/lib/airports.js.map +7 -0
- package/build/lib/classify.js +339 -0
- package/build/lib/classify.js.map +7 -0
- package/build/lib/config.js +103 -0
- package/build/lib/config.js.map +7 -0
- package/build/lib/flightInfo.js +1409 -0
- package/build/lib/flightInfo.js.map +7 -0
- package/build/lib/geo.js +84 -0
- package/build/lib/geo.js.map +7 -0
- package/build/lib/images.js +422 -0
- package/build/lib/images.js.map +7 -0
- package/build/lib/specialLiveries.js +342 -0
- package/build/lib/specialLiveries.js.map +7 -0
- package/build/lib/states.js +971 -0
- package/build/lib/states.js.map +7 -0
- package/build/lib/staticFiles.js +73 -0
- package/build/lib/staticFiles.js.map +7 -0
- package/build/lib/types.js +17 -0
- package/build/lib/types.js.map +7 -0
- package/build/lib/visConfig.js +52 -0
- package/build/lib/visConfig.js.map +7 -0
- package/build/main.js +1454 -0
- package/build/main.js.map +7 -0
- package/io-package.json +169 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var flightInfo_exports = {};
|
|
20
|
+
__export(flightInfo_exports, {
|
|
21
|
+
enrichFlightInfo: () => enrichFlightInfo,
|
|
22
|
+
resolveImageViaFr24Aircraft: () => resolveImageViaFr24Aircraft
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(flightInfo_exports);
|
|
25
|
+
const flighteraPlaneRouteCache = {};
|
|
26
|
+
const fr24LiveRouteCache = {};
|
|
27
|
+
const fr24AircraftCache = {};
|
|
28
|
+
const adsbdbCallsignCache = {};
|
|
29
|
+
const hexdbRouteCache = {};
|
|
30
|
+
const hexdbAirlineCache = {};
|
|
31
|
+
const CACHE = {
|
|
32
|
+
flighteraMs: 12 * 60 * 60 * 1e3,
|
|
33
|
+
fr24LiveMs: 60 * 60 * 1e3,
|
|
34
|
+
fr24Ms: 24 * 60 * 60 * 1e3,
|
|
35
|
+
adsbdbMs: 12 * 60 * 60 * 1e3,
|
|
36
|
+
hexdbRouteMs: 6 * 60 * 60 * 1e3,
|
|
37
|
+
hexdbAirlineMs: 24 * 60 * 60 * 1e3
|
|
38
|
+
};
|
|
39
|
+
async function enrichFlightInfo(adapter, config, a, httpJson, httpText, logDebug, logWarn) {
|
|
40
|
+
if (!a.callsign) {
|
|
41
|
+
return {
|
|
42
|
+
...a,
|
|
43
|
+
aircraftType: a.aircraftType || a.type || "",
|
|
44
|
+
...buildSpecialInfo(a)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const operationalCallsign = clean(a.callsign).toUpperCase();
|
|
49
|
+
const operationalData = await loadAdsbdbByCallsign(operationalCallsign, httpJson, logDebug, logWarn);
|
|
50
|
+
const hexAirline = await resolveAirlineViaHexDb(a.hex, httpText, logDebug, logWarn);
|
|
51
|
+
let parsed = parseAdsbdbResponse(config, operationalData, a, operationalCallsign, operationalCallsign);
|
|
52
|
+
if (hexAirline == null ? void 0 : hexAirline.name) {
|
|
53
|
+
parsed.airlineName = hexAirline.name;
|
|
54
|
+
parsed.airlineIata = hexAirline.iata || parsed.airlineIata || "";
|
|
55
|
+
parsed.airlineIcao = hexAirline.icao || parsed.airlineIcao || guessAirlineIcao(operationalCallsign);
|
|
56
|
+
parsed.logoUrl = buildExternalAirlineLogoUrl(
|
|
57
|
+
config,
|
|
58
|
+
parsed.airlineIcao || guessAirlineIcao(operationalCallsign),
|
|
59
|
+
parsed.airlineIata || ""
|
|
60
|
+
);
|
|
61
|
+
logDebug(`HexDB Airline bevorzugt: ${parsed.airlineName}`);
|
|
62
|
+
}
|
|
63
|
+
const regForRoute = parsed.registration || a.registration;
|
|
64
|
+
let routeFound = false;
|
|
65
|
+
const hexRoute = await resolveRouteViaHexDb(adapter, operationalCallsign, httpJson, logDebug, logWarn, config);
|
|
66
|
+
const flighteraRoute = await resolveRouteViaFlighteraPlane(
|
|
67
|
+
regForRoute,
|
|
68
|
+
operationalCallsign,
|
|
69
|
+
a.mode || "",
|
|
70
|
+
httpText,
|
|
71
|
+
logDebug,
|
|
72
|
+
logWarn,
|
|
73
|
+
config
|
|
74
|
+
);
|
|
75
|
+
const mergedRoute = mergeHexAndFlighteraRoute(hexRoute, flighteraRoute);
|
|
76
|
+
if ((mergedRoute == null ? void 0 : mergedRoute.originIata) && (mergedRoute == null ? void 0 : mergedRoute.destIata)) {
|
|
77
|
+
parsed.routeCallsign = (flighteraRoute == null ? void 0 : flighteraRoute.routeCallsign) || (hexRoute == null ? void 0 : hexRoute.routeCallsign) || parsed.routeCallsign || operationalCallsign;
|
|
78
|
+
parsed.originIata = mergedRoute.originIata;
|
|
79
|
+
parsed.destIata = mergedRoute.destIata;
|
|
80
|
+
parsed.routeReliable = true;
|
|
81
|
+
if ((hexRoute == null ? void 0 : hexRoute.originIata) && (hexRoute == null ? void 0 : hexRoute.destIata) && (flighteraRoute == null ? void 0 : flighteraRoute.originIata) && (flighteraRoute == null ? void 0 : flighteraRoute.destIata) && (hexRoute.originIata !== flighteraRoute.originIata || hexRoute.destIata !== flighteraRoute.destIata)) {
|
|
82
|
+
parsed.routeWarning = (flighteraRoute == null ? void 0 : flighteraRoute.isLive) ? "Flightera Live bevorzugt, HexDB abweichend" : "HexDB bevorzugt, Flightera abweichend";
|
|
83
|
+
parsed.routeSource = (flighteraRoute == null ? void 0 : flighteraRoute.isLive) ? "flightera-live-route-conflict-hexdb+airportjson" : "hexdb-route-verified-conflict+airportjson";
|
|
84
|
+
} else if ((hexRoute == null ? void 0 : hexRoute.originIata) && (hexRoute == null ? void 0 : hexRoute.destIata)) {
|
|
85
|
+
parsed.routeWarning = flighteraRoute ? "HexDB + Flightera gepr\xFCft" : "HexDB Route";
|
|
86
|
+
parsed.routeSource = flighteraRoute ? "hexdb-route+flightera-check+airportjson" : "hexdb-route+airportjson";
|
|
87
|
+
} else {
|
|
88
|
+
parsed.routeWarning = (flighteraRoute == null ? void 0 : flighteraRoute.isLive) ? "Live-Flug erkannt" : "";
|
|
89
|
+
parsed.routeSource = (flighteraRoute == null ? void 0 : flighteraRoute.isLive) ? "flightera-plane-live-route+airportjson" : "flightera-plane-callsign-route+airportjson";
|
|
90
|
+
}
|
|
91
|
+
parsed.routeText = `${parsed.originIata} \u2192 ${parsed.destIata}`;
|
|
92
|
+
routeFound = true;
|
|
93
|
+
}
|
|
94
|
+
if (!routeFound) {
|
|
95
|
+
const fr24Live = await resolveRouteViaFr24Live(
|
|
96
|
+
operationalCallsign,
|
|
97
|
+
a.mode || "",
|
|
98
|
+
httpText,
|
|
99
|
+
logDebug,
|
|
100
|
+
logWarn,
|
|
101
|
+
config
|
|
102
|
+
);
|
|
103
|
+
if ((fr24Live == null ? void 0 : fr24Live.originIata) && (fr24Live == null ? void 0 : fr24Live.destIata)) {
|
|
104
|
+
parsed.routeCallsign = fr24Live.routeCallsign || parsed.routeCallsign || operationalCallsign;
|
|
105
|
+
parsed.originIata = fr24Live.originIata;
|
|
106
|
+
parsed.destIata = fr24Live.destIata;
|
|
107
|
+
parsed.routeReliable = true;
|
|
108
|
+
parsed.routeWarning = "FR24 Live-Fallback";
|
|
109
|
+
parsed.routeSource = "fr24-live-route+airportjson";
|
|
110
|
+
parsed.routeText = `${parsed.originIata} \u2192 ${parsed.destIata}`;
|
|
111
|
+
routeFound = true;
|
|
112
|
+
}
|
|
113
|
+
if (fr24Live == null ? void 0 : fr24Live.imageUrl) {
|
|
114
|
+
parsed.fr24ImageUrl = fr24Live.imageUrl;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!routeFound) {
|
|
118
|
+
const adsbdbRoute = parseAdsbdbRouteFallback(operationalData, a.mode || "", config);
|
|
119
|
+
if ((adsbdbRoute == null ? void 0 : adsbdbRoute.originIata) && (adsbdbRoute == null ? void 0 : adsbdbRoute.destIata)) {
|
|
120
|
+
parsed.routeCallsign = adsbdbRoute.routeCallsign || parsed.routeCallsign || operationalCallsign;
|
|
121
|
+
parsed.originIata = adsbdbRoute.originIata;
|
|
122
|
+
parsed.destIata = adsbdbRoute.destIata;
|
|
123
|
+
parsed.routeReliable = true;
|
|
124
|
+
parsed.routeWarning = "ADSBDB Fallback";
|
|
125
|
+
parsed.routeSource = "adsbdb-route-fallback+airportjson";
|
|
126
|
+
parsed.routeText = `${parsed.originIata} \u2192 ${parsed.destIata}`;
|
|
127
|
+
routeFound = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!routeFound) {
|
|
131
|
+
parsed = makeUnknownAirportRoute(a.mode || "", parsed, config);
|
|
132
|
+
}
|
|
133
|
+
if (!parsed.fr24ImageUrl) {
|
|
134
|
+
parsed.fr24ImageUrl = "";
|
|
135
|
+
}
|
|
136
|
+
parsed = await applyAirportNamesFromJson(adapter, config, parsed, logWarn);
|
|
137
|
+
const jet = parsed.fr24ImageUrl ? { best: parsed.fr24ImageUrl } : { best: "" };
|
|
138
|
+
const baseInfo = {
|
|
139
|
+
...parsed,
|
|
140
|
+
operationalCallsign,
|
|
141
|
+
jetphotosUrl: parsed.registration ? `https://www.flightradar24.com/data/aircraft/${encodeURIComponent(
|
|
142
|
+
String(parsed.registration).toLowerCase()
|
|
143
|
+
)}` : "",
|
|
144
|
+
jetphotosImageUrl: jet.best || "",
|
|
145
|
+
aircraftType: parsed.aircraftType || a.aircraftType || a.type || "",
|
|
146
|
+
aircraftModel: parsed.aircraftModel || parsed.aircraftType || a.aircraftModel || a.aircraftType || a.type || ""
|
|
147
|
+
};
|
|
148
|
+
const specialInfo = buildSpecialInfo({
|
|
149
|
+
...a,
|
|
150
|
+
...baseInfo
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
...a,
|
|
154
|
+
...baseInfo,
|
|
155
|
+
...specialInfo
|
|
156
|
+
};
|
|
157
|
+
} catch (e) {
|
|
158
|
+
logWarn(`FlightInfo Fehler: ${errorText(e)}`);
|
|
159
|
+
return {
|
|
160
|
+
...a,
|
|
161
|
+
aircraftType: a.aircraftType || a.type || "",
|
|
162
|
+
...buildSpecialInfo(a)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function resolveAirlineViaHexDb(hex, httpText, logDebug, logWarn) {
|
|
167
|
+
const cleanHex = clean(hex).toLowerCase().replace(/[^a-f0-9]/g, "");
|
|
168
|
+
if (!cleanHex) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const cached = hexdbAirlineCache[cleanHex];
|
|
173
|
+
if (cached && now - cached.ts < CACHE.hexdbAirlineMs) {
|
|
174
|
+
logDebug(`HexDB Airline Cache hit: ${cleanHex}`);
|
|
175
|
+
return cached.data || null;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
logDebug(`HexDB Airline Anfrage: ${cleanHex}`);
|
|
179
|
+
const data = await httpText(`https://hexdb.io/hex-airline?hex=${encodeURIComponent(cleanHex)}`);
|
|
180
|
+
const name = clean(data);
|
|
181
|
+
if (!name || name.toLowerCase().includes("not found")) {
|
|
182
|
+
hexdbAirlineCache[cleanHex] = {
|
|
183
|
+
ts: now,
|
|
184
|
+
data: null
|
|
185
|
+
};
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const result = normalizeHexDbAirlineName(name);
|
|
189
|
+
hexdbAirlineCache[cleanHex] = {
|
|
190
|
+
ts: now,
|
|
191
|
+
data: result
|
|
192
|
+
};
|
|
193
|
+
return result;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
hexdbAirlineCache[cleanHex] = {
|
|
196
|
+
ts: now,
|
|
197
|
+
data: null
|
|
198
|
+
};
|
|
199
|
+
logDebug(`HexDB Airline nicht nutzbar: ${errorText(e)}`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function normalizeHexDbAirlineName(name) {
|
|
204
|
+
return {
|
|
205
|
+
name: clean(name),
|
|
206
|
+
iata: "",
|
|
207
|
+
icao: ""
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function loadAdsbdbByCallsign(callsign, httpJson, logDebug, logWarn) {
|
|
211
|
+
const cs = clean(callsign).toUpperCase();
|
|
212
|
+
if (!cs || cs.length < 3) {
|
|
213
|
+
logDebug("ADSBDB \xFCbersprungen: ung\xFCltiger Callsign");
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const cached = adsbdbCallsignCache[cs];
|
|
218
|
+
if (cached && now - cached.ts < CACHE.adsbdbMs) {
|
|
219
|
+
logDebug(`ADSBDB Cache hit: ${cs}`);
|
|
220
|
+
return cached.data || null;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
logDebug(`ADSBDB Anfrage EINMALIG: ${cs}`);
|
|
224
|
+
const data = await httpJson(`https://api.adsbdb.com/v0/callsign/${encodeURIComponent(cs)}`);
|
|
225
|
+
adsbdbCallsignCache[cs] = {
|
|
226
|
+
ts: now,
|
|
227
|
+
data: data || null
|
|
228
|
+
};
|
|
229
|
+
return data || null;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
adsbdbCallsignCache[cs] = {
|
|
232
|
+
ts: now,
|
|
233
|
+
data: null
|
|
234
|
+
};
|
|
235
|
+
logWarn(`ADSBDB Fehler gecached f\xFCr ${cs}: ${errorText(e)}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function parseAdsbdbResponse(config, data, a, operationalCallsign, routeCallsign) {
|
|
240
|
+
const response = (data == null ? void 0 : data.response) || {};
|
|
241
|
+
const route = response.flightroute || null;
|
|
242
|
+
const aircraft = response.aircraft || null;
|
|
243
|
+
const airlineName = (route == null ? void 0 : route.airline) ? clean(route.airline.name) : guessAirlineName(operationalCallsign);
|
|
244
|
+
const airlineIata = (route == null ? void 0 : route.airline) ? clean(route.airline.iata) : guessAirlineIata(operationalCallsign);
|
|
245
|
+
const airlineIcao = (route == null ? void 0 : route.airline) ? clean(route.airline.icao) : guessAirlineIcao(operationalCallsign);
|
|
246
|
+
const aircraftType = aircraft ? clean(aircraft.type) : a.type || "";
|
|
247
|
+
const aircraftModel = (aircraft ? clean(aircraft.model) : "") || aircraftType || a.aircraftModel || "";
|
|
248
|
+
const registration = aircraft ? clean(aircraft.registration) : a.registration || "";
|
|
249
|
+
const logoKey = airlineIcao || guessAirlineIcao(operationalCallsign);
|
|
250
|
+
const logoUrl = buildExternalAirlineLogoUrl(config, logoKey, airlineIata);
|
|
251
|
+
const logoFallbackUrl = "";
|
|
252
|
+
return {
|
|
253
|
+
operationalCallsign,
|
|
254
|
+
routeCallsign,
|
|
255
|
+
airlineName,
|
|
256
|
+
airlineIata,
|
|
257
|
+
airlineIcao,
|
|
258
|
+
originIata: "",
|
|
259
|
+
destIata: "",
|
|
260
|
+
originName: "",
|
|
261
|
+
destName: "",
|
|
262
|
+
routeText: "",
|
|
263
|
+
routeTextLong: "",
|
|
264
|
+
routeReliable: false,
|
|
265
|
+
routeWarning: "",
|
|
266
|
+
routeSource: "adsbdb-no-route",
|
|
267
|
+
aircraftModel,
|
|
268
|
+
aircraftType,
|
|
269
|
+
registration,
|
|
270
|
+
logoUrl,
|
|
271
|
+
logoFallbackUrl,
|
|
272
|
+
fr24ImageUrl: ""
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function buildExternalAirlineLogoUrl(config, airlineIcao, airlineIata) {
|
|
276
|
+
if (!config.externalAirlineLogos) {
|
|
277
|
+
return "";
|
|
278
|
+
}
|
|
279
|
+
const base = String(config.airlineLogoBaseUrl || "").trim();
|
|
280
|
+
if (!base || !airlineIcao) {
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
const icao = clean(airlineIcao).toUpperCase();
|
|
284
|
+
const iata = clean(airlineIata).toUpperCase();
|
|
285
|
+
if (base.includes("{icao}") || base.includes("{iata}") || base.includes("{code}")) {
|
|
286
|
+
return base.replace(/\{icao\}/g, encodeURIComponent(icao)).replace(/\{iata\}/g, encodeURIComponent(iata || icao)).replace(/\{code\}/g, encodeURIComponent(icao));
|
|
287
|
+
}
|
|
288
|
+
return `${base.replace(/\/+$/, "")}/${encodeURIComponent(icao)}.png`;
|
|
289
|
+
}
|
|
290
|
+
function parseAdsbdbRouteFallback(data, mode, config) {
|
|
291
|
+
const response = (data == null ? void 0 : data.response) || {};
|
|
292
|
+
const route = response.flightroute || null;
|
|
293
|
+
if (!route) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const originIata = route.origin ? clean(route.origin.iata_code).toUpperCase() : "";
|
|
297
|
+
const destIata = route.destination ? clean(route.destination.iata_code).toUpperCase() : "";
|
|
298
|
+
const callsign = clean(route.callsign || route.flight_number || "").toUpperCase();
|
|
299
|
+
if (!isIataCode(originIata) || !isIataCode(destIata)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (originIata === destIata) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (mode === "TAKEOFF" && originIata !== config.airport.iata) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (mode === "LANDING" && destIata !== config.airport.iata) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
routeCallsign: callsign,
|
|
313
|
+
originIata,
|
|
314
|
+
destIata
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function resolveRouteViaHexDb(adapter, operationalCallsign, httpJson, logDebug, logWarn, config) {
|
|
318
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
319
|
+
if (!op) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const cached = hexdbRouteCache[op];
|
|
324
|
+
if (cached && now - cached.ts < CACHE.hexdbRouteMs) {
|
|
325
|
+
logDebug(`HexDB Route Cache hit: ${op}`);
|
|
326
|
+
return cached.data || null;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const url = `https://hexdb.io/api/v1/route/icao/${encodeURIComponent(op)}`;
|
|
330
|
+
logDebug(`HexDB Route Anfrage: ${op} \u2192 ${url}`);
|
|
331
|
+
const data = await httpJson(url);
|
|
332
|
+
if ((data == null ? void 0 : data.status) === "404" || (data == null ? void 0 : data.error)) {
|
|
333
|
+
hexdbRouteCache[op] = {
|
|
334
|
+
ts: now,
|
|
335
|
+
data: null
|
|
336
|
+
};
|
|
337
|
+
logDebug(`HexDB Route nicht gefunden: ${op}`);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const routeRaw = clean(data == null ? void 0 : data.route).toUpperCase();
|
|
341
|
+
if (!routeRaw || !routeRaw.includes("-")) {
|
|
342
|
+
hexdbRouteCache[op] = {
|
|
343
|
+
ts: now,
|
|
344
|
+
data: null
|
|
345
|
+
};
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const parts = routeRaw.split("-").map((x) => clean(x).toUpperCase());
|
|
349
|
+
const originIcao = parts[0] || "";
|
|
350
|
+
const destIcao = parts[1] || "";
|
|
351
|
+
if (!isIcaoCode(originIcao) || !isIcaoCode(destIcao)) {
|
|
352
|
+
hexdbRouteCache[op] = {
|
|
353
|
+
ts: now,
|
|
354
|
+
data: null
|
|
355
|
+
};
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const originIata = await iataFromIcao(adapter, config, originIcao);
|
|
359
|
+
const destIata = await iataFromIcao(adapter, config, destIcao);
|
|
360
|
+
if (!originIata || !destIata) {
|
|
361
|
+
hexdbRouteCache[op] = {
|
|
362
|
+
ts: now,
|
|
363
|
+
data: null
|
|
364
|
+
};
|
|
365
|
+
logDebug(`HexDB Route ohne IATA-Mapping: ${originIcao}-${destIcao}`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const result = {
|
|
369
|
+
routeCallsign: clean(data == null ? void 0 : data.flight).toUpperCase() || op,
|
|
370
|
+
originIata,
|
|
371
|
+
destIata,
|
|
372
|
+
isLive: true
|
|
373
|
+
};
|
|
374
|
+
hexdbRouteCache[op] = {
|
|
375
|
+
ts: now,
|
|
376
|
+
data: result
|
|
377
|
+
};
|
|
378
|
+
logDebug(`HexDB Route parsed: ${op} | ${originIata} \u2192 ${destIata}`);
|
|
379
|
+
return result;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
hexdbRouteCache[op] = {
|
|
382
|
+
ts: now,
|
|
383
|
+
data: null
|
|
384
|
+
};
|
|
385
|
+
logDebug(`HexDB Route Fehler f\xFCr ${op}: ${errorText(e)}`);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function iataFromIcao(adapter, config, icao) {
|
|
390
|
+
icao = clean(icao).toUpperCase();
|
|
391
|
+
if (!icao) {
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
if (icao === clean(config.airport.icao).toUpperCase()) {
|
|
395
|
+
return clean(config.airport.iata).toUpperCase();
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const st = await adapter.getForeignStateAsync(config.airportJsonDp);
|
|
399
|
+
const raw = (st == null ? void 0 : st.val) ? String(st.val) : "";
|
|
400
|
+
if (!raw || raw === "[]") {
|
|
401
|
+
return "";
|
|
402
|
+
}
|
|
403
|
+
const airports = JSON.parse(raw);
|
|
404
|
+
if (!Array.isArray(airports)) {
|
|
405
|
+
return "";
|
|
406
|
+
}
|
|
407
|
+
const found = airports.find((a) => clean(a.icao || a.ICAO).toUpperCase() === icao);
|
|
408
|
+
if (!found) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
return clean(found.iata || found.IATA || "").toUpperCase();
|
|
412
|
+
} catch {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function mergeHexAndFlighteraRoute(hexRoute, flighteraRoute) {
|
|
417
|
+
const flighteraComplete = !!(flighteraRoute == null ? void 0 : flighteraRoute.originIata) && !!(flighteraRoute == null ? void 0 : flighteraRoute.destIata);
|
|
418
|
+
const hexComplete = !!(hexRoute == null ? void 0 : hexRoute.originIata) && !!(hexRoute == null ? void 0 : hexRoute.destIata);
|
|
419
|
+
if (flighteraComplete && (flighteraRoute == null ? void 0 : flighteraRoute.isLive)) {
|
|
420
|
+
return flighteraRoute;
|
|
421
|
+
}
|
|
422
|
+
if (flighteraComplete && !hexComplete) {
|
|
423
|
+
return flighteraRoute;
|
|
424
|
+
}
|
|
425
|
+
if (hexComplete) {
|
|
426
|
+
return hexRoute;
|
|
427
|
+
}
|
|
428
|
+
if (flighteraComplete) {
|
|
429
|
+
return flighteraRoute;
|
|
430
|
+
}
|
|
431
|
+
return hexRoute || flighteraRoute || null;
|
|
432
|
+
}
|
|
433
|
+
function isIcaoCode(code) {
|
|
434
|
+
code = clean(code).toUpperCase();
|
|
435
|
+
return /^[A-Z]{4}$/.test(code);
|
|
436
|
+
}
|
|
437
|
+
async function resolveRouteViaFlighteraPlane(registration, operationalCallsign, mode, httpText, logDebug, logWarn, config) {
|
|
438
|
+
const reg = clean(registration).toUpperCase();
|
|
439
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
440
|
+
if (!reg || !op) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
const cacheKey = `${reg}|${op}`;
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const cached = flighteraPlaneRouteCache[cacheKey];
|
|
446
|
+
if (cached && now - cached.ts < CACHE.flighteraMs) {
|
|
447
|
+
logDebug(`Flightera Plane Cache hit: ${cacheKey}`);
|
|
448
|
+
return cached.data || null;
|
|
449
|
+
}
|
|
450
|
+
const urls = [
|
|
451
|
+
`https://www.flightera.net/de/planes/${encodeURIComponent(reg)}`,
|
|
452
|
+
`https://www.flightera.net/en/planes/${encodeURIComponent(reg)}`
|
|
453
|
+
];
|
|
454
|
+
for (const url of urls) {
|
|
455
|
+
try {
|
|
456
|
+
logDebug(`Flightera Plane Anfrage EINMALIG: ${cacheKey} \u2192 ${url}`);
|
|
457
|
+
const htmlRaw = await httpText(url);
|
|
458
|
+
const html = normalizeHtml(htmlRaw);
|
|
459
|
+
const text = htmlToText(html);
|
|
460
|
+
const parsed = parseFlighteraPlaneRoute(html, text, op, mode, config, logDebug);
|
|
461
|
+
if ((parsed == null ? void 0 : parsed.originIata) && (parsed == null ? void 0 : parsed.destIata)) {
|
|
462
|
+
flighteraPlaneRouteCache[cacheKey] = {
|
|
463
|
+
ts: now,
|
|
464
|
+
data: parsed
|
|
465
|
+
};
|
|
466
|
+
logDebug(
|
|
467
|
+
`Flightera Plane Route parsed: ${op} | ${parsed.originIata} \u2192 ${parsed.destIata} | routeCallsign=${parsed.routeCallsign || "?"} | live=${parsed.isLive ? "ja" : "nein"}`
|
|
468
|
+
);
|
|
469
|
+
return parsed;
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {
|
|
472
|
+
logWarn(`Flightera Plane Fehler f\xFCr ${cacheKey}: ${errorText(e)}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
flighteraPlaneRouteCache[cacheKey] = {
|
|
476
|
+
ts: now,
|
|
477
|
+
data: null
|
|
478
|
+
};
|
|
479
|
+
logDebug(`Flightera Plane keine Route gefunden f\xFCr ${cacheKey}`);
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
function parseFlighteraPlaneRoute(html, text, operationalCallsign, mode, config, logDebug) {
|
|
483
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
484
|
+
const rows = extractFlighteraRowsStrict(html, text, op, mode, config, logDebug);
|
|
485
|
+
const picked = pickBestFlighteraRow(rows, op, mode, config, logDebug);
|
|
486
|
+
if (picked) {
|
|
487
|
+
return {
|
|
488
|
+
routeCallsign: /[A-Z]/.test(picked.routeCallsign || "") ? picked.routeCallsign : op,
|
|
489
|
+
originIata: picked.originIata,
|
|
490
|
+
destIata: picked.destIata,
|
|
491
|
+
isLive: !!picked.isLive
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
logDebug("[Flightera] Keine passende Live/Callsign-Zeile \u2192 Route verworfen.");
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function extractFlighteraRowsStrict(html, text, operationalCallsign, mode, config, logDebug) {
|
|
498
|
+
const blocks = [];
|
|
499
|
+
const rows = [];
|
|
500
|
+
function addBlock(raw, source, index) {
|
|
501
|
+
const plain = htmlToText(raw);
|
|
502
|
+
if (!plain || plain.length < 25) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
blocks.push({
|
|
506
|
+
text: plain.replace(/\s+/g, " ").trim(),
|
|
507
|
+
source,
|
|
508
|
+
index: index || 0
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
let m;
|
|
512
|
+
const trRegex = /<tr[\s\S]*?<\/tr>/gi;
|
|
513
|
+
while ((m = trRegex.exec(String(html || ""))) !== null) {
|
|
514
|
+
addBlock(m[0], "tr", m.index);
|
|
515
|
+
}
|
|
516
|
+
const fullText = String(text || "").replace(/\s+/g, " ").trim();
|
|
517
|
+
const upperText = fullText.toUpperCase();
|
|
518
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
519
|
+
const opIndex = upperText.indexOf(op);
|
|
520
|
+
if (opIndex >= 0) {
|
|
521
|
+
const start = Math.max(0, opIndex - 500);
|
|
522
|
+
const end = Math.min(fullText.length, opIndex + 1800);
|
|
523
|
+
addBlock(fullText.substring(start, end), "op-live-scope", opIndex);
|
|
524
|
+
} else {
|
|
525
|
+
logDebug(`Flightera op-live-scope: Operational Callsign nicht gefunden: ${op}`);
|
|
526
|
+
}
|
|
527
|
+
const iataLike = operationalToLikelyIataCallsign(op);
|
|
528
|
+
if (iataLike && iataLike !== op) {
|
|
529
|
+
const marketingIndex = upperText.indexOf(iataLike.toUpperCase());
|
|
530
|
+
if (marketingIndex >= 0) {
|
|
531
|
+
const start = Math.max(0, marketingIndex - 500);
|
|
532
|
+
const end = Math.min(fullText.length, marketingIndex + 1800);
|
|
533
|
+
addBlock(fullText.substring(start, end), "marketing-scope", marketingIndex);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const liveRegex = /\bLIVE\b/gi;
|
|
537
|
+
let liveCount = 0;
|
|
538
|
+
while ((m = liveRegex.exec(fullText)) !== null && liveCount < 5) {
|
|
539
|
+
const liveIndex = m.index;
|
|
540
|
+
const start = Math.max(0, liveIndex - 900);
|
|
541
|
+
const end = Math.min(fullText.length, liveIndex + 2400);
|
|
542
|
+
addBlock(fullText.substring(start, end), "live-fallback", liveIndex);
|
|
543
|
+
liveCount++;
|
|
544
|
+
}
|
|
545
|
+
for (const b of blocks) {
|
|
546
|
+
const row = parseFlighteraSingleRow(b.text, operationalCallsign, mode, b.source, b.index, config);
|
|
547
|
+
if (row) {
|
|
548
|
+
rows.push(row);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const unique = [];
|
|
552
|
+
const seen = /* @__PURE__ */ new Set();
|
|
553
|
+
for (const r of rows) {
|
|
554
|
+
const key = [
|
|
555
|
+
r.routeCallsign || "",
|
|
556
|
+
r.operationalCallsign || "",
|
|
557
|
+
r.originIata || "",
|
|
558
|
+
r.destIata || "",
|
|
559
|
+
r.isLive ? "live" : "no",
|
|
560
|
+
r.source || ""
|
|
561
|
+
].join("|");
|
|
562
|
+
if (!seen.has(key)) {
|
|
563
|
+
seen.add(key);
|
|
564
|
+
unique.push(r);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return unique;
|
|
568
|
+
}
|
|
569
|
+
function parseFlighteraSingleRow(rowText, operationalCallsign, mode, source, index, config) {
|
|
570
|
+
const text = String(rowText || "").replace(/\s+/g, " ").trim();
|
|
571
|
+
const upper = text.toUpperCase();
|
|
572
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
573
|
+
const iataLike = operationalToLikelyIataCallsign(op);
|
|
574
|
+
const isLive = /\bLIVE\b/i.test(text);
|
|
575
|
+
const containsOp = !!op && upper.indexOf(op) !== -1;
|
|
576
|
+
const containsIataLike = !!iataLike && upper.indexOf(iataLike.toUpperCase()) !== -1;
|
|
577
|
+
const calls = [];
|
|
578
|
+
let cm;
|
|
579
|
+
const callRegex = /\b([A-Z]{2,3}\d{1,4}[A-Z]?)\b/g;
|
|
580
|
+
const livePairRegex = /\b([A-Z]{2,3}\d{1,4}[A-Z]?)\s+([A-Z]{3}\d+[A-Z]{0,2})\b/g;
|
|
581
|
+
while ((cm = callRegex.exec(upper)) !== null) {
|
|
582
|
+
const cs = clean(cm[1]).toUpperCase();
|
|
583
|
+
if (!looksLikeMarketingCallsign(cs)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!calls.includes(cs)) {
|
|
587
|
+
calls.push(cs);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
let routeCallsign = "";
|
|
591
|
+
const pairMatches = [...text.matchAll(livePairRegex)];
|
|
592
|
+
for (const pm of pairMatches) {
|
|
593
|
+
const marketing = clean(pm[1]).toUpperCase();
|
|
594
|
+
const operational = clean(pm[2]).toUpperCase();
|
|
595
|
+
if (operational === op) {
|
|
596
|
+
routeCallsign = marketing;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!routeCallsign) {
|
|
601
|
+
if (calls.includes(iataLike)) {
|
|
602
|
+
routeCallsign = iataLike;
|
|
603
|
+
} else if (calls.length) {
|
|
604
|
+
routeCallsign = calls.find((cs) => /[A-Z]/.test(cs)) || "";
|
|
605
|
+
} else if (containsOp) {
|
|
606
|
+
routeCallsign = op;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!/[A-Z]/.test(routeCallsign)) {
|
|
610
|
+
routeCallsign = containsOp ? op : "";
|
|
611
|
+
}
|
|
612
|
+
const airportPairs = [];
|
|
613
|
+
let m;
|
|
614
|
+
const pairRegex = /([A-Za-zÄÖÜäöüß .'-]+?)\s*\(([A-Z]{3})\s*\/\s*[A-Z]{4}\)/g;
|
|
615
|
+
while ((m = pairRegex.exec(text)) !== null) {
|
|
616
|
+
const code = clean(m[2]).toUpperCase();
|
|
617
|
+
if (isIataCode(code)) {
|
|
618
|
+
airportPairs.push(code);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
let originIata = "";
|
|
622
|
+
let destIata = "";
|
|
623
|
+
if (airportPairs.length >= 2) {
|
|
624
|
+
originIata = airportPairs[0];
|
|
625
|
+
destIata = airportPairs[1];
|
|
626
|
+
} else {
|
|
627
|
+
const codes = [];
|
|
628
|
+
const codeRegex = /\b([A-Z]{3})\s*\/\s*[A-Z]{4}\b/g;
|
|
629
|
+
while ((m = codeRegex.exec(upper)) !== null) {
|
|
630
|
+
const code = clean(m[1]).toUpperCase();
|
|
631
|
+
if (isIataCode(code) && !codes.includes(code)) {
|
|
632
|
+
codes.push(code);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (codes.length >= 2) {
|
|
636
|
+
originIata = codes[0];
|
|
637
|
+
destIata = codes[1];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
originIata = clean(originIata).toUpperCase();
|
|
641
|
+
destIata = clean(destIata).toUpperCase();
|
|
642
|
+
if (!originIata || !destIata) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
if (!isIataCode(originIata) || !isIataCode(destIata)) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
if (originIata === destIata) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
if (mode === "TAKEOFF" && originIata !== config.airport.iata) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
if (mode === "LANDING" && destIata !== config.airport.iata) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
routeCallsign,
|
|
659
|
+
operationalCallsign: containsOp ? op : "",
|
|
660
|
+
originIata,
|
|
661
|
+
destIata,
|
|
662
|
+
isLive,
|
|
663
|
+
containsOp,
|
|
664
|
+
containsIataLike,
|
|
665
|
+
source,
|
|
666
|
+
index
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function pickBestFlighteraRow(rows, operationalCallsign, mode, config, logDebug) {
|
|
670
|
+
if (!rows.length) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
674
|
+
const iataLike = operationalToLikelyIataCallsign(op);
|
|
675
|
+
const scoreAndSort = (list, bonus) => {
|
|
676
|
+
for (const r of list) {
|
|
677
|
+
r.score = scoreFlighteraRow(r, op, iataLike, mode, config) + bonus;
|
|
678
|
+
}
|
|
679
|
+
list.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
680
|
+
return list[0];
|
|
681
|
+
};
|
|
682
|
+
const liveExact = rows.filter((r) => r.isLive && r.containsOp);
|
|
683
|
+
if (liveExact.length) {
|
|
684
|
+
return scoreAndSort(liveExact, 5e4);
|
|
685
|
+
}
|
|
686
|
+
const liveMarketing = rows.filter((r) => r.isLive && r.containsIataLike);
|
|
687
|
+
if (liveMarketing.length) {
|
|
688
|
+
return scoreAndSort(liveMarketing, 4e4);
|
|
689
|
+
}
|
|
690
|
+
const liveRows = rows.filter((r) => r.isLive);
|
|
691
|
+
if (liveRows.length) {
|
|
692
|
+
return scoreAndSort(liveRows, 3e4);
|
|
693
|
+
}
|
|
694
|
+
const exact = rows.filter((r) => r.containsOp || r.containsIataLike);
|
|
695
|
+
if (exact.length) {
|
|
696
|
+
return scoreAndSort(exact, 1e4);
|
|
697
|
+
}
|
|
698
|
+
logDebug(`Flightera: keine passende Live/Callsign-Zeile f\xFCr ${op}`);
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
function scoreFlighteraRow(r, op, iataLike, mode, config) {
|
|
702
|
+
let score = 0;
|
|
703
|
+
if (r.isLive) {
|
|
704
|
+
score += 1e4;
|
|
705
|
+
}
|
|
706
|
+
if (r.containsOp) {
|
|
707
|
+
score += 5e3;
|
|
708
|
+
}
|
|
709
|
+
if (r.containsIataLike) {
|
|
710
|
+
score += 2500;
|
|
711
|
+
}
|
|
712
|
+
if (r.routeCallsign && iataLike && r.routeCallsign === iataLike) {
|
|
713
|
+
score += 1500;
|
|
714
|
+
}
|
|
715
|
+
if (r.routeCallsign && iataLike && r.routeCallsign.startsWith(iataLike.substring(0))) {
|
|
716
|
+
score += 400;
|
|
717
|
+
}
|
|
718
|
+
if (mode === "TAKEOFF" && r.originIata === config.airport.iata) {
|
|
719
|
+
score += 1e3;
|
|
720
|
+
}
|
|
721
|
+
if (mode === "LANDING" && r.destIata === config.airport.iata) {
|
|
722
|
+
score += 1e3;
|
|
723
|
+
}
|
|
724
|
+
score -= Math.min(r.index || 0, 2e5) / 1e3;
|
|
725
|
+
return score;
|
|
726
|
+
}
|
|
727
|
+
async function resolveRouteViaFr24Live(operationalCallsign, mode, httpText, logDebug, logWarn, config) {
|
|
728
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
729
|
+
if (!op) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const now = Date.now();
|
|
733
|
+
const cached = fr24LiveRouteCache[op];
|
|
734
|
+
if (cached && now - cached.ts < CACHE.fr24LiveMs) {
|
|
735
|
+
logDebug(`FR24 Live Cache hit: ${op}`);
|
|
736
|
+
return cached.data || null;
|
|
737
|
+
}
|
|
738
|
+
const url = `https://www.flightradar24.com/${encodeURIComponent(op)}`;
|
|
739
|
+
try {
|
|
740
|
+
logDebug(`FR24 Live Anfrage EINMALIG: ${op} \u2192 ${url}`);
|
|
741
|
+
const htmlRaw = await httpText(url);
|
|
742
|
+
const html = normalizeHtml(htmlRaw);
|
|
743
|
+
const text = htmlToText(html);
|
|
744
|
+
const imageUrl = pickBestFr24Image(collectFr24Images(html));
|
|
745
|
+
const parsed = parseFr24LiveRoute(html, text, op, mode, config);
|
|
746
|
+
const result = parsed || {
|
|
747
|
+
routeCallsign: "",
|
|
748
|
+
originIata: "",
|
|
749
|
+
destIata: ""
|
|
750
|
+
};
|
|
751
|
+
if (imageUrl) {
|
|
752
|
+
result.imageUrl = imageUrl;
|
|
753
|
+
}
|
|
754
|
+
fr24LiveRouteCache[op] = {
|
|
755
|
+
ts: now,
|
|
756
|
+
data: result
|
|
757
|
+
};
|
|
758
|
+
return result;
|
|
759
|
+
} catch (e) {
|
|
760
|
+
fr24LiveRouteCache[op] = {
|
|
761
|
+
ts: now,
|
|
762
|
+
data: null
|
|
763
|
+
};
|
|
764
|
+
logWarn(`FR24 Live Fehler gecached f\xFCr ${op}: ${errorText(e)}`);
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function parseFr24LiveRoute(html, text, operationalCallsign, mode, config) {
|
|
769
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
770
|
+
const iataLike = operationalToLikelyIataCallsign(op);
|
|
771
|
+
const fullText = String(text || "").replace(/\s+/g, " ").trim();
|
|
772
|
+
const upper = fullText.toUpperCase();
|
|
773
|
+
let scope = fullText;
|
|
774
|
+
let idx = upper.indexOf(op);
|
|
775
|
+
if (idx < 0 && iataLike) {
|
|
776
|
+
idx = upper.indexOf(iataLike.toUpperCase());
|
|
777
|
+
}
|
|
778
|
+
if (idx >= 0) {
|
|
779
|
+
const start = Math.max(0, idx - 700);
|
|
780
|
+
const end = Math.min(fullText.length, idx + 2e3);
|
|
781
|
+
scope = fullText.substring(start, end);
|
|
782
|
+
}
|
|
783
|
+
const row = parseRouteFromAirportPairs(scope, op, mode, config);
|
|
784
|
+
if (row) {
|
|
785
|
+
row.routeCallsign = findBestMarketingCallsign(scope, op) || iataLike || op;
|
|
786
|
+
return row;
|
|
787
|
+
}
|
|
788
|
+
const jsonRoute = parseJsonLikeRouteFromHtml(html, op, mode, config);
|
|
789
|
+
if (jsonRoute) {
|
|
790
|
+
return jsonRoute;
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
function parseRouteFromAirportPairs(scope, operationalCallsign, mode, config) {
|
|
795
|
+
const text = String(scope || "").replace(/\s+/g, " ").trim();
|
|
796
|
+
const pairs = [];
|
|
797
|
+
let m;
|
|
798
|
+
const pairRegex = /([A-Za-zÄÖÜäöüß .'-]+?)\s*\(([A-Z]{3})\s*\/\s*[A-Z]{4}\)/g;
|
|
799
|
+
while ((m = pairRegex.exec(text)) !== null) {
|
|
800
|
+
const code = clean(m[2]).toUpperCase();
|
|
801
|
+
if (isIataCode(code)) {
|
|
802
|
+
pairs.push(code);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (pairs.length < 2) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
let originIata = "";
|
|
809
|
+
let destIata = "";
|
|
810
|
+
if (mode === "TAKEOFF") {
|
|
811
|
+
const aptIndex = pairs.indexOf(config.airport.iata);
|
|
812
|
+
if (aptIndex >= 0 && pairs[aptIndex + 1]) {
|
|
813
|
+
originIata = config.airport.iata;
|
|
814
|
+
destIata = pairs[aptIndex + 1];
|
|
815
|
+
}
|
|
816
|
+
} else if (mode === "LANDING") {
|
|
817
|
+
const aptIndex = pairs.indexOf(config.airport.iata);
|
|
818
|
+
if (aptIndex > 0) {
|
|
819
|
+
originIata = pairs[aptIndex - 1];
|
|
820
|
+
destIata = config.airport.iata;
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
originIata = pairs[0];
|
|
824
|
+
destIata = pairs[1];
|
|
825
|
+
}
|
|
826
|
+
originIata = clean(originIata).toUpperCase();
|
|
827
|
+
destIata = clean(destIata).toUpperCase();
|
|
828
|
+
if (!isIataCode(originIata) || !isIataCode(destIata)) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
if (originIata === destIata) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
if (mode === "TAKEOFF" && originIata !== config.airport.iata) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
if (mode === "LANDING" && destIata !== config.airport.iata) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
routeCallsign: findBestMarketingCallsign(text, operationalCallsign),
|
|
842
|
+
originIata,
|
|
843
|
+
destIata
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function parseJsonLikeRouteFromHtml(html, operationalCallsign, mode, config) {
|
|
847
|
+
html = normalizeHtml(html);
|
|
848
|
+
const iataHits = [];
|
|
849
|
+
let m;
|
|
850
|
+
const iataRegexes = [
|
|
851
|
+
/"iata"\s*:\s*"([A-Z]{3})"/g,
|
|
852
|
+
/"iataCode"\s*:\s*"([A-Z]{3})"/g,
|
|
853
|
+
/"iata_code"\s*:\s*"([A-Z]{3})"/g
|
|
854
|
+
];
|
|
855
|
+
for (const re of iataRegexes) {
|
|
856
|
+
while ((m = re.exec(html)) !== null) {
|
|
857
|
+
const code = clean(m[1]).toUpperCase();
|
|
858
|
+
if (isIataCode(code) && !iataHits.includes(code)) {
|
|
859
|
+
iataHits.push(code);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (iataHits.length < 2) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
let originIata = "";
|
|
867
|
+
let destIata = "";
|
|
868
|
+
if (mode === "TAKEOFF") {
|
|
869
|
+
const aptIndex = iataHits.indexOf(config.airport.iata);
|
|
870
|
+
if (aptIndex >= 0 && iataHits[aptIndex + 1]) {
|
|
871
|
+
originIata = config.airport.iata;
|
|
872
|
+
destIata = iataHits[aptIndex + 1];
|
|
873
|
+
}
|
|
874
|
+
} else if (mode === "LANDING") {
|
|
875
|
+
const aptIndex = iataHits.indexOf(config.airport.iata);
|
|
876
|
+
if (aptIndex > 0) {
|
|
877
|
+
originIata = iataHits[aptIndex - 1];
|
|
878
|
+
destIata = config.airport.iata;
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
originIata = iataHits[0];
|
|
882
|
+
destIata = iataHits[1];
|
|
883
|
+
}
|
|
884
|
+
if (!isIataCode(originIata) || !isIataCode(destIata)) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
if (originIata === destIata) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
routeCallsign: findBestMarketingCallsign(htmlToText(html), operationalCallsign),
|
|
892
|
+
originIata,
|
|
893
|
+
destIata
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function resolveImageViaFr24Aircraft(registration, operationalCallsign, httpText, logDebug, logWarn) {
|
|
897
|
+
const reg = clean(registration).toLowerCase();
|
|
898
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
899
|
+
if (!reg) {
|
|
900
|
+
return "";
|
|
901
|
+
}
|
|
902
|
+
const now = Date.now();
|
|
903
|
+
const cached = fr24AircraftCache[reg];
|
|
904
|
+
if (cached && now - cached.ts < CACHE.fr24Ms) {
|
|
905
|
+
logDebug(`FR24 Bild Cache hit: ${reg}`);
|
|
906
|
+
return cached.imageUrl || "";
|
|
907
|
+
}
|
|
908
|
+
const url = `https://www.flightradar24.com/data/aircraft/${encodeURIComponent(reg)}`;
|
|
909
|
+
try {
|
|
910
|
+
logDebug(`FR24 Aircraft Bild Anfrage EINMALIG: ${reg} / ${op} \u2192 ${url}`);
|
|
911
|
+
const htmlRaw = await httpText(url);
|
|
912
|
+
const html = normalizeHtml(htmlRaw);
|
|
913
|
+
const imageUrl = pickBestFr24Image(collectFr24Images(html));
|
|
914
|
+
fr24AircraftCache[reg] = {
|
|
915
|
+
ts: now,
|
|
916
|
+
imageUrl: imageUrl || ""
|
|
917
|
+
};
|
|
918
|
+
return imageUrl || "";
|
|
919
|
+
} catch (e) {
|
|
920
|
+
fr24AircraftCache[reg] = {
|
|
921
|
+
ts: now,
|
|
922
|
+
imageUrl: ""
|
|
923
|
+
};
|
|
924
|
+
logWarn(`FR24 Bild Fehler gecached f\xFCr ${reg}: ${errorText(e)}`);
|
|
925
|
+
return "";
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async function applyAirportNamesFromJson(adapter, config, parsed, logWarn) {
|
|
929
|
+
const originName = parsed.originIata ? await cityNameFromIata(adapter, config, parsed.originIata, logWarn) : "";
|
|
930
|
+
const destName = parsed.destIata ? await cityNameFromIata(adapter, config, parsed.destIata, logWarn) : "";
|
|
931
|
+
parsed.originName = originName;
|
|
932
|
+
parsed.destName = destName;
|
|
933
|
+
if (parsed.originIata && parsed.destIata) {
|
|
934
|
+
parsed.routeText = `${parsed.originIata} \u2192 ${parsed.destIata}`;
|
|
935
|
+
}
|
|
936
|
+
if (originName && destName) {
|
|
937
|
+
parsed.routeTextLong = `${originName} \u2192 ${destName}`;
|
|
938
|
+
} else if (originName && !destName && parsed.destIata) {
|
|
939
|
+
parsed.routeTextLong = `${originName} \u2192 ${parsed.destIata}`;
|
|
940
|
+
} else if (!originName && destName && parsed.originIata) {
|
|
941
|
+
parsed.routeTextLong = `${parsed.originIata} \u2192 ${destName}`;
|
|
942
|
+
} else {
|
|
943
|
+
parsed.routeTextLong = "";
|
|
944
|
+
}
|
|
945
|
+
return parsed;
|
|
946
|
+
}
|
|
947
|
+
async function cityNameFromIata(adapter, config, iata, logWarn) {
|
|
948
|
+
const lang = await getSystemLanguage(adapter);
|
|
949
|
+
const useGermanNames = lang.toLowerCase().startsWith("de");
|
|
950
|
+
const code = clean(iata).toUpperCase();
|
|
951
|
+
if (!code) {
|
|
952
|
+
return "";
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
const st = await adapter.getForeignStateAsync(config.airportJsonDp);
|
|
956
|
+
const raw = (st == null ? void 0 : st.val) ? String(st.val) : "";
|
|
957
|
+
if (!raw || raw === "[]") {
|
|
958
|
+
return code;
|
|
959
|
+
}
|
|
960
|
+
const airports = JSON.parse(raw);
|
|
961
|
+
if (!Array.isArray(airports)) {
|
|
962
|
+
return code;
|
|
963
|
+
}
|
|
964
|
+
const found = airports.find((a) => clean(a.iata || a.IATA).toUpperCase() === code);
|
|
965
|
+
if (!found) {
|
|
966
|
+
return code;
|
|
967
|
+
}
|
|
968
|
+
const cityDe = clean(found.city_DE);
|
|
969
|
+
const municipality = clean(found.municipality);
|
|
970
|
+
const city = clean(found.city);
|
|
971
|
+
const airport = clean(found.airport || found.name);
|
|
972
|
+
if (useGermanNames && cityDe && cityDe.length >= 3) {
|
|
973
|
+
return cityDe;
|
|
974
|
+
}
|
|
975
|
+
if (municipality && municipality.length >= 3 && !/airport|flug|intl|international/i.test(municipality)) {
|
|
976
|
+
return municipality;
|
|
977
|
+
}
|
|
978
|
+
if (city && city.length >= 3) {
|
|
979
|
+
return city;
|
|
980
|
+
}
|
|
981
|
+
if (airport) {
|
|
982
|
+
return airport.replace(/international airport/gi, "").replace(/international/gi, "").replace(/airport/gi, "").replace(/\s+/g, " ").trim();
|
|
983
|
+
}
|
|
984
|
+
return code;
|
|
985
|
+
} catch (e) {
|
|
986
|
+
logWarn(`airportjson Lookup Fehler f\xFCr ${code}: ${errorText(e)}`);
|
|
987
|
+
return code;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function getSystemLanguage(adapter) {
|
|
991
|
+
var _a, _b;
|
|
992
|
+
try {
|
|
993
|
+
const obj = await adapter.getForeignObjectAsync("system.config");
|
|
994
|
+
return String(((_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.language) || ((_b = obj == null ? void 0 : obj.native) == null ? void 0 : _b.language) || "").trim();
|
|
995
|
+
} catch {
|
|
996
|
+
return "";
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function makeUnknownAirportRoute(mode, parsed, config) {
|
|
1000
|
+
const A = config.airport.iata;
|
|
1001
|
+
if (mode === "TAKEOFF") {
|
|
1002
|
+
return {
|
|
1003
|
+
...parsed,
|
|
1004
|
+
originIata: A,
|
|
1005
|
+
destIata: "",
|
|
1006
|
+
originName: "",
|
|
1007
|
+
destName: "",
|
|
1008
|
+
routeText: `${A} \u2192 ?`,
|
|
1009
|
+
routeTextLong: "",
|
|
1010
|
+
routeReliable: false,
|
|
1011
|
+
routeWarning: "Ziel unbekannt",
|
|
1012
|
+
routeSource: "no-route"
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (mode === "LANDING") {
|
|
1016
|
+
return {
|
|
1017
|
+
...parsed,
|
|
1018
|
+
originIata: "",
|
|
1019
|
+
destIata: A,
|
|
1020
|
+
originName: "",
|
|
1021
|
+
destName: "",
|
|
1022
|
+
routeText: `? \u2192 ${A}`,
|
|
1023
|
+
routeTextLong: "",
|
|
1024
|
+
routeReliable: false,
|
|
1025
|
+
routeWarning: "Start unbekannt",
|
|
1026
|
+
routeSource: "no-route"
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
...parsed,
|
|
1031
|
+
originIata: "",
|
|
1032
|
+
destIata: "",
|
|
1033
|
+
routeText: "",
|
|
1034
|
+
routeTextLong: "",
|
|
1035
|
+
routeReliable: false,
|
|
1036
|
+
routeWarning: "Route unbekannt",
|
|
1037
|
+
routeSource: "no-route"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function collectFr24Images(html) {
|
|
1041
|
+
html = normalizeHtml(html);
|
|
1042
|
+
const found = [];
|
|
1043
|
+
function add(url) {
|
|
1044
|
+
url = normalizeImageUrl(url);
|
|
1045
|
+
if (!url) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (!/\.(jpg|jpeg|webp|png)(\?|$)/i.test(url)) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (found.includes(url)) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (/logo|icon|sprite|avatar|upgrade|plans|app-store|google-play/i.test(url)) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
found.push(url);
|
|
1058
|
+
}
|
|
1059
|
+
let m;
|
|
1060
|
+
const regexes = [
|
|
1061
|
+
/<img[^>]+src=["']([^"']+)["'][^>]*alt=["'][^"']*(?:aircraft|plane|photo|picture)[^"']*["']/gi,
|
|
1062
|
+
/<img[^>]+alt=["'][^"']*(?:aircraft|plane|photo|picture)[^"']*["'][^>]+src=["']([^"']+)["']/gi,
|
|
1063
|
+
/https?:\/\/[^"'<> ]+\.(?:jpg|jpeg|webp|png)(?:\?[^"'<> ]*)?/gi,
|
|
1064
|
+
/src=["']([^"']+\.(?:jpg|jpeg|webp|png)(?:\?[^"']*)?)["']/gi,
|
|
1065
|
+
/content=["']([^"']+\.(?:jpg|jpeg|webp|png)(?:\?[^"']*)?)["']/gi
|
|
1066
|
+
];
|
|
1067
|
+
for (const re of regexes) {
|
|
1068
|
+
while ((m = re.exec(html)) !== null) {
|
|
1069
|
+
add(m[1] || m[0]);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return found;
|
|
1073
|
+
}
|
|
1074
|
+
function pickBestFr24Image(images) {
|
|
1075
|
+
if (!images.length) {
|
|
1076
|
+
return "";
|
|
1077
|
+
}
|
|
1078
|
+
return images.sort((a, b) => scoreFr24Image(b) - scoreFr24Image(a))[0];
|
|
1079
|
+
}
|
|
1080
|
+
function scoreFr24Image(url) {
|
|
1081
|
+
url = String(url || "").toLowerCase();
|
|
1082
|
+
let score = 0;
|
|
1083
|
+
if (url.includes("fr24")) {
|
|
1084
|
+
score += 60;
|
|
1085
|
+
}
|
|
1086
|
+
if (url.includes("cdn")) {
|
|
1087
|
+
score += 50;
|
|
1088
|
+
}
|
|
1089
|
+
if (url.includes("aircraft")) {
|
|
1090
|
+
score += 50;
|
|
1091
|
+
}
|
|
1092
|
+
if (url.includes("large")) {
|
|
1093
|
+
score += 40;
|
|
1094
|
+
}
|
|
1095
|
+
if (url.includes("full")) {
|
|
1096
|
+
score += 40;
|
|
1097
|
+
}
|
|
1098
|
+
if (url.includes("photo")) {
|
|
1099
|
+
score += 20;
|
|
1100
|
+
}
|
|
1101
|
+
if (url.includes("thumb")) {
|
|
1102
|
+
score -= 80;
|
|
1103
|
+
}
|
|
1104
|
+
if (url.includes("small")) {
|
|
1105
|
+
score -= 50;
|
|
1106
|
+
}
|
|
1107
|
+
if (url.includes("logo") || url.includes("icon")) {
|
|
1108
|
+
score -= 100;
|
|
1109
|
+
}
|
|
1110
|
+
return score;
|
|
1111
|
+
}
|
|
1112
|
+
function buildSpecialInfo(a) {
|
|
1113
|
+
const model = String(a.aircraftModel || a.aircraftType || a.type || "").toUpperCase();
|
|
1114
|
+
const callsign = String(a.callsign || "").toUpperCase();
|
|
1115
|
+
const tags = [];
|
|
1116
|
+
let score = 0;
|
|
1117
|
+
if (/A38/i.test(model)) {
|
|
1118
|
+
tags.push("Airbus A380");
|
|
1119
|
+
score += 10;
|
|
1120
|
+
}
|
|
1121
|
+
if (/B74/i.test(model)) {
|
|
1122
|
+
tags.push("Boeing 747");
|
|
1123
|
+
score += 8;
|
|
1124
|
+
}
|
|
1125
|
+
if (containsAny(model, ["BELUGA"])) {
|
|
1126
|
+
tags.push("Airbus Beluga");
|
|
1127
|
+
score += 10;
|
|
1128
|
+
}
|
|
1129
|
+
if (containsAny(model, ["AN-124", "ANTONOV", "AN225", "AN-225"])) {
|
|
1130
|
+
tags.push("Antonov");
|
|
1131
|
+
score += 10;
|
|
1132
|
+
}
|
|
1133
|
+
if (containsAny(callsign, ["GAF", "GOV", "BAF", "NAF", "RCH", "IAM"])) {
|
|
1134
|
+
tags.push("Regierungs-/Milit\xE4rflug");
|
|
1135
|
+
score += 8;
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
isSpecial: score >= 8,
|
|
1139
|
+
specialText: tags.length ? tags.join(", ") : ""
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function guessAirlineIata(callsign) {
|
|
1143
|
+
callsign = clean(callsign).toUpperCase();
|
|
1144
|
+
const map = {
|
|
1145
|
+
DLH: "LH",
|
|
1146
|
+
CFG: "DE",
|
|
1147
|
+
CPA: "CX",
|
|
1148
|
+
KAL: "KE",
|
|
1149
|
+
GEC: "LH",
|
|
1150
|
+
BOX: "3S",
|
|
1151
|
+
EWG: "EW",
|
|
1152
|
+
RYR: "FR",
|
|
1153
|
+
EZY: "U2",
|
|
1154
|
+
BAW: "BA",
|
|
1155
|
+
KLM: "KL",
|
|
1156
|
+
AFR: "AF",
|
|
1157
|
+
SWR: "LX",
|
|
1158
|
+
AUA: "OS",
|
|
1159
|
+
SIA: "SQ",
|
|
1160
|
+
THY: "TK",
|
|
1161
|
+
UAE: "EK",
|
|
1162
|
+
QTR: "QR",
|
|
1163
|
+
CCA: "CA",
|
|
1164
|
+
ROT: "RO",
|
|
1165
|
+
SAS: "SK",
|
|
1166
|
+
SEH: "GQ"
|
|
1167
|
+
};
|
|
1168
|
+
return map[callsign.substring(0)] || "";
|
|
1169
|
+
}
|
|
1170
|
+
function guessAirlineIcao(callsign) {
|
|
1171
|
+
callsign = clean(callsign).toUpperCase();
|
|
1172
|
+
return callsign.length >= 3 ? callsign.substring(0, 3) : "";
|
|
1173
|
+
}
|
|
1174
|
+
function guessAirlineName(callsign) {
|
|
1175
|
+
callsign = clean(callsign).toUpperCase();
|
|
1176
|
+
if (callsign.startsWith("DLH")) {
|
|
1177
|
+
return "Lufthansa";
|
|
1178
|
+
}
|
|
1179
|
+
if (callsign.startsWith("CFG")) {
|
|
1180
|
+
return "Condor";
|
|
1181
|
+
}
|
|
1182
|
+
if (callsign.startsWith("CPA")) {
|
|
1183
|
+
return "Cathay Pacific";
|
|
1184
|
+
}
|
|
1185
|
+
if (callsign.startsWith("KAL")) {
|
|
1186
|
+
return "Korean Air Cargo";
|
|
1187
|
+
}
|
|
1188
|
+
if (callsign.startsWith("GEC")) {
|
|
1189
|
+
return "Lufthansa Cargo";
|
|
1190
|
+
}
|
|
1191
|
+
if (callsign.startsWith("BOX")) {
|
|
1192
|
+
return "AeroLogic";
|
|
1193
|
+
}
|
|
1194
|
+
if (callsign.startsWith("EWG")) {
|
|
1195
|
+
return "Eurowings";
|
|
1196
|
+
}
|
|
1197
|
+
if (callsign.startsWith("RYR")) {
|
|
1198
|
+
return "Ryanair";
|
|
1199
|
+
}
|
|
1200
|
+
if (callsign.startsWith("EZY")) {
|
|
1201
|
+
return "easyJet";
|
|
1202
|
+
}
|
|
1203
|
+
if (callsign.startsWith("BAW")) {
|
|
1204
|
+
return "British Airways";
|
|
1205
|
+
}
|
|
1206
|
+
if (callsign.startsWith("KLM")) {
|
|
1207
|
+
return "KLM";
|
|
1208
|
+
}
|
|
1209
|
+
if (callsign.startsWith("AFR")) {
|
|
1210
|
+
return "Air France";
|
|
1211
|
+
}
|
|
1212
|
+
if (callsign.startsWith("SWR")) {
|
|
1213
|
+
return "SWISS";
|
|
1214
|
+
}
|
|
1215
|
+
if (callsign.startsWith("AUA")) {
|
|
1216
|
+
return "Austrian";
|
|
1217
|
+
}
|
|
1218
|
+
if (callsign.startsWith("SIA")) {
|
|
1219
|
+
return "Singapore Airlines";
|
|
1220
|
+
}
|
|
1221
|
+
if (callsign.startsWith("THY")) {
|
|
1222
|
+
return "Turkish Airlines";
|
|
1223
|
+
}
|
|
1224
|
+
if (callsign.startsWith("UAE")) {
|
|
1225
|
+
return "Emirates";
|
|
1226
|
+
}
|
|
1227
|
+
if (callsign.startsWith("QTR")) {
|
|
1228
|
+
return "Qatar Airways";
|
|
1229
|
+
}
|
|
1230
|
+
if (callsign.startsWith("CCA")) {
|
|
1231
|
+
return "Air China";
|
|
1232
|
+
}
|
|
1233
|
+
if (callsign.startsWith("ROT")) {
|
|
1234
|
+
return "TAROM";
|
|
1235
|
+
}
|
|
1236
|
+
if (callsign.startsWith("SAS")) {
|
|
1237
|
+
return "SAS";
|
|
1238
|
+
}
|
|
1239
|
+
if (callsign.startsWith("SEH")) {
|
|
1240
|
+
return "Sky Express";
|
|
1241
|
+
}
|
|
1242
|
+
return "";
|
|
1243
|
+
}
|
|
1244
|
+
function operationalToLikelyIataCallsign(callsign) {
|
|
1245
|
+
callsign = clean(callsign).toUpperCase();
|
|
1246
|
+
const map = {
|
|
1247
|
+
DLH: "LH",
|
|
1248
|
+
CFG: "DE",
|
|
1249
|
+
CPA: "CX",
|
|
1250
|
+
KAL: "KE",
|
|
1251
|
+
GEC: "LH",
|
|
1252
|
+
BOX: "3S",
|
|
1253
|
+
EWG: "EW",
|
|
1254
|
+
RYR: "FR",
|
|
1255
|
+
EZY: "U2",
|
|
1256
|
+
BAW: "BA",
|
|
1257
|
+
KLM: "KL",
|
|
1258
|
+
AFR: "AF",
|
|
1259
|
+
SWR: "LX",
|
|
1260
|
+
AUA: "OS",
|
|
1261
|
+
SIA: "SQ",
|
|
1262
|
+
THY: "TK",
|
|
1263
|
+
UAE: "EK",
|
|
1264
|
+
QTR: "QR",
|
|
1265
|
+
ETD: "EY",
|
|
1266
|
+
IBE: "IB",
|
|
1267
|
+
TAP: "TP",
|
|
1268
|
+
SAS: "SK",
|
|
1269
|
+
FIN: "AY",
|
|
1270
|
+
LOT: "LO",
|
|
1271
|
+
CCA: "CA",
|
|
1272
|
+
ROT: "RO",
|
|
1273
|
+
SEH: "GQ"
|
|
1274
|
+
};
|
|
1275
|
+
const prefix = callsign.substring(0, 3);
|
|
1276
|
+
const rest = callsign.substring(3);
|
|
1277
|
+
if (map[prefix] && rest) {
|
|
1278
|
+
return map[prefix] + rest;
|
|
1279
|
+
}
|
|
1280
|
+
return callsign;
|
|
1281
|
+
}
|
|
1282
|
+
function findBestMarketingCallsign(text, operationalCallsign) {
|
|
1283
|
+
const op = clean(operationalCallsign).toUpperCase();
|
|
1284
|
+
const iataLike = operationalToLikelyIataCallsign(op);
|
|
1285
|
+
const upper = String(text || "").toUpperCase();
|
|
1286
|
+
if (iataLike && upper.includes(iataLike)) {
|
|
1287
|
+
return iataLike;
|
|
1288
|
+
}
|
|
1289
|
+
const calls = [];
|
|
1290
|
+
let m;
|
|
1291
|
+
const re = /\b([A-Z0-9]{2}\d{1,4}[A-Z]?)\b/g;
|
|
1292
|
+
while ((m = re.exec(upper)) !== null) {
|
|
1293
|
+
const cs = clean(m[1]).toUpperCase();
|
|
1294
|
+
if (!looksLikeMarketingCallsign(cs)) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (!calls.includes(cs)) {
|
|
1298
|
+
calls.push(cs);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return calls.length ? calls[0] : "";
|
|
1302
|
+
}
|
|
1303
|
+
function looksLikeMarketingCallsign(cs) {
|
|
1304
|
+
cs = clean(cs).toUpperCase();
|
|
1305
|
+
if (!/^[A-Z0-9]{2}\d{1,4}[A-Z]?$/.test(cs)) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
if (/^[A-Z]{3}\d/.test(cs)) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
function isIataCode(code) {
|
|
1314
|
+
code = clean(code).toUpperCase();
|
|
1315
|
+
if (!/^[A-Z]{3}$/.test(code)) {
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
const bad = [
|
|
1319
|
+
"THE",
|
|
1320
|
+
"AND",
|
|
1321
|
+
"FOR",
|
|
1322
|
+
"YOU",
|
|
1323
|
+
"ARE",
|
|
1324
|
+
"NOT",
|
|
1325
|
+
"YES",
|
|
1326
|
+
"NEW",
|
|
1327
|
+
"OLD",
|
|
1328
|
+
"AIR",
|
|
1329
|
+
"API",
|
|
1330
|
+
"APP",
|
|
1331
|
+
"MAP",
|
|
1332
|
+
"UTC",
|
|
1333
|
+
"ETA",
|
|
1334
|
+
"STD",
|
|
1335
|
+
"STA",
|
|
1336
|
+
"ATD",
|
|
1337
|
+
"ATA",
|
|
1338
|
+
"IMG",
|
|
1339
|
+
"PNG",
|
|
1340
|
+
"JPG",
|
|
1341
|
+
"WEB",
|
|
1342
|
+
"CSS",
|
|
1343
|
+
"DIV",
|
|
1344
|
+
"SVG",
|
|
1345
|
+
"WWW",
|
|
1346
|
+
"TOP",
|
|
1347
|
+
"VAR",
|
|
1348
|
+
"REL",
|
|
1349
|
+
"ORG",
|
|
1350
|
+
"USE",
|
|
1351
|
+
"DAY",
|
|
1352
|
+
"PER",
|
|
1353
|
+
"MMM",
|
|
1354
|
+
"MAY",
|
|
1355
|
+
"BTN",
|
|
1356
|
+
"HEX",
|
|
1357
|
+
"NET",
|
|
1358
|
+
"SRC",
|
|
1359
|
+
"PAN",
|
|
1360
|
+
"COL",
|
|
1361
|
+
"VON",
|
|
1362
|
+
"NACH",
|
|
1363
|
+
"ABF",
|
|
1364
|
+
"ANK",
|
|
1365
|
+
"LIVE"
|
|
1366
|
+
];
|
|
1367
|
+
return !bad.includes(code);
|
|
1368
|
+
}
|
|
1369
|
+
function normalizeHtml(html) {
|
|
1370
|
+
return String(html || "").replace(/\\\//g, "/").replace(/&/g, "&").replace(///g, "/").replace(/"/g, '"').replace(/'/g, "'").replace(/\\u002F/g, "/").replace(/\\/g, "");
|
|
1371
|
+
}
|
|
1372
|
+
function htmlToText(html) {
|
|
1373
|
+
return decodeHtml(html).replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<\/(?:tr|td|th|div|section|article|li|p)>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
1374
|
+
}
|
|
1375
|
+
function decodeHtml(s) {
|
|
1376
|
+
return String(s || "").replace(/&/g, "&").replace(/ /g, " ").replace(/→/g, "\u2192").replace(/→/g, "\u2192").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
1377
|
+
}
|
|
1378
|
+
function normalizeImageUrl(url) {
|
|
1379
|
+
return String(url || "").replace(/\\\//g, "/").replace(/&/g, "&").replace(///g, "/").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
1380
|
+
}
|
|
1381
|
+
function containsAny(text, arr) {
|
|
1382
|
+
text = String(text || "").toUpperCase();
|
|
1383
|
+
return arr.some((x) => text.includes(String(x).toUpperCase()));
|
|
1384
|
+
}
|
|
1385
|
+
function clean(v) {
|
|
1386
|
+
return String(v || "").trim();
|
|
1387
|
+
}
|
|
1388
|
+
function errorText(e) {
|
|
1389
|
+
if (!e) {
|
|
1390
|
+
return "unbekannter Fehler";
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof e === "string") {
|
|
1393
|
+
return e;
|
|
1394
|
+
}
|
|
1395
|
+
if (e instanceof Error) {
|
|
1396
|
+
return e.message;
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
return JSON.stringify(e);
|
|
1400
|
+
} catch {
|
|
1401
|
+
return String(e);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1405
|
+
0 && (module.exports = {
|
|
1406
|
+
enrichFlightInfo,
|
|
1407
|
+
resolveImageViaFr24Aircraft
|
|
1408
|
+
});
|
|
1409
|
+
//# sourceMappingURL=flightInfo.js.map
|