iobroker.jetframe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +357 -0
- package/admin/SF-Pro.ttf +0 -0
- package/admin/admin.d.ts +65 -0
- package/admin/frame.html +982 -0
- package/admin/frame.html.bak-aircraft-card-real-row-20260518-1608 +1236 -0
- package/admin/frame.html.bak-aircraft-card-structure-20260518-1517 +1236 -0
- package/admin/frame.html.bak-aircraft-logo-id-fix-20260518-1639 +1239 -0
- package/admin/frame.html.bak-shortcut-test +1236 -0
- package/admin/frame.html.bak-tablet-class-20260518-1729 +1239 -0
- package/admin/heatmap.html +216 -0
- package/admin/index.html +268 -0
- package/admin/index_m.html +1749 -0
- package/admin/jetframe.css +1260 -0
- package/admin/jetframe.css.bak-airbus-landscape-fix +4630 -0
- package/admin/jetframe.css.bak-aircraft-card-clean-equal-20260518-1438 +4899 -0
- package/admin/jetframe.css.bak-aircraft-card-real-row-20260518-1608 +4814 -0
- package/admin/jetframe.css.bak-aircraft-card-row-left-20260518-1525 +4604 -0
- package/admin/jetframe.css.bak-aircraft-card-slim-equal-20260518-1446 +4647 -0
- package/admin/jetframe.css.bak-aircraft-card-structure-20260518-1517 +4646 -0
- package/admin/jetframe.css.bak-aircraft-inline-final-20260518-1527 +4654 -0
- package/admin/jetframe.css.bak-aircraft-row-compact-fix-20260518-1639 +4763 -0
- package/admin/jetframe.css.bak-before-aircrafttype-purge +4818 -0
- package/admin/jetframe.css.bak-before-cleanup +4670 -0
- package/admin/jetframe.css.bak-before-remove-tablet-only-20260518-1711 +4896 -0
- package/admin/jetframe.css.bak-before-tablet-layout-rework-20260518-1650 +4914 -0
- package/admin/jetframe.css.bak-clean-duplicate-fonts-20260518-1340 +4975 -0
- package/admin/jetframe.css.bak-clean-old-index-fix-20260518-1937 +5167 -0
- package/admin/jetframe.css.bak-hardleft-airbus +4751 -0
- package/admin/jetframe.css.bak-index-iphone-landscape-20260518-1931 +5030 -0
- package/admin/jetframe.css.bak-index-landscape-final-20260518-1941 +5167 -0
- package/admin/jetframe.css.bak-index-landscape-real-20260518-1936 +5186 -0
- package/admin/jetframe.css.bak-landscape-compact-jumbo-bold-20260518-1343 +4802 -0
- package/admin/jetframe.css.bak-logo-align-final +4551 -0
- package/admin/jetframe.css.bak-logo-final2 +4551 -0
- package/admin/jetframe.css.bak-narrowbody-font-fix +4992 -0
- package/admin/jetframe.css.bak-nuke-airbus-align +4790 -0
- package/admin/jetframe.css.bak-pill-balance-20260518-1603 +4773 -0
- package/admin/jetframe.css.bak-pill-balance-fix +4910 -0
- package/admin/jetframe.css.bak-radar-fix-fonts +4710 -0
- package/admin/jetframe.css.bak-shortcut-test +4899 -0
- package/admin/jetframe.css.bak-smaller-aircraft-card-fonts-20260518-1345 +4897 -0
- package/admin/jetframe.css.bak-tablet-fix-real-20260518-1748 +4945 -0
- package/admin/jetframe.css.bak-tablet-fullscreen-fix-20260518-1804 +4972 -0
- package/admin/jetframe.css.bak-tablet-landscape-layout-20260518-1645 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-final-20260518-1839 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-v3-20260518-1729 +4802 -0
- package/admin/jetframe.css.bak-tablet-layout-v4-20260518-1801 +4957 -0
- package/admin/jetframe.css.bak-tablet-layout-v5-20260518-1843 +4970 -0
- package/admin/jetframe.css.bak-tablet-layout-v6-20260518-1848 +4958 -0
- package/admin/jetframe.css.bak-tablet-layout-v7-20260518-1909 +4985 -0
- package/admin/jetframe.css.bak-tablet-only-landscape-v2-20260518-1707 +4802 -0
- package/admin/jetframe.css.bak-tablet-pages-final-20260519-1857 +5188 -0
- package/admin/jetframe.css.bak-tablet-pages-final-20260519-1859 +5347 -0
- package/admin/jetframe.css.bak-tablet-pages-v2-20260519-190807 +5349 -0
- package/admin/jetframe.css.bak-typography-align-final +4818 -0
- package/admin/jetframe.png +0 -0
- package/admin/manifest.webmanifest +15 -0
- package/admin/src/app.tsx +58 -0
- package/admin/src/components/settings.tsx +97 -0
- package/admin/src/i18n/de.json +11 -0
- package/admin/src/i18n/en.json +11 -0
- package/admin/src/i18n/es.json +11 -0
- package/admin/src/i18n/fr.json +11 -0
- package/admin/src/i18n/i18n.d.ts +28 -0
- package/admin/src/i18n/it.json +11 -0
- package/admin/src/i18n/nl.json +11 -0
- package/admin/src/i18n/pl.json +11 -0
- package/admin/src/i18n/pt.json +11 -0
- package/admin/src/i18n/ru.json +11 -0
- package/admin/src/i18n/uk.json +11 -0
- package/admin/src/i18n/zh-cn.json +11 -0
- package/admin/src/index.tsx +25 -0
- package/admin/stats.html +228 -0
- package/admin/style.css +32 -0
- package/admin/tsconfig.json +11 -0
- package/admin/words.js +46 -0
- package/build/lib/adsb.js +218 -0
- package/build/lib/adsb.js.map +7 -0
- package/build/lib/airportNamesDe.js +131 -0
- package/build/lib/airportNamesDe.js.map +7 -0
- package/build/lib/airports.js +281 -0
- package/build/lib/airports.js.map +7 -0
- package/build/lib/classify.js +339 -0
- package/build/lib/classify.js.map +7 -0
- package/build/lib/config.js +103 -0
- package/build/lib/config.js.map +7 -0
- package/build/lib/flightInfo.js +1409 -0
- package/build/lib/flightInfo.js.map +7 -0
- package/build/lib/geo.js +84 -0
- package/build/lib/geo.js.map +7 -0
- package/build/lib/images.js +422 -0
- package/build/lib/images.js.map +7 -0
- package/build/lib/specialLiveries.js +342 -0
- package/build/lib/specialLiveries.js.map +7 -0
- package/build/lib/states.js +971 -0
- package/build/lib/states.js.map +7 -0
- package/build/lib/staticFiles.js +73 -0
- package/build/lib/staticFiles.js.map +7 -0
- package/build/lib/types.js +17 -0
- package/build/lib/types.js.map +7 -0
- package/build/lib/visConfig.js +52 -0
- package/build/lib/visConfig.js.map +7 -0
- package/build/main.js +1454 -0
- package/build/main.js.map +7 -0
- package/io-package.json +169 -0
- package/package.json +82 -0
|
@@ -0,0 +1,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: '© 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) + '°');
|
|
467
|
+
$('#windowFovDegValue').html(n('windowFovDeg', 120) + '°');
|
|
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
|
+
? '🟢 Ü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
|
+
'🪟 Fenster: <b>' +
|
|
618
|
+
norm(bearing).toFixed(0) +
|
|
619
|
+
'°</b><br>' +
|
|
620
|
+
'👁️ Sichtbereich: <b>' +
|
|
621
|
+
norm(bearing - fov / 2).toFixed(0) +
|
|
622
|
+
'° bis ' +
|
|
623
|
+
norm(bearing + fov / 2).toFixed(0) +
|
|
624
|
+
'°</b><br>' +
|
|
625
|
+
'🛫 Flughafen von Zuhause: <b>' +
|
|
626
|
+
airportBearing.toFixed(0) +
|
|
627
|
+
'°</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
|
+
? '✅ Flughafen liegt im Sichtfenster.'
|
|
635
|
+
: '⚠️ Flughafen liegt auß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, '"') + '"></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, '"') + '"></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">✈️ 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">⚙️ 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">🔊 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">🛫 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ü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">🏠 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">🪟 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">📡 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">🛩️ Ü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 Ü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 Überflü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ö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öhe ft</label>
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
</div>
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
<div class="jetframe-card">
|
|
1526
|
+
<div class="jetframe-title">⭐ 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">🖼️ 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">📺 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">🗺️ 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>
|