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,281 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var airports_exports = {};
30
+ __export(airports_exports, {
31
+ parseAirportCsv: () => parseAirportCsv,
32
+ updateAirportJson: () => updateAirportJson
33
+ });
34
+ module.exports = __toCommonJS(airports_exports);
35
+ var https = __toESM(require("https"));
36
+ var import_airportNamesDe = require("./airportNamesDe");
37
+ const AIRPORTS_URL = "https://ourairports.com/data/airports.csv";
38
+ const RUNWAYS_URL = "https://ourairports.com/data/runways.csv";
39
+ const IATA_WIKI_DE_BASE = "https://de.wikipedia.org/wiki/Liste_der_IATA-Codes/";
40
+ function countryFlagEmoji(countryCode) {
41
+ if (!countryCode || countryCode.length !== 2) {
42
+ return "";
43
+ }
44
+ return countryCode.toUpperCase().replace(/./g, (char) => String.fromCodePoint(127397 + char.charCodeAt(0)));
45
+ }
46
+ async function updateAirportJson(adapter, logDebug, logWarn) {
47
+ try {
48
+ logDebug == null ? void 0 : logDebug("Lade Airport Datenbank...", 1);
49
+ const csv = await downloadCsv(AIRPORTS_URL, 0, "iata_code");
50
+ const runwayCsv = await downloadCsv(RUNWAYS_URL, 0, "airport_ident");
51
+ const runwaysByIdent = parseRunwayCsv(runwayCsv);
52
+ let airports = parseAirportCsv(csv).map((a) => ({
53
+ ...a,
54
+ runways: runwaysByIdent[a.ident] || []
55
+ }));
56
+ logDebug == null ? void 0 : logDebug(`Airport DB parsed: ${airports.length} Airports`, 1);
57
+ logDebug == null ? void 0 : logDebug(`Runways parsed: ${Object.keys(runwaysByIdent).length} Airports mit Runways`, 1);
58
+ try {
59
+ const deNames = await getGermanIataNamesCached(adapter, logDebug);
60
+ airports = airports.map((a) => ({
61
+ ...a,
62
+ city_DE: deNames[a.iata] || ""
63
+ }));
64
+ logDebug == null ? void 0 : logDebug(`Airport DB DE-Namen erg\xE4nzt: ${Object.keys(deNames).length}`, 1);
65
+ } catch (e) {
66
+ logWarn == null ? void 0 : logWarn(`Airport DB DE-Namen Fehler: ${(e == null ? void 0 : e.message) || e}`);
67
+ }
68
+ await adapter.setForeignStateAsync(`${adapter.namespace}.airportjson`, JSON.stringify(airports), true);
69
+ await adapter.setForeignStateAsync(
70
+ `${adapter.namespace}.airportjsonLastUpdate`,
71
+ (/* @__PURE__ */ new Date()).toISOString(),
72
+ true
73
+ );
74
+ logDebug == null ? void 0 : logDebug("Airport Datenbank aktualisiert", 1);
75
+ } catch (e) {
76
+ logWarn == null ? void 0 : logWarn(`Airport DB Fehler: ${(e == null ? void 0 : e.message) || e}`);
77
+ }
78
+ }
79
+ async function getGermanIataNamesCached(adapter, logDebug) {
80
+ try {
81
+ const st = await adapter.getForeignStateAsync(`${adapter.namespace}.airportjson`);
82
+ const raw = (st == null ? void 0 : st.val) ? String(st.val) : "";
83
+ if (raw && raw !== "[]") {
84
+ const airports = JSON.parse(raw);
85
+ if (Array.isArray(airports)) {
86
+ const cached = {};
87
+ for (const a of airports) {
88
+ const iata = String(a.iata || a.IATA || "").trim().toUpperCase();
89
+ const cityDe = String(a.city_DE || "").trim();
90
+ if (iata && cityDe) {
91
+ cached[iata] = cityDe;
92
+ }
93
+ }
94
+ if (Object.keys(cached).length > 100) {
95
+ logDebug == null ? void 0 : logDebug(`Airport DB DE-Namen aus Cache \xFCbernommen: ${Object.keys(cached).length}`, 1);
96
+ return cached;
97
+ }
98
+ }
99
+ }
100
+ } catch {
101
+ }
102
+ return (0, import_airportNamesDe.loadGermanIataNames)(logDebug);
103
+ }
104
+ function parseAirportCsv(csv) {
105
+ const lines = csv.split("\n").map((l) => l.trim()).filter(Boolean);
106
+ if (lines.length < 2) {
107
+ return [];
108
+ }
109
+ const headers = parseCsvLine(lines[0]);
110
+ const idx = (name) => headers.indexOf(name);
111
+ const result = [];
112
+ for (let i = 1; i < lines.length; i++) {
113
+ try {
114
+ const row = parseCsvLine(lines[i]);
115
+ const iata = String(row[idx("iata_code")] || "").trim().toUpperCase();
116
+ if (!iata || iata.length !== 3) {
117
+ continue;
118
+ }
119
+ const type = String(row[idx("type")] || "");
120
+ if (type === "closed") {
121
+ continue;
122
+ }
123
+ const lat = Number(row[idx("latitude_deg")]);
124
+ const lon = Number(row[idx("longitude_deg")]);
125
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
126
+ continue;
127
+ }
128
+ const country = String(row[idx("iso_country")] || "").trim().toUpperCase();
129
+ const ident = String(row[idx("ident")] || "").trim().toUpperCase();
130
+ result.push({
131
+ iata,
132
+ ident,
133
+ icao: String(row[idx("icao_code")] || row[idx("gps_code")] || ident || "").trim().toUpperCase(),
134
+ name: String(row[idx("name")] || "").trim(),
135
+ city: String(row[idx("municipality")] || "").trim(),
136
+ country,
137
+ flag: country,
138
+ flagEmoji: countryFlagEmoji(country),
139
+ lat,
140
+ lon,
141
+ type,
142
+ scheduled: String(row[idx("scheduled_service")] || "").trim().toLowerCase() === "yes"
143
+ });
144
+ } catch {
145
+ }
146
+ }
147
+ result.sort((a, b) => {
148
+ return a.iata.localeCompare(b.iata);
149
+ });
150
+ return result;
151
+ }
152
+ function parseRunwayCsv(csv) {
153
+ const lines = csv.split("\n").map((l) => l.trim()).filter(Boolean);
154
+ if (lines.length < 2) {
155
+ return {};
156
+ }
157
+ const headers = parseCsvLine(lines[0]);
158
+ const idx = (name) => headers.indexOf(name);
159
+ const result = {};
160
+ for (let i = 1; i < lines.length; i++) {
161
+ try {
162
+ const row = parseCsvLine(lines[i]);
163
+ const airportIdent = String(row[idx("airport_ident")] || "").trim().toUpperCase();
164
+ if (!airportIdent) {
165
+ continue;
166
+ }
167
+ if (String(row[idx("closed")] || "").trim() === "1") {
168
+ continue;
169
+ }
170
+ const leIdent = String(row[idx("le_ident")] || "").trim().toUpperCase();
171
+ const heIdent = String(row[idx("he_ident")] || "").trim().toUpperCase();
172
+ const leHeadingDegRaw = Number(row[idx("le_heading_degT")]);
173
+ const heHeadingDegRaw = Number(row[idx("he_heading_degT")]);
174
+ if (!leIdent && !heIdent) {
175
+ continue;
176
+ }
177
+ const runway = {
178
+ airportIdent,
179
+ lengthFt: Number(row[idx("length_ft")]) || 0,
180
+ widthFt: Number(row[idx("width_ft")]) || 0,
181
+ surface: String(row[idx("surface")] || "").trim(),
182
+ lighted: String(row[idx("lighted")] || "").trim() === "1",
183
+ leIdent,
184
+ leHeadingDeg: Number.isFinite(leHeadingDegRaw) ? Math.round(leHeadingDegRaw) : null,
185
+ heIdent,
186
+ heHeadingDeg: Number.isFinite(heHeadingDegRaw) ? Math.round(heHeadingDegRaw) : null
187
+ };
188
+ if (!result[airportIdent]) {
189
+ result[airportIdent] = [];
190
+ }
191
+ result[airportIdent].push(runway);
192
+ } catch {
193
+ }
194
+ }
195
+ for (const ident of Object.keys(result)) {
196
+ result[ident].sort((a, b) => Number(b.lengthFt || 0) - Number(a.lengthFt || 0));
197
+ }
198
+ return result;
199
+ }
200
+ function parseCsvLine(line) {
201
+ const result = [];
202
+ let current = "";
203
+ let inQuotes = false;
204
+ for (let i = 0; i < line.length; i++) {
205
+ const c = line[i];
206
+ if (c === '"') {
207
+ if (inQuotes && line[i + 1] === '"') {
208
+ current += '"';
209
+ i++;
210
+ } else {
211
+ inQuotes = !inQuotes;
212
+ }
213
+ continue;
214
+ }
215
+ if (c === "," && !inQuotes) {
216
+ result.push(current);
217
+ current = "";
218
+ continue;
219
+ }
220
+ current += c;
221
+ }
222
+ result.push(current);
223
+ return result;
224
+ }
225
+ function downloadCsv(url, redirects = 0, expectedHeader = "iata_code") {
226
+ return new Promise((resolve, reject) => {
227
+ const req = https.get(
228
+ url,
229
+ {
230
+ timeout: 2e4,
231
+ headers: {
232
+ "User-Agent": "Mozilla/5.0 JetFrame",
233
+ Accept: "text/csv,text/plain,*/*"
234
+ }
235
+ },
236
+ (res) => {
237
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
238
+ if (redirects >= 5) {
239
+ reject(new Error("Zu viele Redirects beim Airport CSV Download"));
240
+ return;
241
+ }
242
+ const nextUrl = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, url).toString();
243
+ resolve(downloadCsv(nextUrl, redirects + 1, expectedHeader));
244
+ return;
245
+ }
246
+ if (res.statusCode && res.statusCode >= 400) {
247
+ reject(new Error(`HTTP ${res.statusCode} beim Airport CSV Download`));
248
+ return;
249
+ }
250
+ let body = "";
251
+ res.setEncoding("utf8");
252
+ res.on("data", (chunk) => {
253
+ body += chunk;
254
+ });
255
+ res.on("end", () => {
256
+ const text = String(body || "").trim();
257
+ if (!text) {
258
+ return reject(new Error("Airport CSV leer"));
259
+ }
260
+ if (text.startsWith("<")) {
261
+ return reject(new Error("Airport CSV Download lieferte HTML statt CSV"));
262
+ }
263
+ if (expectedHeader && !text.includes(expectedHeader)) {
264
+ return reject(new Error(`CSV sieht ung\xFCltig aus: Header ${expectedHeader} fehlt`));
265
+ }
266
+ resolve(text);
267
+ });
268
+ }
269
+ );
270
+ req.on("timeout", () => {
271
+ req.destroy(new Error("Airport CSV Download Timeout"));
272
+ });
273
+ req.on("error", reject);
274
+ });
275
+ }
276
+ // Annotate the CommonJS export names for ESM import in node:
277
+ 0 && (module.exports = {
278
+ parseAirportCsv,
279
+ updateAirportJson
280
+ });
281
+ //# sourceMappingURL=airports.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/airports.ts"],
4
+ "sourcesContent": ["import type { AdapterLike } from './types';\nimport * as https from 'https';\nimport { loadGermanIataNames } from './airportNamesDe';\n\n/**\n *\n */\nexport interface AirportEntry {\n\t/**\n\t *\n\t */\n\tiata: string;\n\t/**\n\t *\n\t */\n\tident: string;\n\t/**\n\t *\n\t */\n\ticao: string;\n\t/**\n\t *\n\t */\n\tname: string;\n\t/**\n\t *\n\t */\n\tcity: string;\n\t/**\n\t *\n\t */\n\tcity_DE?: string;\n\n\t/**\n\t *\n\t */\n\tcountry: string;\n\t/**\n\t *\n\t */\n\tflag: string;\n\t/**\n\t *\n\t */\n\tflagEmoji: string;\n\n\t/**\n\t *\n\t */\n\tlat: number;\n\t/**\n\t *\n\t */\n\tlon: number;\n\n\t/**\n\t *\n\t */\n\ttype: string;\n\t/**\n\t *\n\t */\n\tscheduled: boolean;\n\t/**\n\t *\n\t */\n\trunways?: RunwayEntry[];\n}\n\nexport interface RunwayEntry {\n\tairportIdent: string;\n\tlengthFt: number;\n\twidthFt: number;\n\tsurface: string;\n\tlighted: boolean;\n\tleIdent: string;\n\tleHeadingDeg: number | null;\n\theIdent: string;\n\theHeadingDeg: number | null;\n}\n\nconst AIRPORTS_URL = 'https://ourairports.com/data/airports.csv';\nconst RUNWAYS_URL = 'https://ourairports.com/data/runways.csv';\n\nconst IATA_WIKI_DE_BASE = 'https://de.wikipedia.org/wiki/Liste_der_IATA-Codes/';\n\nfunction countryFlagEmoji(countryCode: string): string {\n\tif (!countryCode || countryCode.length !== 2) {\n\t\treturn '';\n\t}\n\n\treturn countryCode.toUpperCase().replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt(0)));\n}\n\n/**\n *\n */\nexport async function updateAirportJson(\n\tadapter: AdapterLike,\n\tlogDebug?: (msg: string, level?: number) => void,\n\tlogWarn?: (msg: string) => void,\n): Promise<void> {\n\ttry {\n\t\tlogDebug?.('Lade Airport Datenbank...', 1);\n\n\t\tconst csv = await downloadCsv(AIRPORTS_URL, 0, 'iata_code');\n\t\tconst runwayCsv = await downloadCsv(RUNWAYS_URL, 0, 'airport_ident');\n\n\t\tconst runwaysByIdent = parseRunwayCsv(runwayCsv);\n\n\t\tlet airports = parseAirportCsv(csv).map(a => ({\n\t\t\t...a,\n\t\t\trunways: runwaysByIdent[a.ident] || [],\n\t\t}));\n\n\t\tlogDebug?.(`Airport DB parsed: ${airports.length} Airports`, 1);\n\t\tlogDebug?.(`Runways parsed: ${Object.keys(runwaysByIdent).length} Airports mit Runways`, 1);\n\n\t\ttry {\n\t\t\tconst deNames = await getGermanIataNamesCached(adapter, logDebug);\n\n\t\t\tairports = airports.map(a => ({\n\t\t\t\t...a,\n\t\t\t\tcity_DE: deNames[a.iata] || '',\n\t\t\t}));\n\n\t\t\tlogDebug?.(`Airport DB DE-Namen erg\u00E4nzt: ${Object.keys(deNames).length}`, 1);\n\t\t} catch (e: any) {\n\t\t\tlogWarn?.(`Airport DB DE-Namen Fehler: ${e?.message || e}`);\n\t\t}\n\n\t\tawait adapter.setForeignStateAsync(`${adapter.namespace}.airportjson`, JSON.stringify(airports), true);\n\n\t\tawait adapter.setForeignStateAsync(\n\t\t\t`${adapter.namespace}.airportjsonLastUpdate`,\n\t\t\tnew Date().toISOString(),\n\t\t\ttrue,\n\t\t);\n\n\t\tlogDebug?.('Airport Datenbank aktualisiert', 1);\n\t} catch (e: any) {\n\t\tlogWarn?.(`Airport DB Fehler: ${e?.message || e}`);\n\t}\n}\n\nasync function getGermanIataNamesCached(\n\tadapter: AdapterLike,\n\tlogDebug?: (msg: string, level?: number) => void,\n): Promise<Record<string, string>> {\n\ttry {\n\t\tconst st = await adapter.getForeignStateAsync(`${adapter.namespace}.airportjson`);\n\n\t\tconst raw = st?.val ? String(st.val) : '';\n\n\t\tif (raw && raw !== '[]') {\n\t\t\tconst airports = JSON.parse(raw);\n\n\t\t\tif (Array.isArray(airports)) {\n\t\t\t\tconst cached: Record<string, string> = {};\n\n\t\t\t\tfor (const a of airports) {\n\t\t\t\t\tconst iata = String(a.iata || a.IATA || '')\n\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t.toUpperCase();\n\n\t\t\t\t\tconst cityDe = String(a.city_DE || '').trim();\n\n\t\t\t\t\tif (iata && cityDe) {\n\t\t\t\t\t\tcached[iata] = cityDe;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (Object.keys(cached).length > 100) {\n\t\t\t\t\tlogDebug?.(`Airport DB DE-Namen aus Cache \u00FCbernommen: ${Object.keys(cached).length}`, 1);\n\n\t\t\t\t\treturn cached;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// fallback\n\t}\n\n\treturn loadGermanIataNames(logDebug);\n}\n\nexport function parseAirportCsv(csv: string): AirportEntry[] {\n\tconst lines = csv\n\t\t.split('\\n')\n\t\t.map(l => l.trim())\n\t\t.filter(Boolean);\n\n\tif (lines.length < 2) {\n\t\treturn [];\n\t}\n\n\tconst headers = parseCsvLine(lines[0]);\n\n\tconst idx = (name: string): number => headers.indexOf(name);\n\n\tconst result: AirportEntry[] = [];\n\n\tfor (let i = 1; i < lines.length; i++) {\n\t\ttry {\n\t\t\tconst row = parseCsvLine(lines[i]);\n\n\t\t\tconst iata = String(row[idx('iata_code')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tif (!iata || iata.length !== 3) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst type = String(row[idx('type')] || '');\n\n\t\t\tif (type === 'closed') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst lat = Number(row[idx('latitude_deg')]);\n\n\t\t\tconst lon = Number(row[idx('longitude_deg')]);\n\n\t\t\tif (!Number.isFinite(lat) || !Number.isFinite(lon)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst country = String(row[idx('iso_country')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tconst ident = String(row[idx('ident')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tresult.push({\n\t\t\t\tiata,\n\n\t\t\t\tident,\n\n\t\t\t\ticao: String(row[idx('icao_code')] || row[idx('gps_code')] || ident || '')\n\t\t\t\t\t.trim()\n\t\t\t\t\t.toUpperCase(),\n\n\t\t\t\tname: String(row[idx('name')] || '').trim(),\n\n\t\t\t\tcity: String(row[idx('municipality')] || '').trim(),\n\n\t\t\t\tcountry,\n\n\t\t\t\tflag: country,\n\n\t\t\t\tflagEmoji: countryFlagEmoji(country),\n\n\t\t\t\tlat,\n\t\t\t\tlon,\n\n\t\t\t\ttype,\n\n\t\t\t\tscheduled:\n\t\t\t\t\tString(row[idx('scheduled_service')] || '')\n\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t.toLowerCase() === 'yes',\n\t\t\t});\n\t\t} catch {\n\t\t\t// ignore broken row\n\t\t}\n\t}\n\n\tresult.sort((a, b) => {\n\t\treturn a.iata.localeCompare(b.iata);\n\t});\n\n\treturn result;\n}\n\nfunction parseRunwayCsv(csv: string): Record<string, RunwayEntry[]> {\n\tconst lines = csv\n\t\t.split('\\n')\n\t\t.map(l => l.trim())\n\t\t.filter(Boolean);\n\n\tif (lines.length < 2) {\n\t\treturn {};\n\t}\n\n\tconst headers = parseCsvLine(lines[0]);\n\tconst idx = (name: string): number => headers.indexOf(name);\n\n\tconst result: Record<string, RunwayEntry[]> = {};\n\n\tfor (let i = 1; i < lines.length; i++) {\n\t\ttry {\n\t\t\tconst row = parseCsvLine(lines[i]);\n\n\t\t\tconst airportIdent = String(row[idx('airport_ident')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tif (!airportIdent) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (String(row[idx('closed')] || '').trim() === '1') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst leIdent = String(row[idx('le_ident')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tconst heIdent = String(row[idx('he_ident')] || '')\n\t\t\t\t.trim()\n\t\t\t\t.toUpperCase();\n\n\t\t\tconst leHeadingDegRaw = Number(row[idx('le_heading_degT')]);\n\t\t\tconst heHeadingDegRaw = Number(row[idx('he_heading_degT')]);\n\n\t\t\tif (!leIdent && !heIdent) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst runway: RunwayEntry = {\n\t\t\t\tairportIdent,\n\t\t\t\tlengthFt: Number(row[idx('length_ft')]) || 0,\n\t\t\t\twidthFt: Number(row[idx('width_ft')]) || 0,\n\t\t\t\tsurface: String(row[idx('surface')] || '').trim(),\n\t\t\t\tlighted: String(row[idx('lighted')] || '').trim() === '1',\n\t\t\t\tleIdent,\n\t\t\t\tleHeadingDeg: Number.isFinite(leHeadingDegRaw) ? Math.round(leHeadingDegRaw) : null,\n\t\t\t\theIdent,\n\t\t\t\theHeadingDeg: Number.isFinite(heHeadingDegRaw) ? Math.round(heHeadingDegRaw) : null,\n\t\t\t};\n\n\t\t\tif (!result[airportIdent]) {\n\t\t\t\tresult[airportIdent] = [];\n\t\t\t}\n\n\t\t\tresult[airportIdent].push(runway);\n\t\t} catch {\n\t\t\t// ignore broken runway row\n\t\t}\n\t}\n\n\tfor (const ident of Object.keys(result)) {\n\t\tresult[ident].sort((a, b) => Number(b.lengthFt || 0) - Number(a.lengthFt || 0));\n\t}\n\n\treturn result;\n}\n\nfunction parseCsvLine(line: string): string[] {\n\tconst result: string[] = [];\n\n\tlet current = '';\n\tlet inQuotes = false;\n\n\tfor (let i = 0; i < line.length; i++) {\n\t\tconst c = line[i];\n\n\t\tif (c === '\"') {\n\t\t\tif (inQuotes && line[i + 1] === '\"') {\n\t\t\t\tcurrent += '\"';\n\t\t\t\ti++;\n\t\t\t} else {\n\t\t\t\tinQuotes = !inQuotes;\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (c === ',' && !inQuotes) {\n\t\t\tresult.push(current);\n\t\t\tcurrent = '';\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrent += c;\n\t}\n\n\tresult.push(current);\n\n\treturn result;\n}\n\nfunction downloadCsv(url: string, redirects = 0, expectedHeader = 'iata_code'): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst req = https.get(\n\t\t\turl,\n\t\t\t{\n\t\t\t\ttimeout: 20000,\n\t\t\t\theaders: {\n\t\t\t\t\t'User-Agent': 'Mozilla/5.0 JetFrame',\n\t\t\t\t\tAccept: 'text/csv,text/plain,*/*',\n\t\t\t\t},\n\t\t\t},\n\t\t\tres => {\n\t\t\t\tif (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {\n\t\t\t\t\tif (redirects >= 5) {\n\t\t\t\t\t\treject(new Error('Zu viele Redirects beim Airport CSV Download'));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst nextUrl = res.headers.location.startsWith('http')\n\t\t\t\t\t\t? res.headers.location\n\t\t\t\t\t\t: new URL(res.headers.location, url).toString();\n\n\t\t\t\t\tresolve(downloadCsv(nextUrl, redirects + 1, expectedHeader));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (res.statusCode && res.statusCode >= 400) {\n\t\t\t\t\treject(new Error(`HTTP ${res.statusCode} beim Airport CSV Download`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet body = '';\n\t\t\t\tres.setEncoding('utf8');\n\n\t\t\t\tres.on('data', chunk => {\n\t\t\t\t\tbody += chunk;\n\t\t\t\t});\n\n\t\t\t\tres.on('end', () => {\n\t\t\t\t\tconst text = String(body || '').trim();\n\n\t\t\t\t\tif (!text) {\n\t\t\t\t\t\treturn reject(new Error('Airport CSV leer'));\n\t\t\t\t\t}\n\t\t\t\t\tif (text.startsWith('<')) {\n\t\t\t\t\t\treturn reject(new Error('Airport CSV Download lieferte HTML statt CSV'));\n\t\t\t\t\t}\n\t\t\t\t\tif (expectedHeader && !text.includes(expectedHeader)) {\n\t\t\t\t\t\treturn reject(new Error(`CSV sieht ung\u00FCltig aus: Header ${expectedHeader} fehlt`));\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve(text);\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\n\t\treq.on('timeout', () => {\n\t\t\treq.destroy(new Error('Airport CSV Download Timeout'));\n\t\t});\n\n\t\treq.on('error', reject);\n\t});\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,339 @@
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 classify_exports = {};
20
+ __export(classify_exports, {
21
+ classifyAircraft: () => classifyAircraft,
22
+ enrichAircraft: () => enrichAircraft,
23
+ findCurrentLive: () => findCurrentLive,
24
+ flightKey: () => flightKey,
25
+ getMatches: () => getMatches,
26
+ isDifferentAircraft: () => isDifferentAircraft
27
+ });
28
+ module.exports = __toCommonJS(classify_exports);
29
+ var import_geo = require("./geo");
30
+ function enrichAircraft(config, a) {
31
+ const bearingHomeDeg = (0, import_geo.bearingDeg)(config.homeLat, config.homeLon, a.lat, a.lon);
32
+ const distHomeNm = (0, import_geo.distanceNm)(config.homeLat, config.homeLon, a.lat, a.lon);
33
+ const distAirportNm = (0, import_geo.distanceNm)(config.airport.lat, config.airport.lon, a.lat, a.lon);
34
+ const bearingAircraftToAirportDeg = (0, import_geo.bearingDeg)(a.lat, a.lon, config.airport.lat, config.airport.lon);
35
+ const bearingAirportToAircraftDeg = (0, import_geo.bearingDeg)(config.airport.lat, config.airport.lon, a.lat, a.lon);
36
+ const landingTrackDiffDeg = (0, import_geo.smallestAngleDiff)(a.trackDeg, bearingAircraftToAirportDeg);
37
+ const takeoffTrackDiffDeg = (0, import_geo.smallestAngleDiff)(a.trackDeg, bearingAirportToAircraftDeg);
38
+ const airportTrackDiffDeg = Math.min(landingTrackDiffDeg, takeoffTrackDiffDeg);
39
+ const windowDiffDeg = (0, import_geo.signedAngleDiff)(bearingHomeDeg, config.windowBearingDeg);
40
+ const windowDiffAbsDeg = Math.abs(windowDiffDeg);
41
+ return {
42
+ ...a,
43
+ bearingHomeDeg,
44
+ distHomeNm,
45
+ distAirportNm,
46
+ bearingAircraftToAirportDeg,
47
+ bearingAirportToAircraftDeg,
48
+ landingTrackDiffDeg,
49
+ takeoffTrackDiffDeg,
50
+ airportTrackDiffDeg,
51
+ windowDiffDeg,
52
+ windowDiffAbsDeg,
53
+ inWindow: windowDiffAbsDeg <= config.windowFovDeg / 2
54
+ };
55
+ }
56
+ function classifyAircraft(config, a) {
57
+ const isLanding = (a.verticalRate || 0) <= config.minSinkRate && (a.landingTrackDiffDeg || 999) <= config.autoRunwayTrackToleranceDeg;
58
+ const isTakeoff = (a.verticalRate || 0) >= config.minClimbRate && (a.takeoffTrackDiffDeg || 999) <= config.autoRunwayTrackToleranceDeg;
59
+ if (isLanding) {
60
+ return {
61
+ ...a,
62
+ mode: "LANDING",
63
+ icon: "\u{1F6EC}",
64
+ directionText: `nach ${config.airport.iata}`,
65
+ relevant: true,
66
+ priority: 1
67
+ };
68
+ }
69
+ if (isTakeoff) {
70
+ return {
71
+ ...a,
72
+ mode: "TAKEOFF",
73
+ icon: "\u{1F6EB}",
74
+ directionText: `von ${config.airport.iata}`,
75
+ relevant: true,
76
+ priority: 2
77
+ };
78
+ }
79
+ const isOverflight = (config.overflightOnly || config.overflightEnabled) && (a.distHomeNm || 999) <= config.overflightMaxDistanceNm && a.altFt >= config.overflightMinAltitudeFt && a.altFt <= config.overflightMaxAltitudeFt && (!config.overflightRequiresWindow || !!a.inWindow);
80
+ if (isOverflight) {
81
+ return {
82
+ ...a,
83
+ mode: "OVERFLIGHT",
84
+ icon: "\u{1F6E9}\uFE0F",
85
+ directionText: "\xDCberflug",
86
+ relevant: true,
87
+ priority: 3
88
+ };
89
+ }
90
+ return {
91
+ ...a,
92
+ relevant: false
93
+ };
94
+ }
95
+ function getMatches(config, aircraft) {
96
+ const enriched = aircraft.map((a) => enrichEmergency(config, enrichAircraft(config, a)));
97
+ if (config.overflightOnly) {
98
+ return enriched.filter(
99
+ (a) => (a.distHomeNm || 999) <= config.overflightMaxDistanceNm && a.altFt >= config.overflightMinAltitudeFt && a.altFt <= config.overflightMaxAltitudeFt && (!config.overflightRequiresWindow || !!a.inWindow)
100
+ ).map((a) => ({
101
+ ...a,
102
+ mode: "OVERFLIGHT",
103
+ icon: "\u{1F6E9}\uFE0F",
104
+ directionText: "\xDCberflug",
105
+ relevant: true,
106
+ priority: 1
107
+ })).sort((a, b) => sortOverflightAircraft(config, a, b));
108
+ }
109
+ return enriched.filter((a) => {
110
+ const isAllowedOverflight = config.overflightEnabled && !config.overflightRequiresWindow && (a.distHomeNm || 999) <= config.overflightMaxDistanceNm && a.altFt >= config.overflightMinAltitudeFt && a.altFt <= config.overflightMaxAltitudeFt;
111
+ if (isAllowedOverflight) {
112
+ return true;
113
+ }
114
+ if (!a.inWindow) {
115
+ return false;
116
+ }
117
+ if ((a.distHomeNm || 999) > config.maxHomeDistanceNm) {
118
+ return false;
119
+ }
120
+ if (a.altFt < config.minAltitudeFt || a.altFt > config.maxAltitudeFt) {
121
+ return false;
122
+ }
123
+ return true;
124
+ }).map((a) => classifyAircraft(config, a)).filter((a) => {
125
+ if (!a.relevant) {
126
+ return false;
127
+ }
128
+ if (a.mode === "OVERFLIGHT" && !config.overflightEnabled) {
129
+ return false;
130
+ }
131
+ return true;
132
+ }).sort((a, b) => sortAircraft(config, a, b));
133
+ }
134
+ function enrichEmergency(config, a) {
135
+ const squawk = String(a.squawk || "").trim();
136
+ const emergency = String(a.emergency || "").trim().toLowerCase();
137
+ let emergencyType = "";
138
+ let emergencyText = "";
139
+ if (squawk === "7500" && config.emergencySquawk7500) {
140
+ emergencyType = "HIJACK";
141
+ emergencyText = "Besonderer Notfall-Code 7500";
142
+ } else if (squawk === "7600" && config.emergencySquawk7600) {
143
+ emergencyType = "RADIO";
144
+ emergencyText = "Funkausfall Squawk 7600";
145
+ } else if (squawk === "7700" && config.emergencySquawk7700) {
146
+ emergencyType = "EMERGENCY";
147
+ emergencyText = "Allgemeiner Notfall Squawk 7700";
148
+ } else if (emergency && emergency !== "none") {
149
+ emergencyType = emergency.toUpperCase();
150
+ emergencyText = `Emergency: ${emergency}`;
151
+ }
152
+ return {
153
+ ...a,
154
+ isEmergency: !!emergencyType,
155
+ emergencyType,
156
+ emergencyText
157
+ };
158
+ }
159
+ function emergencyBonus(config, a) {
160
+ if (!config.emergencyPriorityEnabled || !a.isEmergency) {
161
+ return 0;
162
+ }
163
+ const squawk = String(a.squawk || "").trim();
164
+ if (squawk === "7500") {
165
+ return 1e5;
166
+ }
167
+ if (squawk === "7700") {
168
+ return 9e4;
169
+ }
170
+ if (squawk === "7600") {
171
+ return 8e4;
172
+ }
173
+ return 7e4;
174
+ }
175
+ function sortOverflightAircraft(config, a, b) {
176
+ return candidateScore(config, b) - candidateScore(config, a);
177
+ }
178
+ function sortAircraft(config, a, b) {
179
+ return candidateScore(config, b) - candidateScore(config, a);
180
+ }
181
+ function candidateScore(config, a) {
182
+ let score = 0;
183
+ score += priorityBonus(config, a);
184
+ if (a.mode === "LANDING") {
185
+ score += 9e3;
186
+ }
187
+ if (a.mode === "TAKEOFF") {
188
+ score += 7500;
189
+ }
190
+ if (a.mode === "OVERFLIGHT") {
191
+ score += 5500;
192
+ }
193
+ score += centerWindowBonus(config, a);
194
+ if (a.mode === "LANDING") {
195
+ score += Math.max(0, 2500 - Math.abs(a.verticalRate || 0) / 2);
196
+ score += Math.max(0, 2200 - (a.landingTrackDiffDeg || 999) * 35);
197
+ score += Math.max(0, 1800 - (a.altFt || 0) / 4);
198
+ }
199
+ if (a.mode === "TAKEOFF") {
200
+ score += Math.max(0, 2500 - Math.abs(a.verticalRate || 0) / 2);
201
+ score += Math.max(0, 2200 - (a.takeoffTrackDiffDeg || 999) * 35);
202
+ }
203
+ score += Math.max(0, 2600 - (a.distHomeNm || 999) * 260);
204
+ score -= (a.seenSec || 0) * 40;
205
+ return score;
206
+ }
207
+ function centerWindowBonus(config, a) {
208
+ if (!a.inWindow) {
209
+ return -5e3;
210
+ }
211
+ const half = Math.max(1, config.windowFovDeg / 2);
212
+ const diff = Math.abs(a.windowDiffDeg || 0);
213
+ const normalized = Math.max(0, 1 - diff / half);
214
+ return Math.round(normalized * 12e3);
215
+ }
216
+ function priorityBonus(config, a) {
217
+ if (!config.priorityEnabled) {
218
+ return 0;
219
+ }
220
+ let bonus = 0;
221
+ bonus += emergencyBonus(config, a);
222
+ if (config.prioritySpecialLivery && hasSpecialLivery(a)) {
223
+ bonus += 6e4;
224
+ }
225
+ if (config.priorityMilitaryGov && isMilitaryOrGovernment(a)) {
226
+ bonus += 45e3;
227
+ }
228
+ if (config.priorityAircraftSize) {
229
+ bonus += aircraftSizeBonus(a);
230
+ }
231
+ return bonus;
232
+ }
233
+ function hasSpecialLivery(a) {
234
+ return !!String(
235
+ a.specialLiveryVisText || a.specialLiveryFull || a.specialLiveryTitle || a.specialLiveryDescription || a.specialText || ""
236
+ ).trim();
237
+ }
238
+ function aircraftSizeBonus(a) {
239
+ const text = String(a.aircraftSize || a.aircraftType || a.type || a.aircraftModel || "").toUpperCase();
240
+ if (/SUPERJUMBO|A388|A380/.test(text)) {
241
+ return 650;
242
+ }
243
+ if (/JUMBO|B748|B744|B742|B741|B747|B74/.test(text)) {
244
+ return 600;
245
+ }
246
+ if (/WIDEBODY|A300|A310|A330|A332|A333|A339|A340|A343|A346|A350|A359|A35K|A380|A388|B767|B763|B764|B777|B772|B77W|B77L|B787|B788|B789|B78X|MD11|DC10/.test(
247
+ text
248
+ )) {
249
+ return 450;
250
+ }
251
+ if (/NARROWBODY|A318|A319|A320|A321|A20N|A21N|B737|B738|B739|B38M|B39M|B3XM|A220|BCS|E190|E195|E290|E295/.test(text)) {
252
+ return 200;
253
+ }
254
+ return 0;
255
+ }
256
+ function isMilitaryOrGovernment(a) {
257
+ const text = [
258
+ a.callsign,
259
+ a.routeCallsign,
260
+ a.operationalCallsign,
261
+ a.airlineName,
262
+ a.airlineIcao,
263
+ a.airlineIata,
264
+ a.registration,
265
+ a.aircraftModel,
266
+ a.aircraftType,
267
+ a.type
268
+ ].map((v) => String(v || "").toUpperCase()).join(" ");
269
+ return /(GAF|GOV|MIL|NATO|NAF|USAF|RCH|CNV|IAM|BAF|FAF|RAF|RRR|DUKE|ASY|CFC|CTM|AME|FMY|BUNDESWEHR|LUFTWAFFE|AIR FORCE|ARMY|NAVY|GOVERNMENT|REGIERUNG|POLICE|POLIZEI)/.test(
270
+ text
271
+ );
272
+ }
273
+ function findCurrentLive(matches, target) {
274
+ if (!matches.length || !target) {
275
+ return null;
276
+ }
277
+ const targetHex = clean(target.hex).toLowerCase();
278
+ const targetCall = clean(target.callsign).toUpperCase();
279
+ return matches.find((a) => {
280
+ const aHex = clean(a.hex).toLowerCase();
281
+ const aCall = clean(a.callsign).toUpperCase();
282
+ if (aHex && targetHex && aHex === targetHex) {
283
+ return true;
284
+ }
285
+ if (aCall && targetCall && aCall === targetCall) {
286
+ return true;
287
+ }
288
+ return false;
289
+ }) || null;
290
+ }
291
+ function isDifferentAircraft(a, target) {
292
+ if (!a || !target) {
293
+ return false;
294
+ }
295
+ const aHex = clean(a.hex).toLowerCase();
296
+ const tHex = clean(target.hex).toLowerCase();
297
+ const aCall = clean(a.callsign).toUpperCase();
298
+ const tCall = clean(target.callsign).toUpperCase();
299
+ if (aCall && tCall && aCall === tCall) {
300
+ return false;
301
+ }
302
+ if (aHex && tHex && aHex === tHex) {
303
+ return false;
304
+ }
305
+ if (aCall && tCall) {
306
+ return aCall !== tCall;
307
+ }
308
+ if (aHex && tHex) {
309
+ return aHex !== tHex;
310
+ }
311
+ return false;
312
+ }
313
+ function flightKey(a) {
314
+ if (!a) {
315
+ return "";
316
+ }
317
+ const hex = clean(a.hex).toLowerCase();
318
+ const cs = clean(a.callsign).toUpperCase();
319
+ if (cs) {
320
+ return `CS:${cs}`;
321
+ }
322
+ if (hex) {
323
+ return `HEX:${hex}`;
324
+ }
325
+ return "";
326
+ }
327
+ function clean(v) {
328
+ return String(v || "").trim();
329
+ }
330
+ // Annotate the CommonJS export names for ESM import in node:
331
+ 0 && (module.exports = {
332
+ classifyAircraft,
333
+ enrichAircraft,
334
+ findCurrentLive,
335
+ flightKey,
336
+ getMatches,
337
+ isDifferentAircraft
338
+ });
339
+ //# sourceMappingURL=classify.js.map