-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscripts.js
590 lines (527 loc) · 26 KB
/
scripts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File: scripts.js
sessionStorage.setItem("activeUI", "2021-a");
/*
* adds look and functionality updates for the original HTML to the document loaded in the iframe origin
*/
function injectUpdates(origin) {
var ocw = origin.contentWindow;
var ocd = origin.contentDocument;
if(!ocd) { // likely no CORS
return;
}
function updateAirportPage(page) {
page.querySelector("#airportform").style.fontSize = "0";
page.querySelector("#icaoSelector").style.fontSize = "14px";
}
function updateProgressPage(page) {
page.querySelector("#defaultCommands").style.display = "none";
var refresher = ocd.querySelector("#refreshselect");
refresher.addEventListener("change", function() {
storeState("refreshprogressinterval", this.selectedIndex);
});
var refreshInterval = retrieveState("refreshprogressinterval", null);
if(refreshInterval !== null) {
refresher.selectedIndex = refreshInterval;
refresher.dispatchEvent(new Event("change"));
}
}
function updateGenericPage(page) {
page.querySelector("#refreshform").style.display = "none";
}
function updateMapPage(page) {
/*
* settings (developer modifiable)
*/
var defaultMapQuality = 70; // no observed difference across 30 to 100
var fastRefreshMapQuality = 50; // below 17 JPEG artifacts on vertical lines are significantly visible: doubled lines. 18 to 21 (something around these) are significantly more artifacted too including an off blue ocean color. space saved compared to default: around 150 KB or 40 % at 1080p; works in conjunction with fastRefreshMapThreshold
var zoomingMapQuality = 3;
var resizingMapQuality = 3;
var fastRefreshMapThreshold = 12; // time in seconds below which an auto-refreshing map's refresh interval is considered "fast"; works in conjunction with fastRefreshMapQuality
var mapUpdateTimeoutWaitDuration = 10000; // time in milliseconds to wait for the updated map image to have arrived, after this time a "map image updated" (= notifications are run and image update locks are released) is forced, this is server response time + download time! The main purpose is unlocking the locks on an image update when no image received for a reason like a server outage which if not handled would prevent new images to get requested which the server could handle again if the outage was only temporary.
/*
* settings (code modifiable)
*/
var realPixelsPerCSSPixel = /*devicePixelRatio ||*/ 1;
/*
* elements
*/
var refresher = ocd.querySelector("#refreshDelay");
var refreshToggle = ocd.querySelector("#refreshMapToggle");
var refreshTypeWAC = ocd.querySelector("#refreshWithAircraft");
var centerDistance = ocd.querySelector("#centerDistance");
var airportText = ocd.querySelector("#airporttext");
var iAParent = ocd.querySelector("#interactionParent");
var mapElement = ocd.querySelector("#map");
/*
* map image handling
*/
var mapUpdateNotifiables = [];
var mapUpdateCounter = 0;
var mapImageLoaded = true;
var forceLock = false;
var mapUpdateTimeout = null;
var valueMapUpdateCounterMustBeAbove = mapUpdateCounter; // be able to not run code which should only run once per returned image and which was run for the "forced" update on timeout wait duration end if the image does still return after the timeout wait duration, see mapUpdateTimeoutWaitDuration
mapElement.onload = function() {
clearTimeout(mapUpdateTimeout);
mapImageLoaded = true;
forceLock = false;
while(mapUpdateNotifiables.length) { // this loop empties the notifiables thus they cannot run a second time and thus do not need to be inside below "if(mapUpdateCounter > valueMapUpdateCounterMustBeAbove)"
var notifiable = mapUpdateNotifiables.shift();
if(typeof notifiable === "function") {
notifiable(mapUpdateCounter);
}
}
if(mapUpdateCounter > valueMapUpdateCounterMustBeAbove) {}
};
/**
* API: map source update
* locks and does not transfer new commands while waiting for map image to have been received.
* lock can be overridden by force = true which locks again. That lock cannot be overridden unless nolock = true.
* Note: upon assigning a new image source, Chrome based browsers "cancel" the old image request. As http (1.x) apparently has no means of doing so, this is likely done on a lower network connection level. The LNM web server, by Chrome developer tools, does not send an image to cancelled requests anymore. The logic here has been built around minimising cancellation of requests and thus minimising time in which no updated image would be shown.
* notifiable = function to take notification of this function call returned a new image, will be passed id of current update "cycle"
* returns integer id of current update "cycle", will have the negative value of the current update "cycle" if this function call did not request a new image due to locking
*/
// has a copy updateMapImage_cssPixels (see there) which needs parallel treatment
var updateMapImage_realPixels = function(command, quality, force, notifiable, nolock, nearestAirport) {
mapUpdateNotifiables.push(notifiable);
if(mapImageLoaded || force && !forceLock) {
mapImageLoaded = false;
forceLock = force && !nolock;
let url = `/mapimage?format=jpg&quality=${quality}&width=${~~(mapElement.parentElement.clientWidth * realPixelsPerCSSPixel)}&height=${~~(mapElement.parentElement.clientHeight * realPixelsPerCSSPixel)}&session&${command}=${Math.random()}`
if (nearestAirport) url += `&nearest_icao=${nearestAirport}`
mapElement.src = url;
clearTimeout(mapUpdateTimeout);
mapUpdateTimeout = setTimeout((function(mapUpdateCounter) {
return function() {
mapElement.onload();
valueMapUpdateCounterMustBeAbove = mapUpdateCounter + 1;
};
})(mapUpdateCounter), mapUpdateTimeoutWaitDuration);
return ++mapUpdateCounter;
}
return -mapUpdateCounter;
};
// same as updateMapImage_realPixels except width and height are as delivered by JS (= in CSS pixels (which are real when devicePixelRatio == 1))
var updateMapImage_cssPixels = function(command, quality, force, notifiable, nolock, nearestAirport) {
mapUpdateNotifiables.push(notifiable);
if(mapImageLoaded || force && !forceLock) {
mapImageLoaded = False;
forceLock = force && !nolock;
let url = `/mapimage?format=jpg&quality=${quality}&width=${~~mapElement.parentElement.clientWidth}&height=${~~mapElement.parentElement.clientHeight}&session&${command}=${Math.random()}`
if (nearestAirport) url += `&nearest_icao=${nearestAirport}`
mapElement.src = url;
clearTimeout(mapUpdateTimeout);
mapUpdateTimeout = setTimeout((function(mapUpdateCounter) {
return function() {
mapElement.onload();
valueMapUpdateCounterMustBeAbove = mapUpdateCounter + 1;
};
})(mapUpdateCounter), mapUpdateTimeoutWaitDuration);
return ++mapUpdateCounter;
}
return -mapUpdateCounter;
};
async function _fetchNearestAirportData(latitude, longitude) {
try {
const response = await fetch(`/api/nearest_airport?lat=${latitude}&lon=${longitude}`, {
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
return data.icao;
} else {
console.error("Failed to fetch nearest airport data:", response.status);
return null;
}
} catch (error) {
console.error("Error fetching nearest airport data:", error);
return null;
}
}
function setMapImageUpdateFunction() {
window.updateMapImage = realPixelsPerCSSPixel === 1 ? updateMapImage_cssPixels : updateMapImage_realPixels;
}
setMapImageUpdateFunction();
/*
* API: returns user setting for map zoom transformed in map command format, used to be used for explicitly setting zoom level
*/
function getZoomDistance() {
return ~~Math.pow(2, centerDistance.value);
}
/*
* API: returns the map command equalling a reload taking into account relevant settings
*/
function mapCommand() {
return refreshTypeWAC.checked ? "mapcmd=user&cmd" : "reload";
}
/*
* Event handling: resize window
*/
var imageRequestTimeout = null;
ocw.sizeMapToContainer = function() {
ocw.clearTimeout(imageRequestTimeout);
updateMapImage(mapCommand(), resizingMapQuality);
imageRequestTimeout = ocw.setTimeout(function() { // update after the resizing stopped to have an image for the final quality "for certain"
updateMapImage(mapCommand(), defaultMapQuality, true, 0, true); // do not lock for event handling like a new zoom request to be able to take priority over waiting for the potentially longer loading higher-quality final quality
}, 200);
};
/*
* Event handling: mousemove over map (parent)
*/
mapElement.parentElement.onmousemove = function(e) {
var s = e.currentTarget.clientHeight / e.currentTarget.clientWidth;
var x = e.offsetX - e.currentTarget.clientWidth / 2;
var y = -(e.offsetY - e.currentTarget.clientHeight / 2);
if(y > s * x) {
if(y > -s * x) {
e.currentTarget.setAttribute("data-shift", "up"); // north
} else {
e.currentTarget.setAttribute("data-shift", "left"); // west
}
} else {
if(y >-s * x) {
e.currentTarget.setAttribute("data-shift", "right"); # east
} else {
e.currentTarget.setAttribute("data-shift", "down"); # south
}
}
};
/*
* Event handling: click map
*/
ocw.handleInteraction = async function(e) {
var shift = e.currentTarget.getAttribute("data-shift");
let nearestAirport = null;
if(refreshTypeWAC.checked) {
let simInfo = await fetch("/api/sim/info").then(response => response.json())
if (simInfo.active && simInfo.position && simInfo.position.lat !== undefined && simInfo.position.lon !== undefined)
{
nearestAirport = await _fetchNearestAirportData(simInfo.position.lat,simInfo.position.lon)
} else {
nearestAirport = null
}
}
shift !== null ? updateMapImage(mapCommand(), defaultMapQuality, true, null, false, nearestAirport) : 0; // on touch devices, without initial HTML attribute, shift === null when pinching for zoom in
};
/*
* Event handling: mousewheel / finger pinch map
*/
var mapWheelZoomTimeout = null;
var mapZoomCore = async function(condition) {
let nearestAirport = null;
if(refreshTypeWAC.checked) {
let simInfo = await fetch("/api/sim/info").then(response => response.json())
if (simInfo.active && simInfo.position && simInfo.position.lat !== undefined && simInfo.position.lon !== undefined) {
nearestAirport = await _fetchNearestAirportData(simInfo.position.lat,simInfo.position.lon)
} else {
nearestAirport = null;
}
}
ocw.clearTimeout(mapWheelZoomTimeout);
updateMapImage("mapcmd=" + (condition ? "in" : "out") + "&cmd", zoomingMapQuality, true, null, false, nearestAirport);
mapWheelZoomTimeout = ocw.setTimeout(function() {
updateMapImage("reload", defaultMapQuality, true, 0, true, nearestAirport); // do not lock for event handling like a new zoom request to be able to take priority over waiting for the potentially longer loading higher-quality final quality
}, 750);
};
mapElement.onwheel = function(e) {
mapZoomCore(e.deltaY < 0);
};
var pointers = {};
mapElement.onpointerdown = function(e) {
pointers[e.pointerId] = [e, e];
};
mapElement.onpointermove = function(e) {
if(pointers.hasOwnProperty(e.pointerId)) {
pointers[e.pointerId][1] = e;
var keys = Object.keys(pointers);
if(keys.length > 1) {
var key1 = keys[0];
var key2 = keys[1];
var distStart = Math.hypot(pointers[key1][0].clientX - pointers[key2][0].clientX, pointers[key1][0].clientY - pointers[key2][0].clientY);
var distNow = Math.hypot(pointers[key1][1].clientX - pointers[key2][1].clientX, pointers[key1][1].clientY - pointers[key2][1].clientY);
if(distNow < .85 * distStart) {
mapZoomCore(false);
pointers[key1][0] = pointers[key1][1];
pointers[key2][0] = pointers[key2][1];
} else if(distNow > 1.15 * distStart) {
mapZoomCore(true);
pointers[key1][0] = pointers[key1][1];
pointers[key2][0] = pointers[key2][1];
}
}
}
};
function pointerup(e) {
delete pointers[e.pointerId];
}
mapElement.onpointerup = pointerup;
mapElement.onpointercancel = pointerup;
mapElement.onpointerout = pointerup;
mapElement.onpointerleave = pointerup;
/*
* scrollable indicators of header options bar which is a toolbar
*/
enableToolbarIndicators(ocd.querySelector("#header"), ocw);
/*
* Option Commands Handling: refresh
*/
// prevent users from "mapipulation" by adjusting for all features whose setting's values create an "auto" map
function adjustForAutoMapSettings() {
if(refreshTypeWAC.checked && refreshToggle.checked) {
iAParent.setAttribute("disabled", "");
} else {
iAParent.removeAttribute("disabled");
}
}
var mapRefresher = new (function() {
var refreshMapTimeout = null;
var looping = false;
var timeStartLastRequest = 0;
var refreshIn = 0;
function notifiable(id) { // ignore id thus don't determine if some(one/thing) else interrupted own request and thus using time data does not produce expected result (resulting inprecision is not sufficiently detrimental to warrant effort)
var durationTilImageHere = performance.now() - timeStartLastRequest;
refreshIn = ~~(refresher.value * 1000 - durationTilImageHere);
if(looping) {
looper();
}
}
function requester() {
timeStartLastRequest = performance.now();
let nearestAirport = null;
if(refreshTypeWAC.checked) {
let simInfo = ocw.fetch("/api/sim/info").then(response => response.json())
if (simInfo.active && simInfo.position && simInfo.position.lat !== undefined && simInfo.position.lon !== undefined) {
nearestAirport = ocw._fetchNearestAirportData(simInfo.position.lat,simInfo.position.lon)
}
}
updateMapImage(mapCommand(), refresher.value < fastRefreshMapThreshold ? fastRefreshMapQuality : defaultMapQuality, false, notifiable, false, nearestAirport); // not storing return value because using it to determine if some(one/thing) else interrupted own request is not done
}
function looper() {
refreshMapTimeout = ocw.setTimeout(requester, Math.max(0, refreshIn)); // be >= 0
}
// does stop before
this.start = function() {
this.stop();
looping = true;
refreshIn = 0;
looper();
};
this.stop = function() {
ocw.clearTimeout(refreshMapTimeout);
looping = false;
};
})();
function adjustDelay() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "/refresh?session&maprefresh=" + (refreshToggle.checked ? refresher.value : 0), true);
xhttp.send();
};
ocw.checkRefresh = function() {
adjustDelay();
refreshToggle.checked ? mapRefresher.start() : mapRefresher.stop();
refresher.disabled = !refreshToggle.checked;
adjustForAutoMapSettings();
return refreshToggle.checked;
};
ocw.delayRefresh = function() {
ocw.checkRefresh();
};
ocw.refreshMap = function() {
let nearestAirport = null;
if(refreshTypeWAC.checked) {
let simInfo = await fetch("/api/sim/info").then(response => response.json())
if (simInfo.active && simInfo.position && simInfo.position.lat !== undefined && sim_info.position.lon !== undefined)
nearestAirport = await _fetchNearestAirportData(simInfo.position.lat, sim_info.position.lon)
}
updateMapImage(mapCommand(), defaultMapQuality, true, null, true, nearestAirport);
};
// caring and handling state changes and restoration after content switch (after outer ui other button presses)
refreshToggle.addEventListener("click", function() {
storeState("autorefresh", this.checked);
});
refreshToggle.checked = retrieveState("autorefresh", false);
refresher.addEventListener("input", function() {
storeState("refreshdelay", this.value);
});
var retrievedState = retrieveState("refreshdelay", null);
if(retrievedState !== null) {
refresher.value = retrievedState;
}
/*
* Option Commands Handling: aircraft
*/
ocw.toggleCenterAircraft = function () {
ocw.checkRefresh();
};
ocw.centerMapOnAircraft = async function() {
let nearestAirport = null;
if(refreshTypeWAC.checked) {
let simInfo = await fetch("/api/sim/info").then(response => response.json())
if (simInfo.active && simInfo.position && simInfo.position.lat !== undefined && sim_info.position.lon !== undefined)
nearestAirport = await _fetchNearestAirportData(simInfo.position.lat, simInfo.position.lon)
}
updateMapImage("mapcmd=user&cmd", defaultMapQuality, true, null, true, nearestAirport);
};
// caring and handling state changes and restoration after content switch
refreshTypeWAC.addEventListener("click", function() {
storeState("refreshwithaircraft", this.checked);
});
refreshTypeWAC.checked = retrieveState("refreshwithaircraft", false);
/*
* Option Commands Handling: waypoints and airports
*/
function handleAutomap(withFunction) {
if(refreshTypeWAC.checked && refreshToggle.checked) {
if(!refreshTypeWAC.classList.contains("enlarge")) {
ocw.setTimeout(function() {
ocw.setTimeout(function() {
refreshTypeWAC.checked = false;
withFunction();
ocw.setTimeout(function() {
refreshTypeWAC.classList.remove("enlarge");
}, 250);
}, 250);
}, 1500);
refreshTypeWAC.classList.add("enlarge");
}
return;
}
withFunction();
}
// override default function to stay within our new ui look
ocw.submitMapRouteCmd = function() {
handleAutomap(function(){updateMapImage("mapcmd=route&cmd", defaultMapQuality, true)});
};
// override default function to stay within our new ui look
ocw.submitMapAirportCmd = function() {
handleAutomap(function(){updateMapImage("mapcmd=airport&airport=" + airportText.value + "&cmd", defaultMapQuality, true)});
};
/*
* Option Commands Handling: prevent standby
*/
var standbyPreventionVideoContainer = ocd.querySelector("#preventstandbyVideoContainer");
var standbyPreventionVideo = null;
function handleVideo() {
standbyPreventionVideo = standbyPreventionVideoContainer.contentDocument.querySelector("video"); // iOS needs video with audio track to have it work as standby preventer
standbyPreventionVideo.addEventListener("play", function() {
standbyPreventionVideo.classList.add("running");
});
standbyPreventionVideo.addEventListener("timeupdate", function() { // iOS iPad showed a "unprecision" of 2s, thus rewinding after 2s before the end lead to the video ending and rewinding not applied anymore; loop HTML attribute appeared to get ignored
standbyPreventionVideo.currentTime > 6 ? standbyPreventionVideo.currentTime = 4 : 0;
});
standbyPreventionVideo.addEventListener("pause", function() {
standbyPreventionVideo.classList.remove("running");
});
}
var preventStandbyToggle = ocd.querySelector("#preventstandby");
var testTimeout;
function enableStandbyPrevention() {
standbyPreventionVideo.play();
storeState("preventingstandby", true);
testTimeout = ocw.setTimeout(function() {
if(standbyPreventionVideo.paused) { // being paused despite instructing play can occur on "restoring state" after content switch because playing is denied by iOS on page load, content switch is a page load and playing would be autoplay
preventStandbyToggle.click();
}
}, 1000);
}
function disableStandbyPrevention() {
ocw.clearTimeout(testTimeout);
standbyPreventionVideo.pause();
storeState("preventingstandby", false);
}
ocw.preventStandby = function(innerorigin) {
if(innerorigin.checked) {
if(standbyPreventionVideo !== null) {
enableStandbyPrevention();
} else {
standbyPreventionVideoContainer.onload = function() {
handleVideo();
enableStandbyPrevention();
};
standbyPreventionVideoContainer.src = "preventstandbyvideo.html";
}
} else {
if(standbyPreventionVideo !== null) {
disableStandbyPrevention();
} else {
standbyPreventionVideoContainer.onload = function() {
handleVideo();
disableStandbyPrevention();
};
standbyPreventionVideoContainer.src = "preventstandbyvideo.html";
}
}
};
if(retrieveState("preventingstandby", preventStandbyToggle.checked) !== preventStandbyToggle.checked) {
preventStandbyToggle.click();
}
/*ocw.toggleRetinaMap = function(innerorigin) {
if(innerorigin.checked) {
realPixelsPerCSSPixel = devicePixelRatio || 1;
storeState("retinaon", true);
} else {
realPixelsPerCSSPixel = 1;
storeState("retinaon", false);
}
setMapImageUpdateFunction();
ocw.refreshMap();
}
var retinaToggle = ocd.querySelector("#retinaToggle");
if(retrieveState("retinaon", retinaToggle.checked) !== retinaToggle.checked) {
retinaToggle.click();
}*/
/*
* Initial Page initialisations: map
*/
var transitionElement = mapElement.parentElement;
function initiallyShowMap() {
function show() {
mapElement.removeEventListener("load", show);
transitionElement.classList.remove("initially");
transitionElement.classList.remove("transition");
transitionElement.classList.remove("toshow");
}
mapElement.addEventListener("load", show);
ocw.checkRefresh() || ocw.refreshMap(); // takes over for former body[onload]
storeState("mapshown", true);
}
if(!retrieveState("mapshown", false)) {
transitionElement.classList.add("toshow");
transitionElement.classList.remove("initially");
setTimeout(function() {
transitionElement.classList.add("transition");
setTimeout(function() {
transitionElement.classList.remove("toshow");
setTimeout(initiallyShowMap, 3000);
}, 0);
}, 0);
} else {
initiallyShowMap();
}
}
var toDo = { // ordered according to assumed likelihood of access: first assumedly accessed page is checked first: code is done. currently equals the outer ui menu button order.
"#mapPage": updateMapPage,
"#flightplanPage": updateGenericPage,
"#progressPage": updateProgressPage,
"#aircraftPage": updateGenericPage,
"#airportPage": updateAirportPage
};
for(var i in toDo) {
var find = ocd.querySelector(i);
if(find) {
toDo[i](find);
break;
}
}
function storeState(key, state) {
sessionStorage.setItem(key, typeof state === "boolean" ? state ? "1" : "" : "" + state);
sessionStorage.setItem(key + "__type", typeof state === "boolean" ? "1" : typeof state === "number" ? "2" : "");
}
function retrieveState(key, defaultValue) {
var type = sessionStorage.getItem(key + "__type");
var value = sessionStorage.getItem(key);
return value === null ? defaultValue : type === "1" ? value === "1" : type === "2" ? parseFloat(value) : value;
}
}