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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -0
  3. package/admin/SF-Pro.ttf +0 -0
  4. package/admin/admin.d.ts +65 -0
  5. package/admin/frame.html +982 -0
  6. package/admin/frame.html.bak-aircraft-card-real-row-20260518-1608 +1236 -0
  7. package/admin/frame.html.bak-aircraft-card-structure-20260518-1517 +1236 -0
  8. package/admin/frame.html.bak-aircraft-logo-id-fix-20260518-1639 +1239 -0
  9. package/admin/frame.html.bak-shortcut-test +1236 -0
  10. package/admin/frame.html.bak-tablet-class-20260518-1729 +1239 -0
  11. package/admin/heatmap.html +216 -0
  12. package/admin/index.html +268 -0
  13. package/admin/index_m.html +1749 -0
  14. package/admin/jetframe.css +1260 -0
  15. package/admin/jetframe.css.bak-airbus-landscape-fix +4630 -0
  16. package/admin/jetframe.css.bak-aircraft-card-clean-equal-20260518-1438 +4899 -0
  17. package/admin/jetframe.css.bak-aircraft-card-real-row-20260518-1608 +4814 -0
  18. package/admin/jetframe.css.bak-aircraft-card-row-left-20260518-1525 +4604 -0
  19. package/admin/jetframe.css.bak-aircraft-card-slim-equal-20260518-1446 +4647 -0
  20. package/admin/jetframe.css.bak-aircraft-card-structure-20260518-1517 +4646 -0
  21. package/admin/jetframe.css.bak-aircraft-inline-final-20260518-1527 +4654 -0
  22. package/admin/jetframe.css.bak-aircraft-row-compact-fix-20260518-1639 +4763 -0
  23. package/admin/jetframe.css.bak-before-aircrafttype-purge +4818 -0
  24. package/admin/jetframe.css.bak-before-cleanup +4670 -0
  25. package/admin/jetframe.css.bak-before-remove-tablet-only-20260518-1711 +4896 -0
  26. package/admin/jetframe.css.bak-before-tablet-layout-rework-20260518-1650 +4914 -0
  27. package/admin/jetframe.css.bak-clean-duplicate-fonts-20260518-1340 +4975 -0
  28. package/admin/jetframe.css.bak-clean-old-index-fix-20260518-1937 +5167 -0
  29. package/admin/jetframe.css.bak-hardleft-airbus +4751 -0
  30. package/admin/jetframe.css.bak-index-iphone-landscape-20260518-1931 +5030 -0
  31. package/admin/jetframe.css.bak-index-landscape-final-20260518-1941 +5167 -0
  32. package/admin/jetframe.css.bak-index-landscape-real-20260518-1936 +5186 -0
  33. package/admin/jetframe.css.bak-landscape-compact-jumbo-bold-20260518-1343 +4802 -0
  34. package/admin/jetframe.css.bak-logo-align-final +4551 -0
  35. package/admin/jetframe.css.bak-logo-final2 +4551 -0
  36. package/admin/jetframe.css.bak-narrowbody-font-fix +4992 -0
  37. package/admin/jetframe.css.bak-nuke-airbus-align +4790 -0
  38. package/admin/jetframe.css.bak-pill-balance-20260518-1603 +4773 -0
  39. package/admin/jetframe.css.bak-pill-balance-fix +4910 -0
  40. package/admin/jetframe.css.bak-radar-fix-fonts +4710 -0
  41. package/admin/jetframe.css.bak-shortcut-test +4899 -0
  42. package/admin/jetframe.css.bak-smaller-aircraft-card-fonts-20260518-1345 +4897 -0
  43. package/admin/jetframe.css.bak-tablet-fix-real-20260518-1748 +4945 -0
  44. package/admin/jetframe.css.bak-tablet-fullscreen-fix-20260518-1804 +4972 -0
  45. package/admin/jetframe.css.bak-tablet-landscape-layout-20260518-1645 +4802 -0
  46. package/admin/jetframe.css.bak-tablet-layout-final-20260518-1839 +4802 -0
  47. package/admin/jetframe.css.bak-tablet-layout-v3-20260518-1729 +4802 -0
  48. package/admin/jetframe.css.bak-tablet-layout-v4-20260518-1801 +4957 -0
  49. package/admin/jetframe.css.bak-tablet-layout-v5-20260518-1843 +4970 -0
  50. package/admin/jetframe.css.bak-tablet-layout-v6-20260518-1848 +4958 -0
  51. package/admin/jetframe.css.bak-tablet-layout-v7-20260518-1909 +4985 -0
  52. package/admin/jetframe.css.bak-tablet-only-landscape-v2-20260518-1707 +4802 -0
  53. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1857 +5188 -0
  54. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1859 +5347 -0
  55. package/admin/jetframe.css.bak-tablet-pages-v2-20260519-190807 +5349 -0
  56. package/admin/jetframe.css.bak-typography-align-final +4818 -0
  57. package/admin/jetframe.png +0 -0
  58. package/admin/manifest.webmanifest +15 -0
  59. package/admin/src/app.tsx +58 -0
  60. package/admin/src/components/settings.tsx +97 -0
  61. package/admin/src/i18n/de.json +11 -0
  62. package/admin/src/i18n/en.json +11 -0
  63. package/admin/src/i18n/es.json +11 -0
  64. package/admin/src/i18n/fr.json +11 -0
  65. package/admin/src/i18n/i18n.d.ts +28 -0
  66. package/admin/src/i18n/it.json +11 -0
  67. package/admin/src/i18n/nl.json +11 -0
  68. package/admin/src/i18n/pl.json +11 -0
  69. package/admin/src/i18n/pt.json +11 -0
  70. package/admin/src/i18n/ru.json +11 -0
  71. package/admin/src/i18n/uk.json +11 -0
  72. package/admin/src/i18n/zh-cn.json +11 -0
  73. package/admin/src/index.tsx +25 -0
  74. package/admin/stats.html +228 -0
  75. package/admin/style.css +32 -0
  76. package/admin/tsconfig.json +11 -0
  77. package/admin/words.js +46 -0
  78. package/build/lib/adsb.js +218 -0
  79. package/build/lib/adsb.js.map +7 -0
  80. package/build/lib/airportNamesDe.js +131 -0
  81. package/build/lib/airportNamesDe.js.map +7 -0
  82. package/build/lib/airports.js +281 -0
  83. package/build/lib/airports.js.map +7 -0
  84. package/build/lib/classify.js +339 -0
  85. package/build/lib/classify.js.map +7 -0
  86. package/build/lib/config.js +103 -0
  87. package/build/lib/config.js.map +7 -0
  88. package/build/lib/flightInfo.js +1409 -0
  89. package/build/lib/flightInfo.js.map +7 -0
  90. package/build/lib/geo.js +84 -0
  91. package/build/lib/geo.js.map +7 -0
  92. package/build/lib/images.js +422 -0
  93. package/build/lib/images.js.map +7 -0
  94. package/build/lib/specialLiveries.js +342 -0
  95. package/build/lib/specialLiveries.js.map +7 -0
  96. package/build/lib/states.js +971 -0
  97. package/build/lib/states.js.map +7 -0
  98. package/build/lib/staticFiles.js +73 -0
  99. package/build/lib/staticFiles.js.map +7 -0
  100. package/build/lib/types.js +17 -0
  101. package/build/lib/types.js.map +7 -0
  102. package/build/lib/visConfig.js +52 -0
  103. package/build/lib/visConfig.js.map +7 -0
  104. package/build/main.js +1454 -0
  105. package/build/main.js.map +7 -0
  106. package/io-package.json +169 -0
  107. 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(/&amp;/g, "&").replace(/&#x2F;/g, "/").replace(/&quot;/g, '"').replace(/&#39;/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(/&amp;/g, "&").replace(/&nbsp;/g, " ").replace(/&rarr;/g, "\u2192").replace(/&#8594;/g, "\u2192").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
1377
+ }
1378
+ function normalizeImageUrl(url) {
1379
+ return String(url || "").replace(/\\\//g, "/").replace(/&amp;/g, "&").replace(/&#x2F;/g, "/").replace(/&quot;/g, '"').replace(/&#39;/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