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,1749 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>JetFrame</title>
6
+
7
+ <link
8
+ rel="stylesheet"
9
+ type="text/css"
10
+ href="../../css/adapter.css"
11
+ />
12
+ <link
13
+ rel="stylesheet"
14
+ type="text/css"
15
+ href="../../lib/css/materialize.css"
16
+ />
17
+ <link
18
+ rel="stylesheet"
19
+ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
20
+ />
21
+
22
+ <script src="../../lib/js/jquery-3.2.1.min.js"></script>
23
+ <script src="../../socket.io/socket.io.js"></script>
24
+ <script src="../../js/translate.js"></script>
25
+ <script src="../../lib/js/materialize.js"></script>
26
+ <script src="../../js/adapter-settings.js"></script>
27
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
28
+
29
+ <style>
30
+ html,
31
+ body {
32
+ height: 100%;
33
+ margin: 0;
34
+ padding: 0;
35
+ background: #1f1f1f;
36
+ overflow: hidden;
37
+ }
38
+ .adapter-container {
39
+ height: calc(100vh - 70px);
40
+ overflow-y: auto;
41
+ overflow-x: hidden;
42
+ padding: 14px 14px 140px 14px !important;
43
+ -webkit-overflow-scrolling: touch;
44
+ }
45
+ .desktop-layout {
46
+ display: grid;
47
+ grid-template-columns: 1fr 600px;
48
+ gap: 16px;
49
+ align-items: start;
50
+ }
51
+ .left-column,
52
+ .right-column {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 16px;
56
+ }
57
+ .jetframe-card {
58
+ background: #fff;
59
+ border-radius: 18px;
60
+ padding: 18px;
61
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
62
+ margin-bottom: 16px;
63
+ }
64
+ .desktop-layout .jetframe-card {
65
+ margin-bottom: 0;
66
+ }
67
+ .jetframe-card,
68
+ .jetframe-card div,
69
+ .jetframe-card span,
70
+ .jetframe-card label,
71
+ .jetframe-card input {
72
+ color: black !important;
73
+ -webkit-text-fill-color: black !important;
74
+ }
75
+ .jetframe-title {
76
+ font-size: 24px;
77
+ font-weight: 800;
78
+ margin-bottom: 12px;
79
+ }
80
+ .jetframe-hint {
81
+ font-size: 15px;
82
+ line-height: 1.45;
83
+ }
84
+ #map {
85
+ width: 100%;
86
+ height: 62vh;
87
+ min-height: 360px;
88
+ max-height: 620px;
89
+ border-radius: 16px;
90
+ overflow: hidden;
91
+ background: #ddd;
92
+ margin-top: 10px;
93
+ }
94
+ .map-info {
95
+ margin-top: 12px;
96
+ font-size: 14px;
97
+ line-height: 1.5;
98
+ }
99
+ .grid-2 {
100
+ display: grid;
101
+ grid-template-columns: 1fr 1fr;
102
+ gap: 12px;
103
+ }
104
+ .input-field input,
105
+ .input-field label {
106
+ color: black !important;
107
+ -webkit-text-fill-color: black !important;
108
+ }
109
+ .fw-search-row {
110
+ display: flex;
111
+ gap: 10px;
112
+ margin-bottom: 18px;
113
+ }
114
+ .fw-search-row input {
115
+ flex: 1;
116
+ height: 42px !important;
117
+ border-radius: 10px !important;
118
+ border: 1px solid #ccc !important;
119
+ padding: 0 10px !important;
120
+ background: white !important;
121
+ }
122
+ .fw-search-row button {
123
+ border: none;
124
+ border-radius: 10px;
125
+ padding: 0 14px;
126
+ font-weight: 700;
127
+ background: #2196f3;
128
+ color: white !important;
129
+ -webkit-text-fill-color: white !important;
130
+ }
131
+ .range-field label {
132
+ display: block;
133
+ font-weight: 700;
134
+ margin-bottom: 4px;
135
+ }
136
+
137
+ .jetframe-card textarea.value {
138
+ background: #ffffff !important;
139
+ color: #111111 !important;
140
+ -webkit-text-fill-color: #111111 !important;
141
+ border: 1px solid #bdbdbd !important;
142
+ border-radius: 14px !important;
143
+ padding: 14px !important;
144
+ min-height: 150px !important;
145
+ font-size: 15px !important;
146
+ line-height: 1.45 !important;
147
+ box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.12);
148
+ }
149
+
150
+ .jetframe-card textarea.value:focus {
151
+ border-color: #2196f3 !important;
152
+ box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.18) !important;
153
+ }
154
+
155
+ .dropdown-content {
156
+ background: #ffffff !important;
157
+ }
158
+
159
+ .dropdown-content li > span {
160
+ color: #111111 !important;
161
+ -webkit-text-fill-color: #111111 !important;
162
+ font-size: 18px !important;
163
+ }
164
+
165
+ .select-wrapper input.select-dropdown {
166
+ background: #ffffff !important;
167
+ color: #111111 !important;
168
+ -webkit-text-fill-color: #111111 !important;
169
+ border-bottom: 1px solid #999 !important;
170
+ font-size: 17px !important;
171
+ }
172
+
173
+ .speech-token {
174
+ display: inline-block;
175
+ margin: 5px 5px 0 0;
176
+ padding: 7px 11px;
177
+ border-radius: 999px;
178
+ background: #e3f2fd;
179
+ color: #0d47a1 !important;
180
+ -webkit-text-fill-color: #0d47a1 !important;
181
+ font-weight: 700;
182
+ cursor: pointer;
183
+ user-select: none;
184
+ }
185
+
186
+ .speech-preview {
187
+ margin-top: 12px;
188
+ padding: 12px;
189
+ border-radius: 14px;
190
+ background: #f5f5f5;
191
+ border: 1px solid #e0e0e0;
192
+ font-family: monospace;
193
+ font-size: 14px;
194
+ line-height: 1.45;
195
+ white-space: pre-wrap;
196
+ }
197
+
198
+ @media (max-width: 1100px) {
199
+ .desktop-layout {
200
+ display: flex !important;
201
+ flex-direction: column !important;
202
+ }
203
+ .right-column {
204
+ order: -10 !important;
205
+ }
206
+ .left-column {
207
+ order: 10 !important;
208
+ }
209
+ #map {
210
+ height: 360px;
211
+ min-height: unset;
212
+ max-height: unset;
213
+ }
214
+ }
215
+
216
+ @media (max-width: 700px) {
217
+ .grid-2 {
218
+ grid-template-columns: 1fr;
219
+ }
220
+ #map {
221
+ width: 100%;
222
+ height: 48vh;
223
+ min-height: 320px;
224
+ max-height: 430px;
225
+ border-radius: 16px;
226
+ overflow: hidden;
227
+ background: #ddd;
228
+ margin-top: 10px;
229
+ }
230
+ }
231
+
232
+ /* JetFrame mobile map compact final */
233
+ @media (max-width: 700px) {
234
+ #map {
235
+ height: 300px !important;
236
+ min-height: 300px !important;
237
+ max-height: 300px !important;
238
+ }
239
+ }
240
+
241
+ </style>
242
+
243
+ <script>
244
+ let map,
245
+ homeMarker,
246
+ airportMarker,
247
+ lineLayer,
248
+ coneLayer,
249
+ overflightCircle,
250
+ airportScanCircle,
251
+ homeScanCircle;
252
+ let AIRPORTS = [];
253
+ let airportSuggestionMap = {};
254
+ let homeSuggestionMap = {};
255
+ let homeSuggestTimer = null;
256
+ let adminOnChange = null;
257
+
258
+ const defaults = {
259
+ enabled: true,
260
+ airportIata: 'FRA',
261
+ airportName: 'Frankfurt',
262
+ airportLat: 50.035686,
263
+ airportLon: 8.562813,
264
+ homeLat: 50.08637,
265
+ homeLon: 8.69163,
266
+ windowBearingDeg: 184,
267
+ windowFovDeg: 120,
268
+ maxHomeDistanceNm: 3.5,
269
+ radiusNm: 15,
270
+ adsbCustomUrl: '',
271
+ simpleApiHost: '',
272
+ simpleApiPort: 8087,
273
+ visualSource: 'current',
274
+ minAltitudeFt: 1000,
275
+ maxAltitudeFt: 5000,
276
+ autoRunwayTrackToleranceDeg: 65,
277
+ minClimbRate: 60,
278
+ minSinkRate: -60,
279
+ searchPollSeconds: 20,
280
+ livePollSeconds: 5,
281
+ liveMaxSeconds: 120,
282
+ overflightEnabled: false,
283
+ overflightMaxDistanceNm: 1.2,
284
+ overflightMinAltitudeFt: 4000,
285
+ overflightMaxAltitudeFt: 45000,
286
+ overflightRequiresWindow: false,
287
+ priorityEnabled: true,
288
+ prioritySpecialLivery: true,
289
+ priorityAircraftSize: true,
290
+ priorityMilitaryGov: true,
291
+ emergencyPriorityEnabled: true,
292
+ emergencySquawk7500: true,
293
+ emergencySquawk7600: true,
294
+ emergencySquawk7700: true,
295
+
296
+ speechMode: 'browser',
297
+ speechTemplate:
298
+ '{modeSpeechText}: {airlineName} {bestCallsign} {routeDirectionText} {routeOtherAirport} in {altitudeFt} Fuss. {windowPositionSpeechText}.',
299
+ };
300
+
301
+ function clearJetFrameCache() {
302
+ try {
303
+ const id = getInstanceId() + '.clearImageCache';
304
+
305
+ if (typeof socket === 'undefined') {
306
+ alert('Socket nicht verfügbar');
307
+ return;
308
+ }
309
+
310
+ socket.emit('setState', id, true, function () {
311
+ if (typeof M !== 'undefined' && M.toast) {
312
+ M.toast({ html: 'JetFrame Cache wird geleert...' });
313
+ } else {
314
+ alert('JetFrame Cache wird geleert...');
315
+ }
316
+ });
317
+ } catch (e) {
318
+ alert('Cache konnte nicht geleert werden');
319
+ }
320
+ }
321
+
322
+ function markChanged() {
323
+ if (adminOnChange) adminOnChange();
324
+ }
325
+
326
+ function insertSpeechToken(token) {
327
+ const el = document.getElementById('speechTemplate');
328
+ if (!el) return;
329
+
330
+ const value = el.value || '';
331
+ const start = el.selectionStart || value.length;
332
+ const end = el.selectionEnd || value.length;
333
+ const insert = '{' + token + '}';
334
+
335
+ el.value = value.substring(0, start) + insert + value.substring(end);
336
+ el.focus();
337
+ el.selectionStart = el.selectionEnd = start + insert.length;
338
+
339
+ $(el).trigger('input');
340
+ updateSpeechPreview();
341
+ M.updateTextFields();
342
+ markChanged();
343
+ }
344
+
345
+ function setDefaultSpeechTemplate() {
346
+ $('#speechTemplate').val(
347
+ '{modeSpeechText}: {airlineName} {bestCallsign} {routeDirectionText} {routeOtherAirport} in {altitudeFt} Fuss. {windowPositionSpeechText}.',
348
+ );
349
+ updateSpeechPreview();
350
+ M.updateTextFields();
351
+ markChanged();
352
+ }
353
+
354
+ function updateSpeechPreview() {
355
+ const tpl = String($('#speechTemplate').val() || '');
356
+ const values = {
357
+ modeSpeechText: 'Landung',
358
+ airlineName: 'Lufthansa',
359
+ bestCallsign: 'LH0815',
360
+ routeDirectionText: 'aus',
361
+ routeOtherAirport: 'Istanbul',
362
+ altitudeFt: '4000',
363
+ windowPositionSpeechText: 'links vom Fenster',
364
+ aircraftTypeText: 'Airbus A321',
365
+ registration: 'D-AISO',
366
+ speedKt: '165',
367
+ };
368
+
369
+ const text = tpl
370
+ .replace(/\{([a-zA-Z0-9_]+)\}/g, function (_m, key) {
371
+ return values[key] || '';
372
+ })
373
+ .replace(/\s+/g, ' ')
374
+ .replace(/\s+\./g, '.')
375
+ .trim();
376
+
377
+ $('#speechPreviewText').text(text || 'Noch kein Ansage-Text gesetzt.');
378
+ }
379
+
380
+ function n(id, def) {
381
+ const raw = String($('#' + id).val() || '').replace(',', '.');
382
+ const v = Number(raw);
383
+ return Number.isFinite(v) ? v : def;
384
+ }
385
+
386
+ function s(id, def) {
387
+ const v = String($('#' + id).val() || '').trim();
388
+ return v || def;
389
+ }
390
+
391
+ function degToRad(d) {
392
+ return (d * Math.PI) / 180;
393
+ }
394
+ function radToDeg(r) {
395
+ return (r * 180) / Math.PI;
396
+ }
397
+ function norm(d) {
398
+ return ((Number(d) % 360) + 360) % 360;
399
+ }
400
+
401
+ function bearingDeg(lat1, lon1, lat2, lon2) {
402
+ const p1 = degToRad(lat1),
403
+ p2 = degToRad(lat2),
404
+ l1 = degToRad(lon1),
405
+ l2 = degToRad(lon2);
406
+ const y = Math.sin(l2 - l1) * Math.cos(p2);
407
+ const x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(l2 - l1);
408
+ return norm(radToDeg(Math.atan2(y, x)));
409
+ }
410
+
411
+ function angleDiff(a, b) {
412
+ let d = Math.abs(norm(a) - norm(b));
413
+ return d > 180 ? 360 - d : d;
414
+ }
415
+
416
+ function destPoint(lat, lon, bearing, nm) {
417
+ const R = 3440.065;
418
+ const d = nm / R;
419
+ const b = degToRad(bearing);
420
+ const lat1 = degToRad(lat);
421
+ const lon1 = degToRad(lon);
422
+
423
+ const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) + Math.cos(lat1) * Math.sin(d) * Math.cos(b));
424
+ const lon2 =
425
+ lon1 +
426
+ Math.atan2(
427
+ Math.sin(b) * Math.sin(d) * Math.cos(lat1),
428
+ Math.cos(d) - Math.sin(lat1) * Math.sin(lat2),
429
+ );
430
+
431
+ return [radToDeg(lat2), radToDeg(lon2)];
432
+ }
433
+
434
+ function conePoints(lat, lon, bearing, fov, nm) {
435
+ const pts = [[lat, lon]];
436
+ const start = bearing - fov / 2;
437
+ const end = bearing + fov / 2;
438
+
439
+ for (let i = 0; i <= 28; i++) {
440
+ pts.push(destPoint(lat, lon, start + (end - start) * (i / 28), nm));
441
+ }
442
+
443
+ pts.push([lat, lon]);
444
+ return pts;
445
+ }
446
+
447
+ function initMap() {
448
+ if (map || typeof L === 'undefined') return;
449
+
450
+ map = L.map('map').setView([defaults.homeLat, defaults.homeLon], 11);
451
+
452
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
453
+ maxZoom: 19,
454
+ attribution: '&copy; OpenStreetMap',
455
+ }).addTo(map);
456
+
457
+ // ADSB Overlay Pane ohne Maus-Events
458
+ if (!map.getPane('adsbPane')) {
459
+ map.createPane('adsbPane');
460
+ map.getPane('adsbPane').style.pointerEvents = 'none';
461
+ map.getPane('adsbPane').style.zIndex = 350;
462
+ }
463
+ }
464
+
465
+ function updateLabels() {
466
+ $('#windowBearingDegValue').html(n('windowBearingDeg', 184) + '&deg;');
467
+ $('#windowFovDegValue').html(n('windowFovDeg', 120) + '&deg;');
468
+ $('#maxHomeDistanceNmValue').html(n('maxHomeDistanceNm', 3.5) + ' NM');
469
+ $('#overflightMaxDistanceNmValue').html(n('overflightMaxDistanceNm', 1.2) + ' NM');
470
+ }
471
+
472
+ function updateMap() {
473
+ initMap();
474
+ updateLabels();
475
+
476
+ if (!map) return;
477
+
478
+ const hLat = n('homeLat', defaults.homeLat);
479
+ const hLon = n('homeLon', defaults.homeLon);
480
+ const aLat = n('airportLat', defaults.airportLat);
481
+ const aLon = n('airportLon', defaults.airportLon);
482
+ const bearing = n('windowBearingDeg', defaults.windowBearingDeg);
483
+ const fov = n('windowFovDeg', defaults.windowFovDeg);
484
+ const dist = n('maxHomeDistanceNm', defaults.maxHomeDistanceNm);
485
+ const overflightEnabled = $('#overflightEnabled').prop('checked');
486
+ const overflightNm = n('overflightMaxDistanceNm', 1.2);
487
+ const iata = s('airportIata', 'FRA').toUpperCase();
488
+
489
+ [
490
+ homeMarker,
491
+ airportMarker,
492
+ lineLayer,
493
+ coneLayer,
494
+ overflightCircle,
495
+ airportScanCircle,
496
+ homeScanCircle,
497
+ ].forEach(l => {
498
+ if (l) map.removeLayer(l);
499
+ });
500
+
501
+ overflightCircle = null;
502
+ airportScanCircle = null;
503
+ homeScanCircle = null;
504
+
505
+ homeMarker = L.marker([hLat, hLon]).addTo(map).bindPopup('Zuhause');
506
+ airportMarker = L.marker([aLat, aLon]).addTo(map).bindPopup(iata);
507
+ const adsbAirportNm = n('radiusNm', 15);
508
+
509
+ // ADSB Scan rund um Flughafen
510
+ airportScanCircle = L.circle([aLat, aLon], {
511
+ pane: 'adsbPane',
512
+ radius: adsbAirportNm * 1852,
513
+ color: '#1565c0',
514
+ fillColor: '#1565c0',
515
+ fillOpacity: 0.04,
516
+ weight: 2,
517
+ dashArray: '8,8',
518
+ interactive: false,
519
+ }).addTo(map);
520
+
521
+ airportScanCircle.bindPopup('🔵 ADSB Flughafen-Scan: ' + adsbAirportNm + ' NM');
522
+
523
+ // ADSB/Home Scan für Overflight
524
+ if (overflightEnabled) {
525
+ homeScanCircle = L.circle([hLat, hLon], {
526
+ pane: 'adsbPane',
527
+ radius: overflightNm * 1852,
528
+ color: '#8e24aa',
529
+ fillColor: '#8e24aa',
530
+ fillOpacity: 0.05,
531
+ weight: 2,
532
+ dashArray: '4,8',
533
+ interactive: false,
534
+ }).addTo(map);
535
+
536
+ homeScanCircle.bindPopup('🟣 ADSB Zuhause/Überflug-Scan: ' + overflightNm + ' NM');
537
+ }
538
+
539
+ lineLayer = L.polyline(
540
+ [
541
+ [hLat, hLon],
542
+ [aLat, aLon],
543
+ ],
544
+ {
545
+ color: '#555',
546
+ weight: 2,
547
+ dashArray: '6,8',
548
+ },
549
+ ).addTo(map);
550
+
551
+ const airportBearing = bearingDeg(hLat, hLon, aLat, aLon);
552
+ const diff = angleDiff(bearing, airportBearing);
553
+ const inView = diff <= fov / 2;
554
+
555
+ const overflightText = overflightEnabled
556
+ ? '&#128994; &Uuml;berflug-Kreis: <b>' + overflightNm + ' NM</b><br>'
557
+ : '';
558
+
559
+ coneLayer = L.polygon(conePoints(hLat, hLon, bearing, fov, dist), {
560
+ color: inView ? '#1976d2' : '#f57c00',
561
+ fillColor: inView ? '#2196f3' : '#ff9800',
562
+ fillOpacity: 0.25,
563
+ weight: 2,
564
+ interactive: true,
565
+ }).addTo(map);
566
+
567
+ coneLayer.bindPopup(
568
+ '🟦 Sichtfenster<br>' +
569
+ 'Richtung: <b>' +
570
+ norm(bearing).toFixed(0) +
571
+ '°</b><br>' +
572
+ 'Bereich: <b>' +
573
+ norm(bearing - fov / 2).toFixed(0) +
574
+ '° bis ' +
575
+ norm(bearing + fov / 2).toFixed(0) +
576
+ '°</b><br>' +
577
+ 'Distanz: <b>' +
578
+ dist +
579
+ ' NM</b>',
580
+ );
581
+
582
+ if (airportScanCircle) airportScanCircle.bringToBack();
583
+ if (homeScanCircle) homeScanCircle.bringToBack();
584
+ if (overflightCircle) overflightCircle.bringToBack();
585
+ coneLayer.bringToFront();
586
+
587
+ if (overflightEnabled) {
588
+ overflightCircle = L.circle([hLat, hLon], {
589
+ pane: 'adsbPane',
590
+ radius: overflightNm * 1852,
591
+ color: '#8e24aa',
592
+ fillColor: '#ba68c8',
593
+ fillOpacity: 0.12,
594
+ weight: 2,
595
+ }).addTo(map);
596
+
597
+ // kein Popup, liegt nur als optischer Kreis im Hintergrund
598
+ }
599
+
600
+ if (airportScanCircle) airportScanCircle.bringToBack();
601
+ if (homeScanCircle) homeScanCircle.bringToBack();
602
+ if (overflightCircle) overflightCircle.bringToBack();
603
+ if (coneLayer) coneLayer.bringToFront();
604
+
605
+ // Für den automatischen Zoom NICHT die großen ADSB-Scan-Kreise verwenden,
606
+ // sonst zoomt die Karte zu weit raus.
607
+ const fitLayers = [homeMarker, airportMarker, coneLayer].filter(Boolean);
608
+
609
+ if (overflightCircle) {
610
+ fitLayers.push(overflightCircle);
611
+ }
612
+
613
+ const group = L.featureGroup(fitLayers);
614
+ map.fitBounds(group.getBounds().pad(0.12));
615
+
616
+ $('#mapInfo').html(
617
+ '&#129695; Fenster: <b>' +
618
+ norm(bearing).toFixed(0) +
619
+ '&deg;</b><br>' +
620
+ '&#128065;&#65039; Sichtbereich: <b>' +
621
+ norm(bearing - fov / 2).toFixed(0) +
622
+ '&deg; bis ' +
623
+ norm(bearing + fov / 2).toFixed(0) +
624
+ '&deg;</b><br>' +
625
+ '&#128747; Flughafen von Zuhause: <b>' +
626
+ airportBearing.toFixed(0) +
627
+ '&deg;</b><br>' +
628
+ '🔵 ADSB Flughafen-Scan: <b>' +
629
+ adsbAirportNm +
630
+ ' NM</b><br>' +
631
+ overflightText +
632
+ (overflightEnabled ? '🟣 ADSB Zuhause/Überflug-Scan: <b>' + overflightNm + ' NM</b><br>' : '') +
633
+ (inView
634
+ ? '&#9989; Flughafen liegt im Sichtfenster.'
635
+ : '&#9888;&#65039; Flughafen liegt au&szlig;erhalb vom Sichtfenster.'),
636
+ );
637
+
638
+ setTimeout(() => map.invalidateSize(), 150);
639
+ }
640
+
641
+ function normalizeSearchText(text) {
642
+ return String(text || '')
643
+ .toLowerCase()
644
+ .replace(/ä/g, 'ae')
645
+ .replace(/ö/g, 'oe')
646
+ .replace(/ü/g, 'ue')
647
+ .replace(/ß/g, 'ss')
648
+ .normalize('NFD')
649
+ .replace(/[\u0300-\u036f]/g, '')
650
+ .trim();
651
+ }
652
+
653
+ function airportDisplayName(a) {
654
+ const iata = String(a.iata || a.IATA || '').toUpperCase();
655
+ const city = String(a.city_DE || a.city || a.municipality || '').trim();
656
+ const name = String(a.city_DE || a.name || a.airport || '').trim();
657
+
658
+ if (city) return iata + ' - ' + city;
659
+ if (name) return iata + ' - ' + name;
660
+ return iata;
661
+ }
662
+
663
+ function airportSearchBlob(a) {
664
+ return [a.iata, a.IATA, a.icao, a.ICAO, a.name, a.airport, a.city, a.municipality, a.country].join(' ');
665
+ }
666
+
667
+ function findLocalAirport(query) {
668
+ const q = normalizeSearchText(query);
669
+ if (!q) return null;
670
+
671
+ const exact = AIRPORTS.find(
672
+ a =>
673
+ normalizeSearchText(a.iata || a.IATA) === q ||
674
+ normalizeSearchText(a.city_DE || a.city || a.municipality) === q ||
675
+ normalizeSearchText(a.city_DE || a.name || a.airport) === q ||
676
+ normalizeSearchText(airportDisplayName(a)) === q,
677
+ );
678
+
679
+ if (exact) return exact;
680
+
681
+ return (
682
+ AIRPORTS.find(
683
+ a =>
684
+ normalizeSearchText(airportSearchBlob(a)).indexOf(q) >= 0 ||
685
+ normalizeSearchText(airportDisplayName(a)).indexOf(q) >= 0,
686
+ ) || null
687
+ );
688
+ }
689
+
690
+ function fillAirport(a) {
691
+ if (!a) return;
692
+
693
+ const iata = String(a.iata || a.IATA || '').toUpperCase();
694
+ const name = String(
695
+ a.city_DE || a.city || a.municipality || a.city_DE || a.name || a.airport || '',
696
+ ).trim();
697
+ const lat = Number(a.lat ?? a.latitude ?? a.latitude_deg);
698
+ const lon = Number(a.lon ?? a.longitude ?? a.longitude_deg);
699
+
700
+ if (!iata || !Number.isFinite(lat) || !Number.isFinite(lon)) {
701
+ return alert('Flughafen-Daten unvollständig');
702
+ }
703
+
704
+ $('#airportIata').val(iata);
705
+ $('#airportName').val(name || iata);
706
+ $('#airportLat').val(lat.toFixed(6));
707
+ $('#airportLon').val(lon.toFixed(6));
708
+
709
+ M.updateTextFields();
710
+ updateMap();
711
+ markChanged();
712
+ }
713
+
714
+ function getInstanceId() {
715
+ const url = String(window.location.href || '');
716
+
717
+ let m = url.match(/[?&#]instance=(\d+)/);
718
+ if (m) return 'jetframe.' + m[1];
719
+
720
+ m = url.match(/jetframe\.(\d+)/);
721
+ if (m) return 'jetframe.' + m[1];
722
+
723
+ return 'jetframe.0';
724
+ }
725
+
726
+ function loadAirportJsonFromState() {
727
+ try {
728
+ if (typeof socket === 'undefined') {
729
+ AIRPORTS = [];
730
+ return;
731
+ }
732
+
733
+ const id = getInstanceId() + '.airportjson';
734
+
735
+ socket.emit('getState', id, function (err, state) {
736
+ try {
737
+ const raw = state && state.val ? String(state.val) : '[]';
738
+ const arr = JSON.parse(raw);
739
+
740
+ if (Array.isArray(arr) && arr.length) {
741
+ AIRPORTS = arr;
742
+ updateAirportSuggestions();
743
+ console.log('JetFrame airports loaded:', AIRPORTS.length);
744
+ } else {
745
+ AIRPORTS = [];
746
+ console.warn('JetFrame airportjson leer:', id);
747
+ }
748
+ } catch (e) {
749
+ AIRPORTS = [];
750
+ console.warn('JetFrame airportjson parse Fehler:', e);
751
+ }
752
+ });
753
+ } catch (e) {
754
+ AIRPORTS = [];
755
+ }
756
+ }
757
+
758
+ async function geocode(query, limit) {
759
+ const url =
760
+ 'https://nominatim.openstreetmap.org/search?' +
761
+ new URLSearchParams({
762
+ q: query,
763
+ format: 'json',
764
+ addressdetails: 1,
765
+ limit: limit || 5,
766
+ });
767
+
768
+ const res = await fetch(url, {
769
+ headers: { Accept: 'application/json' },
770
+ });
771
+
772
+ return await res.json();
773
+ }
774
+
775
+ function updateAirportSuggestions() {
776
+ const q = $('#airportSearch').val().trim();
777
+ const list = $('#airportSuggestions');
778
+
779
+ airportSuggestionMap = {};
780
+ list.empty();
781
+
782
+ if (q.length < 2) return;
783
+
784
+ const qNorm = normalizeSearchText(q);
785
+
786
+ AIRPORTS.filter(
787
+ a =>
788
+ normalizeSearchText(a.iata || a.IATA).indexOf(qNorm) >= 0 ||
789
+ normalizeSearchText(a.city_DE || a.city || a.municipality).indexOf(qNorm) >= 0 ||
790
+ normalizeSearchText(a.city_DE || a.name || a.airport).indexOf(qNorm) >= 0 ||
791
+ normalizeSearchText(airportDisplayName(a)).indexOf(qNorm) >= 0,
792
+ )
793
+ .slice(0, 12)
794
+ .forEach(a => {
795
+ const value = airportDisplayName(a);
796
+ const iata = String(a.iata || a.IATA || '').toUpperCase();
797
+
798
+ if (!iata || airportSuggestionMap[value]) return;
799
+
800
+ airportSuggestionMap[value] = a;
801
+ airportSuggestionMap[iata] = a;
802
+
803
+ if (a.city) airportSuggestionMap[a.city] = a;
804
+ if (a.name) airportSuggestionMap[a.name] = a;
805
+
806
+ list.append('<option value="' + value.replace(/"/g, '&quot;') + '"></option>');
807
+ });
808
+ }
809
+
810
+ function updateHomeSuggestions() {
811
+ const q = $('#homeSearch').val().trim();
812
+ const list = $('#homeSuggestions');
813
+
814
+ homeSuggestionMap = {};
815
+ list.empty();
816
+
817
+ if (q.length < 3) return;
818
+
819
+ clearTimeout(homeSuggestTimer);
820
+
821
+ homeSuggestTimer = setTimeout(async function () {
822
+ try {
823
+ const result = await geocode(q, 6);
824
+
825
+ result.forEach(r => {
826
+ const label = r.display_name;
827
+ homeSuggestionMap[label] = r;
828
+ list.append('<option value="' + label.replace(/"/g, '&quot;') + '"></option>');
829
+ });
830
+ } catch (e) {}
831
+ }, 350);
832
+ }
833
+
834
+ async function searchAirport() {
835
+ const q = $('#airportSearch').val().trim();
836
+
837
+ if (!q) return;
838
+
839
+ const selected = airportSuggestionMap[q];
840
+
841
+ if (selected) {
842
+ fillAirport(selected);
843
+ return;
844
+ }
845
+
846
+ const local = findLocalAirport(q);
847
+
848
+ if (local) {
849
+ fillAirport(local);
850
+ return;
851
+ }
852
+
853
+ alert('Kein Flughafen gefunden');
854
+ }
855
+
856
+ async function searchHome() {
857
+ const q = $('#homeSearch').val().trim();
858
+
859
+ if (!q) return;
860
+
861
+ let r = homeSuggestionMap[q];
862
+
863
+ if (!r) {
864
+ const result = await geocode(q, 1);
865
+
866
+ if (!result.length) {
867
+ return alert('Adresse nicht gefunden');
868
+ }
869
+
870
+ r = result[0];
871
+ }
872
+
873
+ $('#homeLat').val(Number(r.lat).toFixed(6));
874
+ $('#homeLon').val(Number(r.lon).toFixed(6));
875
+
876
+ M.updateTextFields();
877
+ updateMap();
878
+ markChanged();
879
+ }
880
+
881
+ function load(settings, onChange) {
882
+ adminOnChange = onChange;
883
+ settings = Object.assign({}, defaults, settings || {});
884
+
885
+ loadAirportJsonFromState();
886
+
887
+ $('.value').each(function () {
888
+ const key = $(this).attr('id');
889
+
890
+ if ($(this).attr('type') === 'checkbox') {
891
+ $(this).prop('checked', !!settings[key]);
892
+ } else {
893
+ $(this).val(settings[key]);
894
+ }
895
+
896
+ $(this).on('input change keyup', function () {
897
+ updateMap();
898
+ onChange();
899
+ });
900
+ });
901
+
902
+ $('#airportSearch').on('input keyup', updateAirportSuggestions);
903
+ $('#homeSearch').on('input keyup', updateHomeSuggestions);
904
+ $('#speechTemplate').on('input change keyup', updateSpeechPreview);
905
+
906
+ $('#airportSearch').on('input change', function () {
907
+ const val = String($(this).val() || '').trim();
908
+
909
+ if (airportSuggestionMap[val]) {
910
+ fillAirport(airportSuggestionMap[val]);
911
+ return;
912
+ }
913
+
914
+ const local = findLocalAirport(val);
915
+
916
+ if (local && val.length >= 3) {
917
+ fillAirport(local);
918
+ }
919
+ });
920
+
921
+ $('#homeSearch').on('change', function () {
922
+ const val = $(this).val();
923
+ const r = homeSuggestionMap[val];
924
+
925
+ if (r) {
926
+ $('#homeLat').val(Number(r.lat).toFixed(6));
927
+ $('#homeLon').val(Number(r.lon).toFixed(6));
928
+ M.updateTextFields();
929
+ updateMap();
930
+ markChanged();
931
+ }
932
+ });
933
+
934
+ M.updateTextFields();
935
+
936
+ if (M.FormSelect) {
937
+ M.FormSelect.init(document.querySelectorAll('select'));
938
+ }
939
+
940
+ updateSpeechPreview();
941
+
942
+ setTimeout(function () {
943
+ updateMap();
944
+ onChange(false);
945
+ }, 400);
946
+ }
947
+
948
+ function save(callback) {
949
+ const obj = {};
950
+
951
+ $('.value').each(function () {
952
+ const key = $(this).attr('id');
953
+
954
+ if ($(this).attr('type') === 'checkbox') {
955
+ obj[key] = $(this).prop('checked');
956
+ } else if ($(this).attr('type') === 'number' || $(this).attr('type') === 'range') {
957
+ obj[key] = Number($(this).val());
958
+ } else {
959
+ obj[key] = $(this).val();
960
+ }
961
+ });
962
+
963
+ callback(obj);
964
+ }
965
+ </script>
966
+
967
+ <style>
968
+ @media (min-width: 1000px) {
969
+ .jetframe-map-card,
970
+ #mapCard,
971
+ .map-card {
972
+ position: sticky !important;
973
+ top: 24px !important;
974
+ align-self: flex-start !important;
975
+ z-index: 5 !important;
976
+ }
977
+ }
978
+ </style>
979
+
980
+ <style id="jetframe-sticky-final">
981
+ @media (min-width: 1100px) {
982
+ .leaflet-container,
983
+ #map {
984
+ position: relative !important;
985
+ top: auto !important;
986
+ }
987
+
988
+ .right-column,
989
+ .map-column,
990
+ .sidebar-column {
991
+ position: sticky !important;
992
+ top: 18px !important;
993
+ align-self: start !important;
994
+ height: fit-content !important;
995
+ }
996
+ }
997
+ </style>
998
+
999
+ <style id="jetframe-speech-preview-fix">
1000
+ .speech-preview,
1001
+ #speechPreview,
1002
+ .preview-box,
1003
+ #speechPreviewBox,
1004
+ [id*='preview'],
1005
+ [class*='preview'] {
1006
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif !important;
1007
+ white-space: normal !important;
1008
+ word-break: normal !important;
1009
+ overflow-wrap: anywhere !important;
1010
+ line-height: 1.45 !important;
1011
+ }
1012
+
1013
+ .speech-preview pre,
1014
+ #speechPreview pre,
1015
+ .preview-box pre,
1016
+ #speechPreviewBox pre,
1017
+ [id*='preview'] pre,
1018
+ [class*='preview'] pre {
1019
+ font-family: inherit !important;
1020
+ white-space: normal !important;
1021
+ margin: 0 !important;
1022
+ }
1023
+ </style>
1024
+
1025
+ <style id="jetframe-overflight-only-clean">
1026
+ .jf-overflight-only {
1027
+ margin-top: 18px;
1028
+ margin-bottom: 18px;
1029
+ padding: 0;
1030
+ }
1031
+
1032
+ .jf-overflight-only-title {
1033
+ font-size: 16px !important;
1034
+ font-weight: 700 !important;
1035
+ line-height: 1.3 !important;
1036
+ margin-bottom: 8px !important;
1037
+ color: #000 !important;
1038
+ -webkit-text-fill-color: #000 !important;
1039
+ }
1040
+
1041
+ .jf-overflight-only .jetframe-hint {
1042
+ margin-top: 8px;
1043
+ font-size: 15px !important;
1044
+ line-height: 1.45 !important;
1045
+ color: #000 !important;
1046
+ -webkit-text-fill-color: #000 !important;
1047
+ opacity: 0.72;
1048
+ }
1049
+
1050
+ .switch label {
1051
+ min-height: 44px;
1052
+ display: inline-flex !important;
1053
+ align-items: center;
1054
+ gap: 12px;
1055
+ cursor: pointer;
1056
+ user-select: none;
1057
+ -webkit-tap-highlight-color: transparent;
1058
+ }
1059
+
1060
+ .switch label .lever {
1061
+ margin: 0 8px !important;
1062
+ }
1063
+ </style>
1064
+
1065
+ <style id="jetframe-map-width-fix-final">
1066
+ @media (max-width: 1100px) {
1067
+ .desktop-layout,
1068
+ .left-column,
1069
+ .right-column {
1070
+ width: 100% !important;
1071
+ max-width: 100% !important;
1072
+ }
1073
+
1074
+ .right-column {
1075
+ display: block !important;
1076
+ }
1077
+
1078
+ .right-column > .jetframe-card {
1079
+ width: 100% !important;
1080
+ max-width: 100% !important;
1081
+ margin: 0 !important;
1082
+ box-sizing: border-box !important;
1083
+ }
1084
+
1085
+ #map {
1086
+ width: 100% !important;
1087
+ }
1088
+ }
1089
+ </style>
1090
+
1091
+ </head>
1092
+
1093
+ <body>
1094
+ <div class="m adapter-container">
1095
+ <div class="jetframe-card intro-card">
1096
+ <div class="jetframe-title">&#9992;&#65039; JetFrame</div>
1097
+ <div class="jetframe-hint">Konfiguriere Flughafen, Zuhause und Sichtfenster.</div>
1098
+ </div>
1099
+
1100
+ <div class="desktop-layout">
1101
+ <div class="left-column">
1102
+ <div class="jetframe-card">
1103
+ <div class="jetframe-title">&#9881;&#65039; Allgemein</div>
1104
+ <div class="switch">
1105
+ <label>
1106
+ Off
1107
+ <input
1108
+ id="enabled"
1109
+ class="value"
1110
+ type="checkbox"
1111
+ />
1112
+ <span class="lever"></span>
1113
+ On
1114
+ </label>
1115
+ </div>
1116
+ </div>
1117
+
1118
+ <div class="jetframe-card">
1119
+ <div class="jetframe-title">&#128266; Sprachausgabe</div>
1120
+
1121
+ <div class="input-field">
1122
+ <select
1123
+ id="speechMode"
1124
+ class="value"
1125
+ >
1126
+ <option value="browser">Browser intern</option>
1127
+ <option value="external">Extern per Datenpunkt</option>
1128
+ <option value="both">Browser + extern</option>
1129
+ <option value="off">Aus</option>
1130
+ </select>
1131
+ <label>Modus</label>
1132
+ </div>
1133
+
1134
+ <div class="input-field">
1135
+ <textarea
1136
+ id="speechTemplate"
1137
+ class="materialize-textarea value"
1138
+ ></textarea>
1139
+ <label for="speechTemplate">Ansage-Text</label>
1140
+ </div>
1141
+
1142
+ <div>
1143
+ <span
1144
+ class="speech-token"
1145
+ onclick="insertSpeechToken('modeSpeechText')"
1146
+ >Start/Landung</span
1147
+ >
1148
+ <span
1149
+ class="speech-token"
1150
+ onclick="insertSpeechToken('airlineName')"
1151
+ >Airline</span
1152
+ >
1153
+ <span
1154
+ class="speech-token"
1155
+ onclick="insertSpeechToken('bestCallsign')"
1156
+ >Callsign</span
1157
+ >
1158
+ <span
1159
+ class="speech-token"
1160
+ onclick="insertSpeechToken('routeDirectionText')"
1161
+ >aus/nach</span
1162
+ >
1163
+ <span
1164
+ class="speech-token"
1165
+ onclick="insertSpeechToken('routeOtherAirport')"
1166
+ >Ort</span
1167
+ >
1168
+ <span
1169
+ class="speech-token"
1170
+ onclick="insertSpeechToken('altitudeFt')"
1171
+ >Höhe</span
1172
+ >
1173
+ <span
1174
+ class="speech-token"
1175
+ onclick="insertSpeechToken('windowPositionSpeechText')"
1176
+ >Fensterposition</span
1177
+ >
1178
+ </div>
1179
+
1180
+ <div style="margin-top: 10px">
1181
+ <button
1182
+ type="button"
1183
+ class="btn-small blue"
1184
+ onclick="setDefaultSpeechTemplate()"
1185
+ >
1186
+ Standardtext setzen
1187
+ </button>
1188
+ </div>
1189
+
1190
+ <div class="speech-preview">
1191
+ <b>Vorschau:</b><br />
1192
+ <span id="speechPreviewText">Noch kein Ansage-Text gesetzt.</span>
1193
+ </div>
1194
+ </div>
1195
+
1196
+ <div class="jetframe-card">
1197
+ <div class="jetframe-title">&#128747; Flughafen</div>
1198
+
1199
+ <div class="fw-search-row">
1200
+ <input
1201
+ id="airportSearch"
1202
+ type="text"
1203
+ list="airportSuggestions"
1204
+ placeholder="FRA / Frankfurt / M&uuml;nchen"
1205
+ />
1206
+ <datalist id="airportSuggestions"></datalist>
1207
+ <button
1208
+ type="button"
1209
+ onclick="searchAirport()"
1210
+ >
1211
+ Suchen
1212
+ </button>
1213
+ </div>
1214
+
1215
+ <div class="grid-2">
1216
+ <div class="input-field">
1217
+ <input
1218
+ id="airportIata"
1219
+ class="value"
1220
+ type="text"
1221
+ />
1222
+ <label for="airportIata">IATA</label>
1223
+ </div>
1224
+
1225
+ <div class="input-field">
1226
+ <input
1227
+ id="airportName"
1228
+ class="value"
1229
+ type="text"
1230
+ />
1231
+ <label for="airportName">Name</label>
1232
+ </div>
1233
+
1234
+ <div class="input-field">
1235
+ <input
1236
+ id="airportLat"
1237
+ class="value"
1238
+ type="number"
1239
+ step="0.000001"
1240
+ />
1241
+ <label for="airportLat">Latitude</label>
1242
+ </div>
1243
+
1244
+ <div class="input-field">
1245
+ <input
1246
+ id="airportLon"
1247
+ class="value"
1248
+ type="number"
1249
+ step="0.000001"
1250
+ />
1251
+ <label for="airportLon">Longitude</label>
1252
+ </div>
1253
+ </div>
1254
+ </div>
1255
+
1256
+ <div class="jetframe-card">
1257
+ <div class="jetframe-title">&#127968; Zuhause</div>
1258
+
1259
+ <div class="fw-search-row">
1260
+ <input
1261
+ id="homeSearch"
1262
+ type="text"
1263
+ list="homeSuggestions"
1264
+ placeholder="Adresse / Ort / PLZ"
1265
+ />
1266
+ <datalist id="homeSuggestions"></datalist>
1267
+ <button
1268
+ type="button"
1269
+ onclick="searchHome()"
1270
+ >
1271
+ Suchen
1272
+ </button>
1273
+ </div>
1274
+
1275
+ <div class="grid-2">
1276
+ <div class="input-field">
1277
+ <input
1278
+ id="homeLat"
1279
+ class="value"
1280
+ type="number"
1281
+ step="0.000001"
1282
+ />
1283
+ <label for="homeLat">Latitude</label>
1284
+ </div>
1285
+
1286
+ <div class="input-field">
1287
+ <input
1288
+ id="homeLon"
1289
+ class="value"
1290
+ type="number"
1291
+ step="0.000001"
1292
+ />
1293
+ <label for="homeLon">Longitude</label>
1294
+ </div>
1295
+ </div>
1296
+ </div>
1297
+
1298
+ <div class="jetframe-card">
1299
+ <div class="jetframe-title">&#129695; Sichtfenster</div>
1300
+
1301
+ <div class="range-field">
1302
+ <label>Fensterrichtung: <span id="windowBearingDegValue"></span></label>
1303
+ <input
1304
+ id="windowBearingDeg"
1305
+ class="value"
1306
+ type="range"
1307
+ min="0"
1308
+ max="359"
1309
+ step="1"
1310
+ />
1311
+ </div>
1312
+
1313
+ <div class="range-field">
1314
+ <label>Sichtfeld: <span id="windowFovDegValue"></span></label>
1315
+ <input
1316
+ id="windowFovDeg"
1317
+ class="value"
1318
+ type="range"
1319
+ min="10"
1320
+ max="180"
1321
+ step="1"
1322
+ />
1323
+ </div>
1324
+
1325
+ <div class="range-field">
1326
+ <label>Max Distanz Zuhause: <span id="maxHomeDistanceNmValue"></span></label>
1327
+ <input
1328
+ id="maxHomeDistanceNm"
1329
+ class="value"
1330
+ type="range"
1331
+ min="1"
1332
+ max="30"
1333
+ step="0.5"
1334
+ />
1335
+ </div>
1336
+ </div>
1337
+
1338
+ <div class="jetframe-card">
1339
+ <div class="jetframe-title">&#128225; Empfang & Filter</div>
1340
+
1341
+ <div class="grid-2">
1342
+ <div class="input-field">
1343
+ <input
1344
+ id="radiusNm"
1345
+ class="value"
1346
+ type="number"
1347
+ step="0.5"
1348
+ />
1349
+ <label for="radiusNm">ADSB-Scan Flughafen (NM)</label>
1350
+ </div>
1351
+
1352
+ <div class="input-field">
1353
+ <input
1354
+ id="adsbCustomUrl"
1355
+ class="value"
1356
+ type="text"
1357
+ />
1358
+ <label for="adsbCustomUrl">Eigene ADSB-URL optional</label>
1359
+ </div>
1360
+ <div class="input-field">
1361
+ <input
1362
+ id="autoRunwayTrackToleranceDeg"
1363
+ class="value"
1364
+ type="number"
1365
+ />
1366
+ <label for="autoRunwayTrackToleranceDeg">Kurs-Toleranz Grad</label>
1367
+ </div>
1368
+
1369
+ <div class="input-field">
1370
+ <input
1371
+ id="minAltitudeFt"
1372
+ class="value"
1373
+ type="number"
1374
+ />
1375
+ <label for="minAltitudeFt">Min Höhe ft</label>
1376
+ </div>
1377
+
1378
+ <div class="input-field">
1379
+ <input
1380
+ id="maxAltitudeFt"
1381
+ class="value"
1382
+ type="number"
1383
+ />
1384
+ <label for="maxAltitudeFt">Max Höhe ft</label>
1385
+ </div>
1386
+
1387
+ <div class="input-field">
1388
+ <input
1389
+ id="minClimbRate"
1390
+ class="value"
1391
+ type="number"
1392
+ />
1393
+ <label for="minClimbRate">Min Steigen ft/min</label>
1394
+ </div>
1395
+
1396
+ <div class="input-field">
1397
+ <input
1398
+ id="minSinkRate"
1399
+ class="value"
1400
+ type="number"
1401
+ />
1402
+ <label for="minSinkRate">Min Sinken ft/min</label>
1403
+ </div>
1404
+
1405
+ <div class="input-field">
1406
+ <input
1407
+ id="searchPollSeconds"
1408
+ class="value"
1409
+ type="number"
1410
+ />
1411
+ <label for="searchPollSeconds">Suche alle Sekunden</label>
1412
+ </div>
1413
+
1414
+ <div class="input-field">
1415
+ <input
1416
+ id="livePollSeconds"
1417
+ class="value"
1418
+ type="number"
1419
+ />
1420
+ <label for="livePollSeconds">Live alle Sekunden</label>
1421
+ </div>
1422
+
1423
+ <div class="input-field">
1424
+ <input
1425
+ id="liveMaxSeconds"
1426
+ class="value"
1427
+ type="number"
1428
+ />
1429
+ <label for="liveMaxSeconds">Live max Sekunden</label>
1430
+ </div>
1431
+ </div>
1432
+ </div>
1433
+
1434
+ <div class="jetframe-card">
1435
+ <div class="jetframe-title">&#128745;&#65039; &Uuml;berflug</div>
1436
+
1437
+ <div class="switch">
1438
+ <label>
1439
+ Aus
1440
+ <input
1441
+ id="overflightEnabled"
1442
+ class="value"
1443
+ type="checkbox"
1444
+ />
1445
+ <span class="lever"></span>
1446
+ Ein
1447
+ </label>
1448
+ </div>
1449
+
1450
+ <div class="range-field">
1451
+ <label>Max &Uuml;berflug-Distanz: <span id="overflightMaxDistanceNmValue"></span></label>
1452
+ <input
1453
+ id="overflightMaxDistanceNm"
1454
+ class="value"
1455
+ type="range"
1456
+ min="0.2"
1457
+ max="10"
1458
+ step="0.1"
1459
+ />
1460
+ </div>
1461
+
1462
+ <div class="jf-overflight-only">
1463
+ <div class="jf-overflight-only-title">Nur Überflug-Modus</div>
1464
+
1465
+ <div class="switch">
1466
+ <label>
1467
+ Aus
1468
+ <input
1469
+ id="overflightOnly"
1470
+ class="value"
1471
+ type="checkbox"
1472
+ />
1473
+ <span class="lever"></span>
1474
+ Ein
1475
+ </label>
1476
+ </div>
1477
+
1478
+ <div class="jetframe-hint">
1479
+ Keine Start-/Landungs-Erkennung. ADS-B Suche läuft nur rund um Zuhause.
1480
+ </div>
1481
+ </div>
1482
+
1483
+ <div
1484
+ class="switch"
1485
+ style="margin-top: 18px; margin-bottom: 12px"
1486
+ >
1487
+ <label>
1488
+ egal
1489
+ <input
1490
+ id="overflightRequiresWindow"
1491
+ class="value"
1492
+ type="checkbox"
1493
+ />
1494
+ <span class="lever"></span>
1495
+ nur Sichtfenster
1496
+ </label>
1497
+ </div>
1498
+ <div class="jetframe-hint">
1499
+ Wenn aktiv, werden &Uuml;berfl&uuml;ge nur gemeldet, wenn sie auch im konfigurierten
1500
+ Sichtfenster liegen.
1501
+ </div>
1502
+
1503
+ <div class="grid-2">
1504
+ <div class="input-field">
1505
+ <input
1506
+ id="overflightMinAltitudeFt"
1507
+ class="value"
1508
+ type="number"
1509
+ />
1510
+ <label for="overflightMinAltitudeFt">Min H&ouml;he ft</label>
1511
+ </div>
1512
+
1513
+ <div class="input-field">
1514
+ <input
1515
+ id="overflightMaxAltitudeFt"
1516
+ class="value"
1517
+ type="number"
1518
+ />
1519
+ <label for="overflightMaxAltitudeFt">Max H&ouml;he ft</label>
1520
+ </div>
1521
+ </div>
1522
+ </div>
1523
+
1524
+
1525
+ <div class="jetframe-card">
1526
+ <div class="jetframe-title">&#11088; Priorisierung</div>
1527
+
1528
+ <div class="jetframe-hint" style="margin-bottom:14px;">
1529
+ Wenn mehrere passende Flugzeuge gleichzeitig sichtbar sind, kann JetFrame spannendere Flugzeuge bevorzugen.
1530
+ </div>
1531
+
1532
+ <div class="switch" style="margin-bottom:12px;">
1533
+ <label>
1534
+ Aus
1535
+ <input id="priorityEnabled" class="value" type="checkbox" />
1536
+ <span class="lever"></span>
1537
+ Ein
1538
+ </label>
1539
+ </div>
1540
+
1541
+ <div class="grid-2">
1542
+ <div class="switch">
1543
+ <label>
1544
+ Aus
1545
+ <input id="prioritySpecialLivery" class="value" type="checkbox" />
1546
+ <span class="lever"></span>
1547
+ Speziallackierung
1548
+ </label>
1549
+ </div>
1550
+
1551
+ <div class="switch">
1552
+ <label>
1553
+ Aus
1554
+ <input id="priorityAircraftSize" class="value" type="checkbox" />
1555
+ <span class="lever"></span>
1556
+ Große Flugzeuge
1557
+ </label>
1558
+ </div>
1559
+
1560
+ <div class="switch">
1561
+ <label>
1562
+ Aus
1563
+ <input id="priorityMilitaryGov" class="value" type="checkbox" />
1564
+ <span class="lever"></span>
1565
+ Militär / Government
1566
+ </label>
1567
+ </div>
1568
+ <div class="switch">
1569
+ <label>
1570
+ Aus
1571
+ <input id="emergencyPriorityEnabled" class="value" type="checkbox" />
1572
+ <span class="lever"></span>
1573
+ Notfall / Squawk
1574
+ </label>
1575
+ </div>
1576
+ <div class="switch">
1577
+ <label>
1578
+ Aus
1579
+ <input id="emergencySquawk7700" class="value" type="checkbox" />
1580
+ <span class="lever"></span>
1581
+ 7700 Allgemeiner Notfall
1582
+ </label>
1583
+ </div>
1584
+ <div class="switch">
1585
+ <label>
1586
+ Aus
1587
+ <input id="emergencySquawk7600" class="value" type="checkbox" />
1588
+ <span class="lever"></span>
1589
+ 7600 Funkausfall
1590
+ </label>
1591
+ </div>
1592
+ <div class="switch">
1593
+ <label>
1594
+ Aus
1595
+ <input id="emergencySquawk7500" class="value" type="checkbox" />
1596
+ <span class="lever"></span>
1597
+ 7500 besonderer Code
1598
+ </label>
1599
+ </div>
1600
+ </div>
1601
+ </div>
1602
+
1603
+
1604
+
1605
+ <div class="jetframe-card">
1606
+ <div class="jetframe-title">&#128444;&#65039; Logo-Quellen</div>
1607
+
1608
+ <div class="jetframe-hint" style="margin-bottom:14px;">
1609
+ JetFrame liefert keine Airline- oder Herstellerlogos mit. Hier kannst du eigene externe Logo-URLs hinterlegen.
1610
+ Die URLs werden nur in der Visualisierung angezeigt und nicht gecacht.
1611
+ </div>
1612
+
1613
+ <div class="switch" style="margin-bottom:14px;">
1614
+ <label>
1615
+ Aus
1616
+ <input id="externalManufacturerLogos" class="value" type="checkbox" />
1617
+ <span class="lever"></span>
1618
+ Herstellerlogos per URL aktivieren
1619
+ </label>
1620
+ </div>
1621
+
1622
+ <div class="input-field">
1623
+ <textarea
1624
+ id="manufacturerLogoUrls"
1625
+ class="materialize-textarea value"
1626
+ placeholder='AIRBUS=https://example.com/airbus.svg
1627
+ BOEING=https://example.com/boeing.svg
1628
+ EMBRAER=https://example.com/embraer.svg'
1629
+ ></textarea>
1630
+ <label for="manufacturerLogoUrls">Hersteller Logo-URLs</label>
1631
+ </div>
1632
+
1633
+ <div class="jetframe-hint" style="margin-bottom:18px;">
1634
+ Format: <b>HERSTELLER=https://...</b> pro Zeile. Beispiel: AIRBUS, BOEING, EMBRAER, ATR, DASSAULT.
1635
+ </div>
1636
+
1637
+ <div class="switch" style="margin-bottom:14px;">
1638
+ <label>
1639
+ Aus
1640
+ <input id="externalAirlineLogos" class="value" type="checkbox" />
1641
+ <span class="lever"></span>
1642
+ Airline-Logos per Basis-URL aktivieren
1643
+ </label>
1644
+ </div>
1645
+
1646
+ <div class="input-field">
1647
+ <input
1648
+ id="airlineLogoBaseUrl"
1649
+ class="value"
1650
+ type="text"
1651
+ placeholder="https://example.com/airlines/{icao}.png"
1652
+ />
1653
+ <label for="airlineLogoBaseUrl">Airline Logo Basis-URL</label>
1654
+ </div>
1655
+
1656
+ <div class="jetframe-hint" style="margin-bottom:18px;">
1657
+ Platzhalter: <b>{icao}</b>, <b>{iata}</b> oder <b>{code}</b>. Ohne Platzhalter hängt JetFrame automatisch <b>/ICAO.png</b> an.
1658
+ Als eigene Quelle kann z.B. ein selbst gehosteter Ordner oder ein öffentlich erreichbarer GitHub-Raw-Ordner genutzt werden.
1659
+ </div>
1660
+
1661
+ <div class="switch" style="margin-bottom:14px;">
1662
+ <label>
1663
+ Aus
1664
+ <input id="cacheExternalImages" class="value" type="checkbox" />
1665
+ <span class="lever"></span>
1666
+ Externe Bilder/Logos lokal cachen
1667
+ </label>
1668
+ </div>
1669
+
1670
+ <div class="jetframe-hint">
1671
+ Standard: aus. Wenn aktiv, speichert JetFrame Flugzeugbilder und Logos lokal im ioBroker-Dateispeicher.
1672
+ </div>
1673
+
1674
+ <div style="margin-top:14px; padding:14px; border-radius:14px; background:#e3f2fd; border:1px solid #90caf9;">
1675
+ <b>Bild-/Logo-Cache leeren</b><br />
1676
+ <div class="jetframe-hint" style="margin:8px 0 12px 0;">
1677
+ Setzt <b>clearImageCache</b> einmal auf <b>true</b>. JetFrame löscht den Cache und setzt den Datenpunkt danach wieder zurück.
1678
+ </div>
1679
+ <button
1680
+ type="button"
1681
+ class="btn blue"
1682
+ onclick="clearJetFrameCache()"
1683
+ >
1684
+ Cache jetzt leeren
1685
+ </button>
1686
+ </div>
1687
+ </div>
1688
+
1689
+ <div class="jetframe-card">
1690
+ <div class="jetframe-title">&#128250; Visualisierung</div>
1691
+
1692
+ <div class="jetframe-hint" style="margin-bottom:14px;">
1693
+ Einstellungen für die externe JetFrame-Visualisierung.
1694
+
1695
+ </div>
1696
+
1697
+ <div class="grid-2">
1698
+ <div class="input-field">
1699
+ <input
1700
+ id="simpleApiHost"
1701
+ class="value"
1702
+ type="text"
1703
+ placeholder="leer = gleicher Host"
1704
+ />
1705
+ <label for="simpleApiHost">Simple-API Host/IP</label>
1706
+ </div>
1707
+
1708
+ <div class="input-field">
1709
+ <input
1710
+ id="simpleApiPort"
1711
+ class="value"
1712
+ type="number"
1713
+ step="1"
1714
+ />
1715
+ <label for="simpleApiPort">Simple-API Port</label>
1716
+ </div>
1717
+
1718
+ <div class="input-field">
1719
+ <select
1720
+ id="visualSource"
1721
+ class="value"
1722
+ >
1723
+ <option value="current">Aktueller Flug</option>
1724
+ <option value="airport">Start/Landung</option>
1725
+ <option value="overflight">Überflug</option>
1726
+ </select>
1727
+ <label>VIS Anzeigequelle</label>
1728
+ </div>
1729
+ </div>
1730
+ </div>
1731
+ </div>
1732
+
1733
+ <div class="right-column">
1734
+ <div class="jetframe-card">
1735
+ <div class="jetframe-title">&#128506;&#65039; Karte & Sichtfenster</div>
1736
+ <div id="map"></div>
1737
+ <div
1738
+ id="mapInfo"
1739
+ class="map-info"
1740
+ ></div>
1741
+ </div>
1742
+ </div>
1743
+ </div>
1744
+ </div>
1745
+
1746
+ <script></script>
1747
+
1748
+ </body>
1749
+ </html>