2227 lines
88 KiB
JavaScript
2227 lines
88 KiB
JavaScript
/********************************************************************
|
|
* 2D Map for Radarcape
|
|
********************************************************************
|
|
* Guenter Koellner Embedded Development GmbH
|
|
*
|
|
* Am Rain 24
|
|
* 85256 Vierkirchen
|
|
* Geschaeftsfuehrer Guenter Koellner
|
|
*
|
|
* http://www.modesbeast.com
|
|
********************************************************************/
|
|
|
|
var map, acSource, trackSource, ringsSource, ringsLayer, mapInfo, highlightedPlane, highlightedPlaneSrc, lockOnPlanePosition, lockControlElement;
|
|
var keepPathsFromEviction = new Map(); // array of icao ids of which updateTrack should fetch the paths although their aircraft's live position has gone out of view (but the track should be still rendered)
|
|
var constrainingFiltersEnabled = false; // true if there is at least one filter enabled that constrains the displayed aircrafts (e.g. source, altitude)
|
|
var refreshInterval=2000;
|
|
var timers = {
|
|
refreshData: null
|
|
}
|
|
var AircraftsVisible = {
|
|
inView:0,
|
|
receivable:0,
|
|
valid:0,
|
|
located:0,
|
|
countBySource:{}
|
|
};
|
|
var planes = new Map(); // planes displayed on map
|
|
var planesAvailable = new Map(); // planes available globally to radarcape
|
|
var inLastUpdate = new Map(); // aircrafts that were updated in last aircraftlist run
|
|
|
|
var sourceIds = ["M", "A", "F", "O", "L"].sort(); // Mlat, ADSB, Flightaware, OGN/Flarm, Jetvision/Flarm Local receiver
|
|
var defaultActiveSourceIds = ["M", "A", "F", "L"].sort();
|
|
|
|
var Filters={
|
|
prio: {
|
|
value: null,
|
|
enabled: false,
|
|
domnodes: ["filter-prio, #filter-prio-enabled"],
|
|
indicateChange: false // don't change color of filter button if this property changed
|
|
},
|
|
tracklength: {
|
|
value: null,
|
|
enabled: true,
|
|
domnodes: ["filter-tracklength"],
|
|
indicateChange: false // don't change color of filter button if this property changed
|
|
},
|
|
altitude: {
|
|
from: null,
|
|
to: null,
|
|
enabled: false,
|
|
domnodes: ["filter-alt-min", "filter-alt-max", "filter-alt-enabled"]
|
|
},
|
|
speed: {
|
|
from: null,
|
|
to: null,
|
|
enabled: false,
|
|
domnodes: ["filter-spd-min", "filter-spd-max", "filter-spd-enabled"]
|
|
},
|
|
distance: {
|
|
from: null,
|
|
to: null,
|
|
enabled: false,
|
|
domnodes: ["filter-dis-min", "filter-dis-max", "filter-dis-enabled"]
|
|
},
|
|
gndexcl: {
|
|
value: 1,
|
|
enabled: true,
|
|
invert: true, // enabled means value = 0 and vice versa
|
|
domnodes: ["filter-gnd-enabled"]
|
|
},
|
|
flight: { /* array type */
|
|
value: [],
|
|
enabled: false,
|
|
domnodes: ["filter-fli", "filter-fli-enabled"]
|
|
},
|
|
squawk: {
|
|
value: null,
|
|
enabled: false,
|
|
domnodes: ["filter-squ", "filter-squ-enabled"]
|
|
},
|
|
orig: {
|
|
value: null,
|
|
enabled: false,
|
|
domnodes: ["filter-org", "filter-org-enabled"]
|
|
},
|
|
dest: {
|
|
value: null,
|
|
enabled: false,
|
|
domnodes: ["filter-dst", "filter-dst-enabled"]
|
|
},
|
|
type: { /* array type */
|
|
value: [],
|
|
enabled: false,
|
|
domnodes: ["filter-typ", "filter-typ-enabled"]
|
|
},
|
|
fleetwatch: { /* array type */
|
|
value: [],
|
|
enabled: false,
|
|
domnodes: ["filter-fleet", "filter-fleet-enabled"]
|
|
},
|
|
srcpref: {
|
|
value: null,
|
|
enabled: false,
|
|
indicateChange: false,
|
|
domnodes: ["filter-sources"]
|
|
},
|
|
activeSources: { /* array type */
|
|
value: defaultActiveSourceIds,
|
|
enabled: false,
|
|
domnodes: ["preferred-source"]
|
|
}
|
|
};
|
|
var domIdToFilterKey = new Map();
|
|
{
|
|
var assignkey = function(key, parameterValue){
|
|
parameterValue["domnodes"].forEach(function(val){
|
|
key = key.replace("-enabled", ""); // include the enabling checkboxes in the map by
|
|
domIdToFilterKey.set(val, key);
|
|
})
|
|
}
|
|
|
|
for (var key in Filters) {
|
|
assignkey(key, Filters[key]);
|
|
}
|
|
|
|
}
|
|
|
|
var ActiveDisplayStyle;
|
|
var rotateLockEnabled = true;
|
|
|
|
|
|
/*
|
|
* cleanTracksFromOtherSrc call wenn plane out of sight (hightlightedplane auf null setzen und updatetrack callen?)
|
|
*
|
|
* track wird nicht removed wenn
|
|
*
|
|
* */
|
|
|
|
// flags whether updateTrack or updateAircraftFeatures is running, since js has no mutex/semaphors;
|
|
// prevents collision between event and scheduled calls
|
|
var trackUpdRunning = false;
|
|
var updateAcFeaturesRunning = false;
|
|
|
|
// flags whether the history and filter lock "static download" mode are enabled (latter is a history feature)
|
|
var historyModeEnabled = false;
|
|
var staticModeEnabled = false;
|
|
|
|
// the config object that's used for fetch() calls
|
|
var customRequestOptions = {credentials: 'include'};
|
|
|
|
function getActiveDisplaystyle(){
|
|
return ActiveDisplayStyle;
|
|
}
|
|
function setLayerVisible(lyr, visible){
|
|
lyr.setVisible(visible);
|
|
// ensure that base layers are only exclusively active!
|
|
if (visible && lyr.get('type') === 'base') {
|
|
// Hide all other base layers regardless of grouping
|
|
ol.control.LayerSwitcher.forEachRecursive(map, function(l, idx, a) {
|
|
if (l != lyr && l.get('type') === 'base') {
|
|
l.setVisible(false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function updatePlaneFeature(ac){
|
|
var src = acSource;
|
|
|
|
var vRateIndicator = "";
|
|
if(ac.vrt == null){
|
|
vRateIndicator = '<i class="icon ion-help"></i>';
|
|
} else if(ac.vrt > 10){
|
|
vRateIndicator = '<i class="icon ion-arrow-up-b"></i>';
|
|
} else if(ac.vrt < -10){
|
|
vRateIndicator = '<i class="icon ion-arrow-down-b"></i>';
|
|
} else {
|
|
vRateIndicator = 'FL'; //'■';
|
|
}
|
|
var infoText = getActiveDisplaystyle().getInfoText(ac, vRateIndicator)
|
|
|
|
var newPos = ol.proj.fromLonLat([parseFloat(ac.lon), parseFloat(ac.lat)]);
|
|
|
|
if(!ac.lat || !ac.lon){
|
|
console.log("AC with hex "+ac.hex+" has incomplete location data!");
|
|
return;
|
|
}
|
|
// plane is already on map, update
|
|
if (planes.get(ac.hex) && planes.get(ac.hex).features != undefined && planes.get(ac.hex).features.get("acIcon")) {
|
|
|
|
//updating features
|
|
var features = planes.get(ac.hex).features;
|
|
|
|
// update positions
|
|
// tracks/flightpaths are handled via the style function
|
|
if(getActiveDisplaystyle().drawAllTracks === true || highlightedPlane == ac.hex){
|
|
// set ac icon position as the newest point of the ac's track, ignoring what's coming from aircraftlist since they are often out of sync
|
|
}
|
|
features.get("acIcon").getGeometry().setCoordinates(newPos);
|
|
|
|
if(persistPaths === true && (highlightedPlane==ac.hex || getActiveDisplaystyle().drawAllTracks === true)){
|
|
|
|
if(!features.get("fpaths") || features.get("fpaths").size <= 0 ||
|
|
(ac.src !== features.get("fpaths").values().next().value.src) || // clear flightpath if source changed
|
|
(ac.uti < features.get("fpaths").values().next().value.newestPositionTs) // clear flightpath if scrolled back
|
|
){
|
|
cleanTracksFromOtherSrc(ac.hex, null);
|
|
var flight = Object.assign({}, ac);
|
|
flight.fpth = [newPos]; // single point flightpath
|
|
createLinestringFeature(trackSource, flight, ac.hex);
|
|
features = planes.get(ac.hex).features; // update features variable
|
|
}
|
|
var pathFeature = features.get("fpaths").values().next().value;
|
|
|
|
var newestPositionTs = pathFeature["newestPositionTs"];
|
|
if (newestPositionTs == undefined || ac.uti > newestPositionTs) { // only append new position if it's later in time than the previous
|
|
|
|
pathFeature.getGeometry().appendCoordinate(newPos);
|
|
pathFeature["newestPositionTs"] = ac.uti;
|
|
pathFeature["src"] = ac.src;
|
|
}
|
|
}
|
|
|
|
if(lockOnPlanePosition && (ac.hex == highlightedPlane)){
|
|
panCenterToPosition(newPos);
|
|
}
|
|
|
|
//update label postition
|
|
features.get("infoTextOverlay").setPosition(newPos);
|
|
//update label class (may have changed due to style-change)
|
|
$('#aclabel-'+ac.hex).removeClass(function (index, css) {
|
|
|
|
var classes = (css.match (/(^|\s)acInfoLabel-[a-z0-9]+$/gi) || []).join(' '); // get classname but not prio class
|
|
return classes;
|
|
});
|
|
$('#aclabel-'+ac.hex).addClass('acInfoLabel-'+getActiveDisplaystyle().shortname);
|
|
|
|
// update flightinfo
|
|
document.getElementById('aclabel-'+ac.hex).innerHTML = infoText;
|
|
|
|
|
|
} else { // need to create new feature
|
|
|
|
var aNode = document.createElement("div");
|
|
aNode.className="acInfoLabel overlay unselectable acInfoLabel-"+getActiveDisplaystyle().shortname;
|
|
aNode.id="aclabel-"+ac.hex;
|
|
aNode.addEventListener('mouseup', function(){ // use mouseup instead of click due to bug https://github.com/openlayers/openlayers/issues/10162 else clicking on overlay toggles menubar info
|
|
handlePlaneHighlightSwitch(planes.get(ac.hex));
|
|
});
|
|
|
|
if(lockOnPlanePosition && (ac.hex == highlightedPlane)){
|
|
panCenterToPosition(newPos);
|
|
}
|
|
|
|
document.getElementById('olmap_box').appendChild(aNode);
|
|
|
|
document.getElementById('aclabel-'+ac.hex).innerHTML = infoText; // set html of info node
|
|
$('#aclabel-'+ac.hex).hover(function(e){ // in handler
|
|
title = $(this).find('div').attr('data-mouseover');
|
|
$(this).append('<span><hr style="height:1px;border-top:1px solid black;margin: 1px 1px;padding: 0.5px;"/>'+title+'</span>');
|
|
$(this).removeAttr('title');
|
|
|
|
//$(this).parents('.ol-overlay-container,ol-selectable').css("z-index",999999); // place label on top
|
|
},
|
|
function(e){ // out handler
|
|
$('span', this).remove();
|
|
$(this).attr('title',title);
|
|
|
|
//$(this).parents('.ol-overlay-container,ol-selectable').css("z-index",""); // set z-index back to normal
|
|
|
|
}
|
|
);
|
|
|
|
var infoTextOverlay = new ol.Overlay({
|
|
position: newPos,
|
|
offset: [0,-30],
|
|
element: aNode,
|
|
stopEvent: false // don't catch mouse scroll events e.g./ would prevent map zooming
|
|
});
|
|
map.addOverlay(infoTextOverlay);
|
|
|
|
var acIconFeature = new ol.Feature({
|
|
geometry: new ol.geom.Point(newPos),
|
|
});
|
|
acIconFeature.hex = ac.hex; // enables us to do reverse mapping for click event
|
|
// use a style function to auto-update size and track (heading)
|
|
|
|
acIconFeature.setStyle(getActiveDisplaystyle().acIconStyle);
|
|
|
|
|
|
// highlight aircraft if matching a prioRule
|
|
if(Filters.prio.value !== null && Filters.prio.enabled){
|
|
Filters.prio.value.split(",").every(function(prioRule){
|
|
// for each rule, check aircraft hex whether it matches
|
|
if(prioRule === ac.hex || matchRuleShort(ac.hex, prioRule) || matchRuleShort(ac.reg, prioRule)){
|
|
$('#aclabel-'+ac.hex).addClass('acInfoLabel-'+getActiveDisplaystyle().shortname+"-prio");
|
|
return false; // one filter hit is enough, 'break'
|
|
}
|
|
return true; // continue
|
|
});
|
|
}
|
|
|
|
src.addFeature(acIconFeature);
|
|
|
|
// add plane refs to storage
|
|
var plane = planes.get(ac.hex);
|
|
plane.features.set("infoTextOverlay", infoTextOverlay);
|
|
plane.features.set("acIcon", acIconFeature);
|
|
}
|
|
if(ac.hex == highlightedPlane){
|
|
refreshSelectedPlaneInfo(); // refresh aircraft info in sidebar just after the icon has been moved
|
|
}
|
|
|
|
}
|
|
|
|
function updateURLPlaneSelection(){
|
|
var hashObj = {};
|
|
location.hash.replace('#', '').split('&').forEach(function(item){
|
|
hashObj[item.split('=')[0]] = item.split('=')[1];
|
|
});
|
|
highlightedPlane = hashObj.icao;
|
|
}
|
|
function handleUrlPlanelock(){
|
|
var hashObj = {};
|
|
location.hash.replace('#', '').split('&').forEach(function(item){
|
|
hashObj[item.split('=')[0]] = item.split('=')[1];
|
|
});
|
|
if(hashObj.icao){
|
|
var localPlane = planes.get(hashObj.icao);
|
|
if(localPlane != undefined){
|
|
highlightedPlane = hashObj.icao;
|
|
lockOnPlanePosition = true;
|
|
console.log("enable ac lock (urllock)");
|
|
map.getView().setZoom(12); // zoom to a decent level to identify the aircraft visually
|
|
panCenterToPosition(localPlane.features.get("acIcon").getGeometry().getCoordinates());
|
|
//disable dragging
|
|
map.getInteractions().forEach(function(interaction) {
|
|
if (interaction instanceof ol.interaction.DragPan) {
|
|
interaction.setActive(false);
|
|
} else if (interaction instanceof ol.interaction.MouseWheelZoom){
|
|
interaction.setMouseAnchor(false);
|
|
}
|
|
}, this);
|
|
switchLockButtonState(lockControlElement, lockOnPlanePosition);
|
|
} else {
|
|
rcToast("Could not find aircraft to lock on.");
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// highlights element of controlbar according to bool value
|
|
var setCtrlBarButtonIndication = function(_active, forElement){
|
|
// highlight button if filters are active
|
|
if(forElement) {
|
|
if(_active) {
|
|
$(forElement).addClass("ol-toggle ol-active");
|
|
} else {
|
|
$(forElement).removeClass("ol-toggle ol-active");
|
|
}
|
|
}
|
|
}
|
|
|
|
function switchLockButtonState(element, locked){
|
|
var icon_pinpoint = '<i class="icon ion-pinpoint"></i>';
|
|
var icon_locked = '<i class="icon ion-locked"></i>';
|
|
|
|
if(locked){
|
|
element.childNodes[0].innerHTML = icon_locked;
|
|
setCtrlBarButtonIndication(true, element);
|
|
|
|
} else {
|
|
element.childNodes[0].innerHTML = icon_pinpoint;
|
|
setCtrlBarButtonIndication(false, element);
|
|
}
|
|
}
|
|
|
|
//shows a toast notification
|
|
function rcToast(notification_text, permanent){
|
|
var showTime = (permanent === true) ? Number.MAX_SAFE_INTEGER/1000 : 4000; // default: use 4s show time
|
|
var toast_options = {
|
|
style: {
|
|
main: {
|
|
background: "pink",
|
|
color: "black"
|
|
}
|
|
},
|
|
settings: {
|
|
duration: showTime
|
|
}
|
|
};
|
|
iqwerty.toast.Toast(notification_text, toast_options);
|
|
}
|
|
|
|
function generateRcControlBar() {
|
|
var mainbar = new ol.control.Bar();
|
|
mainbar.setPosition("bottom-right")
|
|
|
|
/* Nested toobar with one control activated at once */
|
|
var nested = new ol.control.Bar ({ toggleOne: true, group:true });
|
|
mainbar.addControl (nested);
|
|
|
|
// highlights button of filterControl according to filter enabled status
|
|
var updateFilterIndication = function(forElement){
|
|
constrainingFiltersEnabled = false;
|
|
|
|
// check if filters are active
|
|
for (var property in Filters) {
|
|
if (Filters.hasOwnProperty(property)) {
|
|
if(!(Filters[property]["indicateChange"] === false) && Filters[property]["enabled"] && Filters[property].enabled &&
|
|
(Filters[property].value || Filters[property].from || Filters[property].to)){
|
|
constrainingFiltersEnabled = true; // show that filters are enabled
|
|
}
|
|
}
|
|
}
|
|
setCtrlBarButtonIndication(constrainingFiltersEnabled, forElement);
|
|
}
|
|
var filterControl = new ol.control.Button({
|
|
html: '<i class="icon ion-funnel"></i>',
|
|
title: 'Modify the filters',
|
|
handleClick: function() {
|
|
// update indication of filters enabled
|
|
updateFilterIndication(filterControl);
|
|
if($("#filter-settings").css('display') == 'none'){
|
|
invokeFilterControl(true, this.element, updateFilterIndication);
|
|
} else {
|
|
$("#filter-settings").hide();
|
|
}
|
|
}
|
|
});
|
|
updateFilterIndication(filterControl.element);
|
|
nested.addControl(filterControl);
|
|
|
|
// initialize filter control but don't show it yet
|
|
invokeFilterControl(false, document.getElementById("filter-settings"), updateFilterIndication);
|
|
|
|
// generate lock to plane pos control
|
|
var lockControl = new ol.control.Button({
|
|
html: '<i class="icon ion-pinpoint"></i>',
|
|
title: 'Lock on aircraft position',
|
|
handleClick: function(){
|
|
if(!lockOnPlanePosition){
|
|
if(highlightedPlane == undefined){
|
|
rcToast("Please select an aircraft first!");
|
|
return;
|
|
}
|
|
|
|
lockOnPlanePosition = true;
|
|
panCenterToPosition(planes.get(highlightedPlane).features.get("acIcon").getGeometry().getCoordinates());
|
|
//disable dragging
|
|
map.getInteractions().forEach(function(interaction) {
|
|
if (interaction instanceof ol.interaction.DragPan) {
|
|
interaction.setActive(false);
|
|
} else if (interaction instanceof ol.interaction.MouseWheelZoom){
|
|
interaction.setMouseAnchor(false);
|
|
}
|
|
}, this);
|
|
} else {
|
|
lockOnPlanePosition = false;
|
|
//enable dragging
|
|
map.getInteractions().forEach(function(interaction) {
|
|
if (interaction instanceof ol.interaction.DragPan) {
|
|
interaction.setActive(true);
|
|
} else if (interaction instanceof ol.interaction.MouseWheelZoom){
|
|
interaction.setMouseAnchor(true);
|
|
}
|
|
}, this);
|
|
location.hash = "";
|
|
}
|
|
switchLockButtonState(this.element, lockOnPlanePosition);
|
|
|
|
}
|
|
});
|
|
lockControlElement = lockControl.element;
|
|
nested.addControl(lockControl);
|
|
|
|
|
|
|
|
// generate home center control
|
|
var homeControl = new ol.control.Button({
|
|
html: '<i class="icon ion-home"></i>',
|
|
title: 'Center receiver position',
|
|
handleClick: function() {
|
|
lockOnPlanePosition = false;
|
|
setCtrlBarButtonIndication(false, lockControl.element);
|
|
panCenterToPosition(mapInfo.defaultCenter);
|
|
}
|
|
});
|
|
mainbar.addControl(homeControl);
|
|
|
|
/* Standard Controls */
|
|
var fullScreenExpandIcon = document.createElement("i");
|
|
fullScreenExpandIcon.classList.add("icon");
|
|
fullScreenExpandIcon.classList.add("ion-arrow-expand");
|
|
var fullScreenCloseIcon = document.createElement("i");
|
|
fullScreenCloseIcon.classList.add("icon");
|
|
fullScreenCloseIcon.classList.add("ion-arrow-shrink");
|
|
|
|
mainbar.addControl (new ol.control.FullScreen({
|
|
label: fullScreenExpandIcon,
|
|
labelActive: fullScreenCloseIcon,
|
|
source: document.getElementById('map-container')
|
|
}));
|
|
|
|
|
|
var rotateLockControl = new ol.control.Button({
|
|
html: `<span class="fa-stack">
|
|
<i class="fa fa-repeat fa-stack-2x"></i>
|
|
<i class="fa fa-lock fa-stack-1x"></i>
|
|
</span>`,
|
|
title: '(Un)lock Map Rotation',
|
|
handleClick: function(evt) {
|
|
rotateLockEnabled = rotateLockEnabled ? false : true; // toggle lock enabled
|
|
|
|
try {// persist setting
|
|
localStorage.setItem("ol_rotateLockEnabled", rotateLockEnabled);
|
|
} catch(e){}
|
|
|
|
setCtrlBarButtonIndication(rotateLockEnabled, rotateLockControl.element);
|
|
|
|
if(rotateLockEnabled === true){
|
|
var deleteList = [];
|
|
map.getInteractions().forEach(function(interx){
|
|
if(interx instanceof ol.interaction.PinchRotate || interx instanceof ol.interaction.DragRotate){
|
|
deleteList.push(interx);
|
|
}
|
|
});
|
|
deleteList.forEach(function(interx){
|
|
map.removeInteraction(interx);
|
|
});
|
|
} else {
|
|
map.addInteraction(new ol.interaction.PinchRotate());
|
|
map.addInteraction(new ol.interaction.DragRotate());
|
|
}
|
|
//ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false})
|
|
//setCtrlBarButtonIndication(false, lockControl.element);
|
|
}
|
|
|
|
});
|
|
//setCtrlBarButtonIndication(rotateLockEnabled, rotateLockControl.element);
|
|
//mainbar.addControl(rotateLockControl);
|
|
mainbar.addControl(new ol.control.Zoom());
|
|
return mainbar;
|
|
}
|
|
|
|
// adapts flightpath, icon and infotext overlay style to current display style
|
|
// also: if aircraft is not highlighted anymore, remove flightpath if needed
|
|
function restyleOrDeleteAircraftFeatures(hex, dontRemoveFlightpath){
|
|
// force restyling of acicon
|
|
var plane = planes.get(hex);
|
|
|
|
if(plane && plane.features.get("acIcon")){
|
|
plane.features.get("acIcon").setStyle(getActiveDisplaystyle().acIconStyle);
|
|
}
|
|
|
|
// refresh overlay essentially
|
|
updatePlaneFeature(planes.get(hex));
|
|
|
|
// trigger updating styles for each track feature
|
|
if(getActiveDisplaystyle().drawAllTracks === true || highlightedPlane == hex || staticModeEnabled){
|
|
// we draw all tracks, no need to remove any
|
|
planes.get(hex).features.get("fpaths").forEach(function(value){
|
|
trackFeature = value;
|
|
trackFeature.changed(); // trigger "changed"
|
|
});
|
|
} else if(highlightedPlane != hex && !dontRemoveFlightpath){
|
|
// track should be removed if aicraft is not highlighted anymore
|
|
cleanTracksFromOtherSrc(hex, null); // remove from any source
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function initialize_olmap()
|
|
{
|
|
$('#data-loading-error').hide();
|
|
$('#data-loading').hide();
|
|
|
|
// determine if history mode requested
|
|
var hashObj = {};
|
|
location.hash.replace('#', '').split('&').forEach(function(item){
|
|
hashObj[item.split('=')[0]] = item.split('=')[1];
|
|
});
|
|
if(hashObj.historymode){
|
|
console.log("History mode enabled");
|
|
historyModeEnabled = true;
|
|
aircraft_endpoint_prefix = rcd_globals.history_endpoint_prefix;
|
|
|
|
if(hashObj.staticmode){
|
|
staticModeEnabled = true;
|
|
initStaticView(location.hash);
|
|
} else {
|
|
initInteractiveHistoryControls();
|
|
}
|
|
} else {
|
|
$('.historycontrol').hide();
|
|
}
|
|
|
|
// in history mode, current time and live statistics are meaningless
|
|
if(!historyModeEnabled){
|
|
refreshGeneralInfo();
|
|
}
|
|
|
|
map_box = document.getElementById('olmap_box');
|
|
|
|
document.getElementById("btn_hide_info_box").onclick = toggleInfoBox;
|
|
|
|
$("#filter-settings").hide();
|
|
hideInfoBox();
|
|
|
|
/* Load Map Style from Cookie */
|
|
var _dsCookie = DisplayStyles.get(localStorage.getItem("ol_displaystyle"));
|
|
if(_dsCookie == undefined){
|
|
ActiveDisplayStyle = DisplayStyles.get("standard");
|
|
} else {
|
|
ActiveDisplayStyle = _dsCookie;
|
|
}
|
|
|
|
/* Load position and zoom from Cookie */
|
|
var currZoom = parseInt(localStorage.getItem("ol_zoom"));
|
|
var center_x = parseFloat(localStorage.getItem("ol_pos_x"));
|
|
var center_y = parseFloat(localStorage.getItem("ol_pos_y"));
|
|
var currCenter = [center_x, center_y];
|
|
|
|
/* Load filter data */
|
|
if(localStorage.getItem("ol-filters") !== null){
|
|
loadedFilters = Object.assign({}, Filters, JSON.parse(localStorage.getItem("ol-filters"))); // merge localStorage items but keep existing fields in case of update
|
|
|
|
Filters = sanitizeFilterValueTypes(Filters, loadedFilters);
|
|
}
|
|
{
|
|
var restoredRefreshInterval = parseInt(localStorage.getItem("ol-refreshInterval"));
|
|
if(restoredRefreshInterval != NaN && restoredRefreshInterval > 0){
|
|
refreshInterval = restoredRefreshInterval; // only restore from LC if value seems reasonable
|
|
}
|
|
}
|
|
|
|
rotateLockEnabled = localStorage.getItem("ol_rotateLockEnabled") === "false" ? false : true;
|
|
|
|
|
|
/* if existent, overwrite Filter entries with components of hashpart of url */
|
|
// FIXME not yet implemented
|
|
/*location.hash.replace('#', '').split('&').forEach(function(item){
|
|
var key = item.split('=')[0];
|
|
var value = item.split('=')[1];
|
|
|
|
if(domIdToFilterKey.get(key)){
|
|
console.log("overriding filter "+domIdToFilterKey.get(key)+" with value "+domIdToFilterKey.get(key)+" via url");
|
|
Filters[domIdToFilterKey.get(key)] = value;
|
|
} else {
|
|
console.log("filter to param "+key+" not found");
|
|
}
|
|
});*/
|
|
|
|
|
|
/* disable irrelevant view items when in history mode */
|
|
if(historyModeEnabled){
|
|
$(".inhist-notavail").find('*').prop('disabled', true); // disable controls/items not available in hist mode
|
|
$(".live-info").hide(); // hide labels etc. not making any sense in hist mode
|
|
if(staticModeEnabled){
|
|
$(".instatic-notavail").find('*').prop('disabled', true);
|
|
}
|
|
}
|
|
|
|
/*
|
|
define layer order
|
|
each layer group or feature layer gets a base index in which it may sort sub-layers on its own
|
|
*/
|
|
layerSetZIndexBase = {
|
|
'standard':0,
|
|
'atcscope':15,
|
|
'vectortiles':20,
|
|
'clientoverlay':30,
|
|
'openaip':51,
|
|
'tracks':60,
|
|
'aircrafts':70
|
|
}
|
|
|
|
acSource = new ol.source.Vector(); /* no loader, doesn't work*/
|
|
var genAcLayer = function(){
|
|
return new ol.layer.Vector({
|
|
name: "aircraft",
|
|
source: acSource,
|
|
zIndex: layerSetZIndexBase['aircrafts']
|
|
})
|
|
};
|
|
trackSource = new ol.source.Vector(); /* no loader, doesn't work*/
|
|
trackSource.on('removefeature', function(event){
|
|
featureId = event.feature.getId(); // e.g. hex-SRC-SERIAL
|
|
featureIdSplit = featureId.split(/-(.+)/);
|
|
hex = featureIdSplit[0];
|
|
srcAndSerial = featureIdSplit[1];
|
|
planes.get(hex).features.get("fpaths").delete(srcAndSerial);
|
|
})
|
|
var genTrackLayer = function(){
|
|
return new ol.layer.Vector({
|
|
name: "track",
|
|
source: trackSource,
|
|
zIndex: layerSetZIndexBase['tracks']
|
|
});
|
|
};
|
|
|
|
function findMapUrl(mapalias){
|
|
|
|
var formatPath = "";
|
|
if(true){
|
|
// temporary as long as tiles.jetvision.de is not online yet
|
|
formatPath = tileservers["maps"][mapalias]["direct"];
|
|
formatPath = formatPath.replace('{key}', 'x5sjkUM0TYj3iKSflPgs');
|
|
} else {
|
|
if(is_mlat_srv()){
|
|
formatPath = tileservers["online-path"];
|
|
} else {
|
|
formatPath = tileservers["device-path"];
|
|
}
|
|
}
|
|
formatPath = formatPath.replace('{{mapalias}}', mapalias);
|
|
formattedPath = formatPath.replace('{tile-schema}', tileservers["maps"][mapalias]["tile-schema"]);
|
|
return formattedPath;
|
|
}
|
|
|
|
var standard_layergroup = new ol.layer.Group({
|
|
title: DisplayStyles.get('standard').name,
|
|
displayStyle:"standard",
|
|
type: 'base',
|
|
combine: true,
|
|
zIndex: layerSetZIndexBase['standard'],
|
|
layers: [
|
|
new ol.layer.Tile({
|
|
title: 'OSM',
|
|
type: 'tiles',
|
|
visible: true,
|
|
zIndex: layerSetZIndexBase['standard']+1,
|
|
//crossOrigin: null,
|
|
source: new ol.source.XYZ({
|
|
url: findMapUrl("maptiler-bright"),
|
|
crossOrigin: 'anonymous',
|
|
attributions: new ol.Attribution({
|
|
html: '© <a target="_blank" href="https://www.maptiler.com/license/maps/" target="_blank">MapTiler</a>'
|
|
})
|
|
})
|
|
})
|
|
]
|
|
});
|
|
var atcscope_layergroup = new ol.layer.Group({
|
|
title: DisplayStyles.get('atcscope').name,
|
|
displayStyle:'atcscope',
|
|
type: 'base',
|
|
combine: true,
|
|
zIndex: layerSetZIndexBase['atcscope'],
|
|
layers : [
|
|
new ol.layer.Tile({
|
|
title: 'Dark',
|
|
type: 'tiles',
|
|
visible: true,
|
|
zIndex: layerSetZIndexBase['atcscope']+1,
|
|
source: new ol.source.XYZ({
|
|
url: findMapUrl("esri-darkgray"),
|
|
attributions: new ol.Attribution({
|
|
html: '© <a target="_blank" href="http://esri.com/" target="_blank">ESRI</a>'
|
|
}),
|
|
})
|
|
}) //end tilesource
|
|
]
|
|
});
|
|
|
|
var openaip_overlays = new ol.layer.Group({
|
|
title: 'OpenAIP',
|
|
combine: false,
|
|
zIndex: layerSetZIndexBase['openaip'],
|
|
layers : [
|
|
new ol.layer.Tile({
|
|
title: 'Navaids (on higher zoom levels)',
|
|
type: 'tiles',
|
|
visible: false,
|
|
opacity: 0.7,
|
|
zIndex: layerSetZIndexBase['openaip']+5,
|
|
source: new ol.source.TileWMS({
|
|
attributions: new ol.Attribution({
|
|
html: 'AIP features kindly provided by <a target="_blank" href="http://www.openaip.net/" target="_blank">http://www.openaip.net/</a>'
|
|
}),
|
|
url: "http://{1-4}.tile.maps.openaip.net/geowebcache/service/wms",
|
|
params: {LAYERS: 'openaip_approved_navaids', TRANSPARENT: true, TILED: true, SRS:'EPSG:900913'}
|
|
})
|
|
}),
|
|
new ol.layer.Group({
|
|
title: 'Airspace Geometries & Labels',
|
|
type: 'tiles',
|
|
visible: false,
|
|
combine: true,
|
|
zIndex: layerSetZIndexBase['openaip']+2,
|
|
layers:[
|
|
new ol.layer.Tile({
|
|
title:'Airspace Geometries',
|
|
opacity: 0.5,
|
|
type: 'tiles',
|
|
zIndex: layerSetZIndexBase['openaip']+3,
|
|
source: new ol.source.TileWMS({
|
|
attributions: new ol.Attribution({
|
|
html: 'AIP features kindly provided by <a target="_blank" href="http://www.openaip.net/" target="_blank">http://www.openaip.net/</a>'
|
|
}),
|
|
url: "http://{1-4}.tile.maps.openaip.net/geowebcache/service/wms",
|
|
params: {LAYERS: 'openaip_approved_airspaces_geometries', TRANSPARENT: true, TILED: true, SRS:'EPSG:900913'}
|
|
})
|
|
}),
|
|
new ol.layer.Tile({
|
|
title: 'Airspace Labels',
|
|
type: 'tiles',
|
|
opacity: 1.0,
|
|
zIndex: layerSetZIndexBase['openaip']+4,
|
|
source: new ol.source.TileWMS({
|
|
attributions: new ol.Attribution({
|
|
html: 'AIP features kindly provided by <a target="_blank" href="http://www.openaip.net/" target="_blank">http://www.openaip.net/</a>'
|
|
}),
|
|
url: "http://{1-4}.tile.maps.openaip.net/geowebcache/service/wms",
|
|
params: {LAYERS: 'openaip_approved_airspaces_labels', TRANSPARENT: true, TILED: true, SRS:'EPSG:900913'}
|
|
})
|
|
}),
|
|
]
|
|
}),
|
|
|
|
new ol.layer.Tile({
|
|
title: 'Airports',
|
|
type: 'tiles',
|
|
visible: false,
|
|
opacity: 1.0,
|
|
zIndex: layerSetZIndexBase['openaip']+1,
|
|
source: new ol.source.TileWMS({
|
|
attributions: new ol.Attribution({
|
|
html: 'AIP features kindly provided by <a target="_blank" href="http://www.openaip.net/" target="_blank">http://www.openaip.net/</a>'
|
|
}),
|
|
url: "http://{1-4}.tile.maps.openaip.net/geowebcache/service/wms",
|
|
params: {LAYERS: 'openaip_approved_airports', TRANSPARENT: true, TILED: true, SRS:'EPSG:900913'}
|
|
})
|
|
}),
|
|
//end tilesource
|
|
]
|
|
});
|
|
/* temporary workaround to prevent overlay from trying to be displayed at 0x0 pixels resolution (zoomed out) */
|
|
var ground_infra_overlay_extent = [12.576354, 41.786571, 12.609483, 41.813856];
|
|
ol.Sphere.getDistance = function(c1, c2) {
|
|
var radius = 6378137; // of WGS84 ellipsoid
|
|
function toRadians(deg) {var pi = Math.PI; return deg * (pi/180);}
|
|
var lat1 = toRadians(c1[1]);
|
|
var lat2 = toRadians(c2[1]);
|
|
var deltaLatBy2 = (lat2 - lat1) / 2;
|
|
var deltaLonBy2 = toRadians(c2[0] - c1[0]) / 2;
|
|
var a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) + Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) * Math.cos(lat1) * Math.cos(lat2);
|
|
return 2 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
};
|
|
/* --- */
|
|
|
|
// generates a blank layer that takes two callbacks to be run for visible and invisible setting of the layer
|
|
// purpose: toggling overlays via layerswitcher
|
|
function generateDummyLayer(options){
|
|
|
|
var layer = new ol.layer.Vector({
|
|
isBaseLayer: false,
|
|
visibility: true,
|
|
title: options.title, type: 'toggleableOverlay',
|
|
transparent: true
|
|
}
|
|
);
|
|
|
|
layer.on("change:visible", function(evt){
|
|
// if option is active, only pass available overlays in map if they contain string options.filterOverlaysBy
|
|
var filteredOverlaysPresent = map.getOverlays().getArray();
|
|
if(options.filterOverlaysBy !== undefined){
|
|
filteredOverlaysPresent = filteredOverlaysPresent.filter(function(overlay){
|
|
overlayId = overlay.getId();
|
|
|
|
if(overlayId !== undefined && overlayId.search(options.filterOverlaysBy) !== -1){
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
options.onSetVisibility(filteredOverlaysPresent, evt.target.get("visible"));
|
|
});
|
|
|
|
return layer;
|
|
}
|
|
|
|
var groundinfo_layergroup = new ol.layer.Group({
|
|
combine: false,
|
|
title: 'Local Ground Information',
|
|
zIndex: layerSetZIndexBase['clientoverlay'],
|
|
layers : function(){
|
|
var ret_layers = [];
|
|
|
|
ret_layers.push(
|
|
generateDummyLayer({
|
|
title: "Ground Stations",
|
|
onSetVisibility: function(matchingOverlays, visible){
|
|
matchingOverlays.forEach(function(overlay){
|
|
if(visible === true){
|
|
$(overlay.getElement()).show();
|
|
} else {
|
|
$(overlay.getElement()).hide();
|
|
}
|
|
});
|
|
},
|
|
filterOverlaysBy: "homeloc-"
|
|
})
|
|
);
|
|
|
|
if(is_mlat_srv()){
|
|
ret_layers.push(
|
|
new ol.layer.Tile({
|
|
title: 'Ground Infrastructure',
|
|
type: 'tiles',
|
|
opacity: 0.9,
|
|
visible:true,
|
|
extent: ol.proj.transformExtent(ground_infra_overlay_extent, 'EPSG:4326', 'EPSG:3857'),
|
|
zIndex: layerSetZIndexBase['clientoverlay']+1,
|
|
useInterimTilesOnError:false,
|
|
source: new ol.source.XYZ({
|
|
url: "/overlays/CIA/Ground Infrastructure/{z}/{x}/{-y}.png",
|
|
tilePixelRatio: 1.000000,
|
|
minZoom: 13,
|
|
maxZoom: 18
|
|
}),
|
|
maxResolution: Math.min(
|
|
ol.Sphere.getDistance(
|
|
[ground_infra_overlay_extent[0], ground_infra_overlay_extent[1]],
|
|
[ground_infra_overlay_extent[2], ground_infra_overlay_extent[1]]
|
|
),
|
|
ol.Sphere.getDistance(
|
|
[ground_infra_overlay_extent[0], ground_infra_overlay_extent[1]],
|
|
[ground_infra_overlay_extent[0], ground_infra_overlay_extent[3]]
|
|
)
|
|
)
|
|
})
|
|
);
|
|
}
|
|
|
|
return ret_layers;
|
|
}() // end layer generation function
|
|
});
|
|
|
|
/*let openflightmaps_overlays= new ol.layer.Group({
|
|
title: 'OpenFlightMaps',
|
|
combine: false,
|
|
layers : [
|
|
new ol.layer.Tile({
|
|
title: 'Airspace Geometries',
|
|
type: 'tiles',
|
|
visible: false,
|
|
opacity: 0.7,
|
|
source: new ol.source.XYZ({
|
|
attributions: new ol.Attribution({
|
|
html: 'AIP features kindly provided by <a target="_blank" href="https://openflightmaps.org/" target="_blank">https://openflightmaps.org/</a>'
|
|
}),
|
|
url: "https://snapshots.openflightmaps.org/live/1707/tiles/world/noninteractive/epsg3857/merged/512/latest/{z}/{y}/{x}.png",
|
|
})
|
|
}),
|
|
]
|
|
});*/
|
|
|
|
// https://snapshots.openflightmaps.org/live/1707/tiles/world/noninteractive/epsg3857/merged/512/latest/7/64/40.png
|
|
|
|
|
|
map = new ol.Map({
|
|
renderer: 'canvas',
|
|
target: 'olmap_box',
|
|
loadTilesWhileAnimating: true,
|
|
loadTilesWhileInteracting: true,
|
|
layers: [
|
|
new ol.layer.Group({
|
|
'title': 'Display Styles',
|
|
layers: [
|
|
//openflightmaps_overlays,
|
|
atcscope_layergroup,
|
|
standard_layergroup,
|
|
openaip_overlays
|
|
]
|
|
}),//end layer group
|
|
],
|
|
view: new ol.View({
|
|
center: currCenter,
|
|
zoom: currZoom
|
|
}),
|
|
controls: [
|
|
function(){
|
|
var bar = new ol.control.Bar();
|
|
bar.setPosition("bottom");
|
|
bar.addControl(new ol.control.ScaleLine());
|
|
|
|
return bar;
|
|
}(),
|
|
function(){
|
|
var bar = new ol.control.Bar();
|
|
bar.setPosition("bottom-left");
|
|
bar.addControl(new ol.control.Attribution({class: "ol-attribution"}));
|
|
return bar;
|
|
}(),
|
|
generateRcControlBar(),
|
|
// layerswitcher is added later
|
|
],
|
|
interactions : ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false}) // disable rotation
|
|
});
|
|
|
|
standard_layergroup.getLayers().push(genTrackLayer());
|
|
standard_layergroup.getLayers().push(genAcLayer());
|
|
atcscope_layergroup.getLayers().push(genTrackLayer());
|
|
atcscope_layergroup.getLayers().push(genAcLayer());
|
|
|
|
var main_overlaygroup = map.getLayerGroup().getLayers().getArray()[0].getLayers();
|
|
main_overlaygroup.push(groundinfo_layergroup);
|
|
|
|
function callbackOverlaysLoaded(){
|
|
//handle layer switches -- this needs to be done *after* home markers are loaded into DOM/map
|
|
ol.control.LayerSwitcher.forEachRecursive(map, function(l, idx, a) {
|
|
var layerType = l.get('type');
|
|
|
|
if (layerType === 'base') {
|
|
|
|
/* restore base layer/display style choice from LS*/
|
|
if(DisplayStyles.get(l.get("displayStyle")) === ActiveDisplayStyle){
|
|
setLayerVisible(l, true);
|
|
}
|
|
|
|
/* setup handling for layer changes / localstorage refresh */
|
|
l.on("change:visible", function(){
|
|
if(l.get("visible")){
|
|
ActiveDisplayStyle = DisplayStyles.get(l.get("displayStyle"));
|
|
try{
|
|
localStorage.setItem("ol_displaystyle",l.get("displayStyle"));
|
|
} catch (e){}
|
|
|
|
// refresh track style display
|
|
acSource.forEachFeature(function(feature){
|
|
restyleOrDeleteAircraftFeatures(feature.hex);
|
|
});
|
|
// remove all features from all aircrafts on map so they can be recreated/styled
|
|
|
|
if(!staticModeEnabled){
|
|
updateAvailablePlanes(historyTimerangeCurrentlyDisplayed)
|
|
.then(
|
|
function(){return updateAircraftFeatures(
|
|
false, // no forcefetch
|
|
historyTimerangeCurrentlyDisplayed,
|
|
null
|
|
);} // refresh view
|
|
).then(
|
|
function(){return updateTrack(trackSource);}
|
|
);
|
|
}
|
|
}
|
|
});
|
|
} else if(layerType === 'tiles' || layerType === 'toggleableOverlay' ){ // now handle all optional overlay layers as well as home location markers
|
|
|
|
/* restore previous active states once on startup */
|
|
var active_overlays_lskey = "ol-active_overlays";
|
|
var activeLayers = JSON.parse(localStorage.getItem(active_overlays_lskey));
|
|
var layerKey = l.get("title")+"@"+l.get("zIndex");
|
|
|
|
// restore active layers that are no basemaps
|
|
if(activeLayers != undefined){
|
|
if(activeLayers[layerKey] === true){
|
|
setLayerVisible(l, true);
|
|
} else if(activeLayers[layerKey] === false){
|
|
setLayerVisible(l, false); // layers that are explicitly disabled in Localstorage will be disabled, regardless whether their default is 'active'
|
|
}
|
|
} else {
|
|
activeLayers = {};
|
|
}
|
|
|
|
|
|
l.on("change:visible", function(){
|
|
var activeLayers = JSON.parse(localStorage.getItem(active_overlays_lskey)) || {};
|
|
|
|
if(l.get("visible")){
|
|
activeLayers[layerKey] = true;
|
|
console.log("activated "+layerKey+" "+JSON.stringify(activeLayers));
|
|
} else {
|
|
activeLayers[layerKey] = false;
|
|
console.log("deactivating "+layerKey+" "+JSON.stringify(activeLayers));
|
|
}
|
|
try {
|
|
localStorage.setItem(active_overlays_lskey, JSON.stringify(activeLayers));
|
|
} catch (e){} // end catch localstorage
|
|
});
|
|
}
|
|
});
|
|
|
|
/* ------------ now that all the layers are there and activation status is correct, render the layerswitcher */
|
|
map.addControl(new ol.control.LayerSwitcher({
|
|
tipLabel: 'Open Display Settings'
|
|
}));
|
|
}
|
|
|
|
|
|
// due to externally loaded css, the final size of the map is not known upon map object creation => update size after everything is loaded
|
|
window.addEventListener('load', function() {
|
|
//map.updateSize();
|
|
setMapDefaults(currZoom, currCenter, callbackOverlaysLoaded); // loads mapInfo and falls back to center and zoom defaults if no cookie set
|
|
});
|
|
|
|
|
|
window.addEventListener('custom_css_loaded', function(){
|
|
map.updateSize();
|
|
}, false);
|
|
|
|
|
|
|
|
// setup cookie update handlers
|
|
map.getView().on("change:resolution", function(event) {
|
|
// do not process event until zoom animation has stopped
|
|
if (map.getView().getAnimating()) {
|
|
return;
|
|
}
|
|
|
|
// update all icon sizes
|
|
acSource.forEachFeature(function(acIconFeature) {
|
|
//FIXME
|
|
//acIconFeature.setStyle(getAcIconStyle(planes.get(acIconFeature.hex)));
|
|
});
|
|
|
|
// refreshing the view is not needed since zooming is also a resolution change
|
|
});
|
|
|
|
map.on("moveend", function() {
|
|
try {
|
|
localStorage.setItem("ol_pos_x", map.getView().getCenter()[0]);
|
|
localStorage.setItem("ol_pos_y", map.getView().getCenter()[1]);
|
|
localStorage.setItem("ol_zoom",map.getView().getZoom()); // moveend also fires when zoom animation ends
|
|
} catch (e){}
|
|
|
|
// do not refresh aircrafts if user locked on one aircraft, this would lead to a loop
|
|
// also do not reload data when static Mode is enabled
|
|
if(!lockOnPlanePosition && !staticModeEnabled){
|
|
updateAvailablePlanes(historyTimerangeCurrentlyDisplayed).then(
|
|
function(){return updateAircraftFeatures(
|
|
false, // no forcefetch
|
|
historyTimerangeCurrentlyDisplayed
|
|
);}, // refresh view
|
|
function(){
|
|
console.log("updateAvailPlanes failed [non-critical] (no range passed?)");
|
|
}
|
|
).then(function(){return updateTrack(trackSource);});
|
|
}
|
|
});
|
|
|
|
//determine if aircraft needs to be selected, features don't fire click events
|
|
map.on('click', function(evt) {
|
|
|
|
var feature = map.forEachFeatureAtPixel(
|
|
evt.pixel,
|
|
function(feature, layer) {
|
|
if ( layer.get('name') === "aircraft" || layer.get('name') === "track" ){
|
|
return feature;
|
|
}
|
|
},
|
|
{hitTolerance: 5}
|
|
);
|
|
handlePlaneHighlightSwitch(feature);
|
|
});
|
|
|
|
$('#olmap_box').on('keypress', function(){
|
|
if(!historyModeEnabled) return true;
|
|
|
|
if(!event) var event = window.event; // cross-browser shenanigans
|
|
if(event.keyCode === 32) { // this is the spacebar
|
|
|
|
}
|
|
return true; // treat all other keys normally;
|
|
});
|
|
|
|
|
|
// When url hash info changes, also re-call plane lock
|
|
window.onhashchange = function() {
|
|
if(historyModeEnabled) return;
|
|
else if(window.location.hash && window.location.hash.indexOf("history") >= 0) {
|
|
// maybe user wants to enable history mode => reload
|
|
console.log("triggered window reload");
|
|
window.location.reload(true);
|
|
}
|
|
|
|
Promise.resolve(updateURLPlaneSelection())
|
|
.then(
|
|
function(){ return updateAircraftFeatures.call(acSource, null, null, true);}// forcefetchall=true
|
|
).then(
|
|
function(){ return Promise.resolve(handleUrlPlanelock());}
|
|
);
|
|
}
|
|
|
|
|
|
// create fullscreen handler adjusting some css
|
|
if (document.addEventListener) {
|
|
document.addEventListener('webkitfullscreenchange', fullscreenHandler, false);
|
|
document.addEventListener('mozfullscreenchange', fullscreenHandler, false);
|
|
document.addEventListener('fullscreenchange', fullscreenHandler, false);
|
|
document.addEventListener('MSFullscreenChange', fullscreenHandler, false);
|
|
}
|
|
|
|
function fullscreenHandler() {
|
|
if (document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement !== null){
|
|
var mcbox = $("#map-container");
|
|
var mibox = $("#map_info_box");
|
|
if(mibox.hasClass("mapinfo_embedded")){
|
|
mibox.removeClass("mapinfo_embedded");
|
|
mibox.addClass("mapinfo_fullscreen");
|
|
mcbox.addClass("mapcontainer_fullscreen");
|
|
|
|
} else {
|
|
mibox.removeClass("mapinfo_fullscreen");
|
|
mcbox.removeClass("mapcontainer_fullscreen");
|
|
mibox.addClass("mapinfo_embedded");
|
|
}
|
|
}
|
|
}
|
|
|
|
registerPositiveNumberInputHandlers();
|
|
|
|
loadAirports();
|
|
|
|
if(rotateLockEnabled === false){
|
|
map.addInteraction(new ol.interaction.PinchRotate());
|
|
map.addInteraction(new ol.interaction.DragRotate());
|
|
}
|
|
|
|
if(!historyModeEnabled){
|
|
// initial refresh of ac layer
|
|
new Promise(function(resolve){updateURLPlaneSelection(); resolve('urlplaneselect');})
|
|
.then(function(){ return updateAvailablePlanes();})
|
|
.then(function(){ return updateAircraftFeatures(true);}, dumpError)
|
|
.then(function(){ return Promise.resolve(handleUrlPlanelock());})
|
|
.then(function(){
|
|
var timerId;
|
|
timerId = setTimeout(function(){ return refreshData(true, null, null, timerId);}, 0); // define background data refresh actions
|
|
timers.refreshData = timerId;
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
function refreshData(regularRefresh, timerange, stepBackInHistory, timerId){
|
|
|
|
if(regularRefresh && timerId != timers.refreshData) {
|
|
return; // break this refresh loop since we were called by a timer which is not the current one in timers.refreshData (we have an outdated refreshInterval data)
|
|
}
|
|
|
|
var begunAtMs;
|
|
// start refresh promise chain
|
|
return new Promise(function(resolve){
|
|
begunAtMs = new Date().getTime();
|
|
resolve(true);
|
|
})
|
|
.then(function(){ return updateAvailablePlanes(timerange);})
|
|
.then(
|
|
// call updateAircraft in acSource context
|
|
function(){ return updateAircraftFeatures(false, timerange, null /*no json*/, stepBackInHistory);}
|
|
).then(
|
|
function(){ return updateTrack(trackSource);}
|
|
).then(
|
|
function(){ return Promise.resolve(refreshSelectedPlaneInfo());} // update sidebar info
|
|
).then(function(){
|
|
if(!historyModeEnabled){
|
|
refreshGeneralInfo(); // update time and num of planes in sidebar, in history mode this is meaningless and thus not used
|
|
updateStatistics();
|
|
|
|
}
|
|
return true; // the refreshed aren't critical to fail, so always pass a positive resolve value
|
|
}).finally(function(){
|
|
// if interval is unspecified, someone else must handle the periodicity
|
|
if(regularRefresh){
|
|
|
|
var nowMs = new Date().getTime();
|
|
var elapsed = nowMs - begunAtMs;
|
|
var waitingTimeLeft = refreshInterval - elapsed;
|
|
var timerId = null;
|
|
if(elapsed < refreshInterval){
|
|
// we're still in time and need to wait for the next iteration to be started
|
|
timerId = setTimeout(function(){ return refreshData(true, timerange, stepBackInHistory, timerId);} , waitingTimeLeft);
|
|
|
|
} else {
|
|
// we need to refresh ASAP
|
|
timerId = setTimeout(function(){ return refreshData(true, timerange, stepBackInHistory, timerId);}, 0);
|
|
}
|
|
timers.refreshData = timerId;
|
|
}
|
|
});
|
|
};
|
|
|
|
// remove aircrafts and all attached graphical objects when out of view e.g.
|
|
function removePlaneFeatures(plane){
|
|
if(!plane){
|
|
console.log("WARN: tried to remove plane features of ",plane);
|
|
}
|
|
plane.features.forEach(function(feature, key){
|
|
if(key === "infoTextOverlay"){
|
|
// remove overlay for ac infos
|
|
var labelOverlay = document.getElementById("aclabel-"+plane.hex);
|
|
if(labelOverlay != null){
|
|
labelOverlay.parentNode.removeChild(labelOverlay);
|
|
}
|
|
map.removeOverlay(feature);
|
|
plane.features.delete(key);
|
|
} else if(key === "fpaths"){
|
|
fpaths = feature; // actually the "fpaths" map value is itself a map of features, not a single feature
|
|
fpaths.forEach(function(fpath){
|
|
trackSource.removeFeature(fpath);
|
|
});
|
|
fpaths.clear(); // clear also the flightpaths map
|
|
}
|
|
else {
|
|
if(!feature){
|
|
//TODO console.log("Warn: undefined feature for "+plane.hex+" type: "+key);
|
|
} else {
|
|
acSource.removeFeature(feature);
|
|
}
|
|
plane.features.delete(key);
|
|
}
|
|
delete _acIconStyleCache[plane.hex];
|
|
});
|
|
}
|
|
|
|
function removeAllPlaneAndTrackFeatures(){
|
|
console.log("clearing inlastupdate due to removeallplaneandtrackft");
|
|
inLastUpdate.clear();
|
|
planes.forEach(function(aircraft){
|
|
removePlaneFeatures(aircraft);
|
|
});
|
|
}
|
|
|
|
function handlePlaneHighlightSwitch(feature){
|
|
|
|
// try to wait for trackupdate to finish for some seconds
|
|
var pollingIntervalMs = 10;
|
|
var waitMs = 5000 / pollingIntervalMs;
|
|
(function pollingDatasetReady (i) {
|
|
setTimeout(function () {
|
|
if(!trackUpdRunning) return;
|
|
if (--i) {pollingDatasetReady(i);}// decrement i and call loop again if i > 0
|
|
}, pollingIntervalMs)
|
|
})(waitMs);
|
|
|
|
// make sure we are able to get a hex-id out of our feature (not true for static image overlay e.g.)
|
|
// case: have aircraft selected, select another aircraft (highlight handover)
|
|
if (feature && (feature["hex"] || (typeof feature.getId === 'function' && feature.getId()) ) ) {
|
|
|
|
// FIXME:: ended up here: problem: when clicking flightpath it gets removed
|
|
|
|
|
|
if(highlightedPlane !== undefined){ // there was a highlight before, remove style from previously highlighted
|
|
var previouslyHighlighted = highlightedPlane;
|
|
highlightedPlane = undefined;
|
|
var dontRemoveFlightpath = false;
|
|
if(feature.geometryName_ == "flightpath"){ dontRemoveFlightpath = true;}
|
|
restyleOrDeleteAircraftFeatures(previouslyHighlighted, dontRemoveFlightpath);
|
|
$('#aclabel-'+previouslyHighlighted).removeClass("boldText"); // remove bold fontweight from previously highlighted ac
|
|
}
|
|
// set highlightedPlane hex to the one that caused the call of this function
|
|
if(feature["hex"] !== undefined){
|
|
// for selection via overlay Infotext
|
|
highlightedPlane = feature["hex"];
|
|
} else if(feature.get("hex") !== undefined){
|
|
// for selection via acFeature (icon)
|
|
highlightedPlane = feature.get("hex");
|
|
} else {
|
|
// for selection via ac Path
|
|
highlightedPlane = feature.getId().split('-')[0]; // extract the plane hex from trackfeature-id
|
|
}
|
|
|
|
$('#aclabel-'+highlightedPlane).addClass("boldText"); // make font of info label bold
|
|
|
|
// refresh style/ mark aircraft
|
|
restyleOrDeleteAircraftFeatures(highlightedPlane); // highlight the newly selected aircraft
|
|
if(lockOnPlanePosition && highlightedPlane){
|
|
panCenterToPosition(planes.get(highlightedPlane).features.get("acIcon").getGeometry().getCoordinates());
|
|
}
|
|
|
|
|
|
showInfoBox();
|
|
refreshSelectedPlaneInfo();
|
|
} else {
|
|
// case: highlight -> click on map to un-highlight e.g.
|
|
if(!lockOnPlanePosition){
|
|
var previouslyHighlighted = highlightedPlane;
|
|
highlightedPlane = undefined;
|
|
if(previouslyHighlighted){ // remove highlighted style after deselection
|
|
acIconFeature = planes.get(previouslyHighlighted).features.get("acIcon");
|
|
if(acIconFeature){
|
|
acIconFeature.setStyle(getActiveDisplaystyle().acIconStyle);
|
|
}
|
|
restyleOrDeleteAircraftFeatures(previouslyHighlighted);
|
|
$('#aclabel-'+previouslyHighlighted).removeClass("boldText"); // remove bold fontweight from previously highlighted ac
|
|
}
|
|
} else {
|
|
rcToast("Tracking-Lock enabled. Will not deselect aircraft.");
|
|
}
|
|
$("#filter-settings").hide();
|
|
}
|
|
|
|
refreshSelectedPlaneInfo(); //hide/update plane info in sidebar if necessary
|
|
|
|
if(!staticModeEnabled && !persistPaths){
|
|
return updateTrack(trackSource); // fetch/update the tracks
|
|
}
|
|
}
|
|
|
|
function panCenterToPosition(pos){
|
|
var pan = map.getView().animate({
|
|
duration: 500,
|
|
center: pos
|
|
});
|
|
}
|
|
|
|
var pathSerial = 1; // incremental serial number appended to each track/flightpath feature stored in trackSource so features with same hex and source don't clash (i.e. when displaying multiple tracks per ICAO)
|
|
var getPathSerial = function(){return pathSerial++};
|
|
|
|
function createLinestringFeature(tracksrc, flight, planehex){
|
|
|
|
// need a new feature since it doesn't exist so far
|
|
trackFeature = new ol.Feature({
|
|
flightpath: new ol.geom.LineString(flight.fpth),
|
|
});
|
|
trackFeature.setGeometryName('flightpath');
|
|
var pathSerial = getPathSerial();
|
|
trackFeature.setId(planehex+"-"+flight.src+"-"+pathSerial); // track id includes plane hex and source id
|
|
trackFeature.hex = planehex;
|
|
trackFeature.setStyle(function(feature){
|
|
|
|
var custWidth = 2;
|
|
var custLineDash = [3, 10];
|
|
var overrideColor = undefined;
|
|
if(planehex === highlightedPlane){ // highlighted track style
|
|
custWidth = 5;
|
|
custLineDash = undefined;
|
|
overrideColor = getActiveDisplaystyle().highlightedColor;
|
|
}
|
|
if(staticModeEnabled){
|
|
custLineDash = undefined;
|
|
}
|
|
|
|
return new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: overrideColor ? overrideColor : getActiveDisplaystyle().datasourceColorMap[flight.src],
|
|
lineDash: custLineDash,
|
|
width: custWidth
|
|
})
|
|
});
|
|
});
|
|
|
|
if(getActiveDisplaystyle().drawAllTracks === true || highlightedPlane === planehex || staticModeEnabled){
|
|
tracksrc.addFeature(trackFeature); // add track feature only if, while fetching the track, no other aircraft has been selected (yielding orphan track)
|
|
planes.get(planehex).features.get("fpaths").set(flight.src+"-"+pathSerial, trackFeature); // save feature reference together with the other features
|
|
console.log("added ft to", planes.get(planehex).features);
|
|
}
|
|
}
|
|
|
|
function updateTrack(src, json) {
|
|
return new Promise(function(resolve, reject){
|
|
|
|
if(trackUpdRunning) {console.log("refusing double-start of updateTrack"); resolve(false);}
|
|
if(!staticModeEnabled && !highlightedPlane && getActiveDisplaystyle().drawAllTracks === false){resolve(false);} // don't fetch tracks if there is no ac highlighted or the displaystyle forces us to draw all paths
|
|
if(historyModeEnabled && !staticModeEnabled && !historyTimerangeCurrentlyDisplayed){resolve(false);} // don't proceed, since timerange is not set yet in interactive history mode
|
|
if(persistPaths){resolve(false);} // dont' proceed fetching flightpaths concurrently to accumulation by persistPaths
|
|
|
|
var processJson = function(json) {
|
|
return new Promise(function(resolve, reject){
|
|
json.forEach(function(ac){
|
|
|
|
//move hex property to icao property (compat to history)
|
|
if(ac.hex){
|
|
ac.icao = ac.hex;
|
|
delete ac.hex;
|
|
}
|
|
var planehex = ac.icao;
|
|
|
|
//convert flat paths to single-element-array
|
|
if(Array.isArray(ac.fpth) && !Array.isArray(ac.fpth[0])){
|
|
ac.flights = [ {
|
|
src: ac.src || ac.fpth[0].src, // use source of first point as fallback
|
|
fpth: ac.fpth
|
|
} ];
|
|
delete ac.fpth;
|
|
}
|
|
|
|
// create reduced object of flight paths with just points and transformed positions
|
|
|
|
// reduce as replacement for flatMap
|
|
var flights = ac.flights.reduce(
|
|
function(acc,one_flight){
|
|
return acc.concat(function(/*one_flight*/){
|
|
|
|
if(one_flight.fpth !== undefined){
|
|
// flight information available here, but not accessed
|
|
return {
|
|
src: one_flight.src,
|
|
fpth: one_flight.fpth.map(function(point_data) {
|
|
var point = ol.proj.fromLonLat([parseFloat(point_data.lon), parseFloat(point_data.lat)]);
|
|
point.uti = parseFloat(point_data.uti);
|
|
return point;
|
|
})
|
|
};
|
|
} else if(one_flight.track !== undefined){ // staticdata.json splits flight into multiple paths
|
|
return one_flight.track.map(function(fpath){
|
|
var lastTrackPoint = fpath.slice(-1)[0];
|
|
return {
|
|
//[0] 'lat'
|
|
//[1] 'lon'
|
|
//[2] 'alt'
|
|
//[3] 'spd'
|
|
//[4] 'uti'
|
|
//[5] 'trk'
|
|
//[6] 'vrt'
|
|
//[7] 'src'
|
|
//[8] 'gda'
|
|
src: lastTrackPoint[7],
|
|
fpth: fpath.map(function(point_data) {
|
|
var point = ol.proj.fromLonLat([parseFloat(point_data[1]), parseFloat(point_data[0])]);
|
|
point.uti = parseFloat(point_data[4]);
|
|
return point;
|
|
})
|
|
};
|
|
});
|
|
}
|
|
|
|
}() // anon function, directly return
|
|
);
|
|
|
|
},
|
|
[]); // start with empty array as acc(umulator)
|
|
|
|
if(!planesAvailable.get(planehex)){
|
|
// adding a track for an non-available plane is forbidden
|
|
console.log("cannot add track for plane that is not in planesAvailable "+planehex);
|
|
return; // skip this ICAO
|
|
}
|
|
|
|
if(planes.get(planehex).features == undefined || !planes.get(planehex).features.get("acIcon")){
|
|
// we haven't got the plane infos yet, wait for next call
|
|
console.log("no ac features yet, quitting");
|
|
return; // skip this ICAO
|
|
}
|
|
|
|
var coords;
|
|
if(flights.length <= 0 || flights[0].fpth.length <=0){
|
|
coords = planes.get(planehex).features.get("acIcon").getGeometry().getCoordinates(); // no track points, borrow position for track feature from icon
|
|
} else {
|
|
coords = flights[0].fpth[0]; // use first point of first path
|
|
}
|
|
// clear features that were from other source than current data source (e.g. source switched between last fetch and now)
|
|
// additionally clear tracks with hex=planehex of new source, since we cannot distinguish the sub-tracks we need to regenerate feature objects each time we get new data
|
|
cleanTracksFromOtherSrc(planehex, null);
|
|
|
|
/* We have no flightpath information -> display question marks point feature
|
|
*/
|
|
if(flights.length === 0 || flights[0].fpth.length === 0){
|
|
|
|
trackFeature = new ol.Feature({
|
|
point: new ol.geom.Point(coords),
|
|
});
|
|
trackFeature.setGeometryName('point');
|
|
var fallbackSrc = planes.get(planehex).src;
|
|
trackFeature.setId(planehex+"-"+fallbackSrc+"-"+getPathSerial()); // track id includes plane hex and source id
|
|
trackFeature.hex = planehex;
|
|
|
|
// use a style function to auto-update position
|
|
trackFeature.setStyle(function(feature, resolution){
|
|
return getActiveDisplaystyle().trackMissingStyle(feature.getGeometry().getCoordinates());
|
|
});
|
|
if(getActiveDisplaystyle().drawAllTracks === true || highlightedPlane === planehex){
|
|
src.addFeature(trackFeature);
|
|
planes.get(planehex).features.get("fpaths").set("nosrc-trackempty", trackFeature); // save feature reference together with the other features
|
|
}
|
|
|
|
} else if(persistPaths === false){ // appending pos to flightpath is handled in updatePlaneFeature for persistPath
|
|
flights.forEach(function(flight){
|
|
createLinestringFeature(src, flight, planehex);
|
|
});
|
|
}
|
|
});
|
|
refreshSelectedPlaneInfo(); //publish changes in sidebar
|
|
trackUpdRunning = false;
|
|
|
|
resolve(true);
|
|
});
|
|
}
|
|
|
|
|
|
if(json){ // in static display, we don't need to fetch the track data before processing but use the passed json
|
|
resolve(
|
|
Promise.resolve(trackUpdRunning = true)
|
|
.then(function(){ return processJson(json);})
|
|
.then(function(){trackUpdRunning=false; return true;})
|
|
);
|
|
} else {
|
|
// fetch all tracks needed
|
|
var pathsToFetch = new Map(); // array of hex ids of planes
|
|
if(getActiveDisplaystyle().drawAllTracks){
|
|
pathsToFetch = new Map(inLastUpdate); // include all planes
|
|
} else if(highlightedPlane) {
|
|
pathsToFetch.set(highlightedPlane, true); // include only highlighted plane
|
|
}
|
|
|
|
// compares bounding box of all feature's geometries with the current map view and only if they don't intersect,
|
|
// acHasFeaturesInCurrentExtent stays false
|
|
keepPathsFromEviction = new Map();
|
|
var currentExtent = map.getView().calculateExtent(map.getSize());
|
|
var trackFeaturesInExtent = trackSource.getFeaturesInExtent(currentExtent);
|
|
for(let i=0; i<trackFeaturesInExtent.length; i++){
|
|
var featureInExtent = trackFeaturesInExtent[i];
|
|
keepPathsFromEviction.set(featureInExtent.hex, 1); // this is used for checking in selfdestruct of display_styles
|
|
pathsToFetch.set(featureInExtent.hex, 1);
|
|
}
|
|
|
|
if(pathsToFetch.size <= 0){
|
|
pathsToFetch = [];
|
|
} else {
|
|
// remove weird 'undefined' entry at array front that Array.from generates for no reason
|
|
var pathsToFetch = Array.from(pathsToFetch.keys()).filter(function(val){
|
|
return typeof val === 'string' || val instanceof String
|
|
});
|
|
}
|
|
|
|
if(pathsToFetch.length == 0){trackUpdRunning = false; resolve(true); return;} // nothing to fetch
|
|
|
|
|
|
// build querystring with multiple icaos
|
|
var qstr = "";
|
|
for(var i=0; i<pathsToFetch.length; i++){
|
|
qstr += "icao="+pathsToFetch[i];
|
|
if( i!==pathsToFetch.length-1){
|
|
qstr += "&"; // append & not at end
|
|
}
|
|
}
|
|
|
|
// if only single flightpath is displayed, set source for it
|
|
if(pathsToFetch.length == 1){
|
|
qstr += "&src="+planes.get(pathsToFetch[0]).src;
|
|
} else {
|
|
qstr += serializeSrcExclFilter();
|
|
}
|
|
|
|
// add lookback in seconds
|
|
if(Filters.tracklength.enabled && Filters.tracklength.value != null){
|
|
qstr += "&lookback="+Filters.tracklength.value;
|
|
} else if(getActiveDisplaystyle().defaultTrackLookbackSecs !== undefined){
|
|
qstr += "&lookback="+getActiveDisplaystyle().defaultTrackLookbackSecs;
|
|
} else if(historyModeEnabled){
|
|
qstr += "&lookback="+300; // need to provide sensible default for history mode
|
|
}
|
|
// else lookback is decided by server
|
|
|
|
if(historyModeEnabled){
|
|
rangesize = historyTimerangeCurrentlyDisplayed.to - historyTimerangeCurrentlyDisplayed.from;
|
|
// intentionally request end of displayed timeframe, so ac icon and its newest trackpoint are "connected"
|
|
qstr += "×tamp="+Math.round(historyTimerangeCurrentlyDisplayed.to);
|
|
}
|
|
|
|
resolve(
|
|
fetch(rcd_globals.aircraft_endpoint_prefix+'flightpath.json?'+qstr, customRequestOptions).then(function(data) {
|
|
return data.json();
|
|
}).then(processJson, function(err) {
|
|
if(!err.message.includes("Failed to fetch")){
|
|
console.log("A problem occurred: "+err.message);
|
|
dumpError(err);
|
|
}
|
|
}).catch(function(err){
|
|
console.log("[updateTrack] Failed processing json: "+err.message);
|
|
dumpError(err);
|
|
}).finally(function(){
|
|
trackUpdRunning = false;
|
|
})
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function dumpError(err) {
|
|
if (typeof err === 'object') {
|
|
if (err.message) {
|
|
console.log('\nMessage: ' + err.message)
|
|
}
|
|
if (err.stack) {
|
|
console.log('\nStacktrace:')
|
|
console.log('====================')
|
|
console.log(err.stack);
|
|
}
|
|
} else {
|
|
console.log('dumpError :: argument is not an object');
|
|
}
|
|
}
|
|
|
|
// removes features of all sources but the one given by 'dontDeleteSrcId', if latter is undefined, removes all
|
|
function cleanTracksFromOtherSrc(hex, dontDeleteSrcId){
|
|
|
|
// try to remove track which come from sources that should be dropped
|
|
// if dontDeleteSrcId is null or undefined, then delete all features with given hex and any source
|
|
planes.get(hex).features.get("fpaths").forEach(function(fpathFeature, key){
|
|
// key is in format "sourceid-serial"
|
|
var sourceId = key.split("-")[0];
|
|
if(sourceId == dontDeleteSrcId){return;}
|
|
else {
|
|
trackSource.removeFeature(fpathFeature);
|
|
planes.get(hex).features.get("fpaths").delete(key);
|
|
}
|
|
});
|
|
|
|
}
|
|
function setMapDefaults(currZoom, currCenter, callbackOverlaysLoaded){
|
|
mapInfo = {}; // attention, this is global
|
|
|
|
fetch(rcd_globals.mapinfo_endpoint_prefix+'mapinfo.json', {credentials: 'include'}).then(function(data) {
|
|
return data.json();
|
|
}).then(function(json) {
|
|
mapInfo.distunit = json.distunit;
|
|
mapInfo.alt = json.alt;
|
|
mapInfo.timesrc = json.tme;
|
|
mapInfo.defaultZoom = json.zoom;
|
|
mapInfo.lon = json.lon;
|
|
mapInfo.lat = json.lat;
|
|
mapInfo.defaultCenter = ol.proj.fromLonLat([mapInfo.lon, mapInfo.lat]);
|
|
|
|
|
|
/*if (json.rings) {
|
|
var generateRingFeature = function(i, dist_unit) {
|
|
var dist_factor = 50000 / ol.proj.METERS_PER_UNIT.m;
|
|
if (dist_unit !== "km") {
|
|
dist_factor = dist_factor * 1.852; // nautical miles to kilometers
|
|
}
|
|
return new ol.Feature({
|
|
geometry: new ol.geom.Circle(center, i * dist_factor)
|
|
});
|
|
};
|
|
ringsSource = new ol.source.Vector({
|
|
features: [1, 2, 3, 4, 5].map(function(x) {
|
|
return generateRingFeature(x, json.distunit)
|
|
})
|
|
});
|
|
mapInfo.ringsLayer = new ol.layer.Vector({
|
|
source: ringsSource,
|
|
style: new ol.style.Style({
|
|
fill: new ol.style.Fill({
|
|
color: 'rgba(255, 255, 255, 0.0)'
|
|
}),
|
|
stroke: new ol.style.Stroke({
|
|
color: 'rgba(0, 0, 0, 0.6)',
|
|
width: 1
|
|
})
|
|
})
|
|
});
|
|
}*/
|
|
|
|
// update map defaults
|
|
if (currZoom <= 0 || isNaN(currZoom) || currZoom == null) {
|
|
map.getView().setZoom(mapInfo.defaultZoom);
|
|
}
|
|
if(isNaN(currCenter[0]) || currCenter[0]==null || isNaN(currCenter[1]) || currCenter[1]==null){
|
|
map.getView().setCenter(mapInfo.defaultCenter);
|
|
}
|
|
|
|
$('#filterdiag-distanceUnit').html("("+mapInfo.distunit+")");
|
|
|
|
/* Add home location icon(s) */
|
|
var markers;
|
|
if(!is_mlat_srv()){
|
|
// marker is fetched from mapinfo.json
|
|
map.addOverlay(getHomeMarkers(mapInfo));
|
|
callbackOverlaysLoaded();
|
|
} else {
|
|
// markers are fetched from clients.json (inputs for mlatserver)
|
|
fetch(rcd_globals.sensors_endpoint_prefix+'clients.json', {credentials: 'include'}).then(function(data) {
|
|
if (!data.ok) {
|
|
throw Error(data.statusText);
|
|
}
|
|
return data.json();
|
|
}).then(function(json) {
|
|
$.each(getHomeMarkers(json["TLS"]), function(idx,marker){
|
|
map.addOverlay(marker);
|
|
});
|
|
callbackOverlaysLoaded();
|
|
}).catch(function(err) {
|
|
if(!err.message.includes("Failed to fetch")){
|
|
console.log("A problem occurred: "+err.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
}).catch(function(err) {
|
|
if(!err.message.includes("Failed to fetch")){
|
|
console.log("A problem occurred: "+err.message);
|
|
}
|
|
});
|
|
|
|
// try to zoom to given extent in hash part of url (if it exists)
|
|
zoomToHashExtent(map);
|
|
|
|
}
|
|
|
|
function is_mlat_srv(){
|
|
if(rcd_globals == undefined || Object.keys(rcd_globals).length === 0){
|
|
alert("The application isn't configured properly, rcd_globals is '"+rcd_globals+"'");
|
|
}
|
|
return rcd_globals["productname"].indexOf("MLAT_SERVER") !== -1;
|
|
}
|
|
|
|
// get a set of home location marker overlays
|
|
// input: string => assume mapinfo.json as source
|
|
// input: array => assume clients.json from MLAT
|
|
function getHomeMarkers(data){
|
|
var newImgNode = function(stationID){
|
|
var divNode = document.createElement("div");
|
|
divNode.className = "overlay unselectable receiverlocation-marker";
|
|
|
|
imageContainer = document.createElement("div")
|
|
imageContainer.className = "receiverlocation-marker-imagecontainer";
|
|
divNode.appendChild(imageContainer);
|
|
/*
|
|
if(stationID){
|
|
divNode.appendChild(document.createTextNode(stationID));
|
|
}*/
|
|
var imgNode = document.createElement("img");
|
|
imgNode.src = 'img/antenna.ico';
|
|
imgNode.id = 'homelocation-marker-'+btoa(Math.random()).substring(0,12); // generate some random id
|
|
// document.getElementById('olmap_box').appendChild(imgNode);
|
|
imageContainer.appendChild(imgNode);
|
|
return divNode;
|
|
}
|
|
var newOverlayPrototype = function(pos, stationID){
|
|
return new ol.Overlay({
|
|
id: "homeloc-"+stationID,
|
|
position: ol.proj.fromLonLat([pos.lon, pos.lat]),
|
|
positioning: 'center-center',
|
|
element: newImgNode(stationID),
|
|
stopEvent: false
|
|
});
|
|
};
|
|
|
|
if(!Array.isArray(data)){
|
|
return newOverlayPrototype({"lon":data.lon, "lat":data.lat}, null);
|
|
} else {
|
|
var markers = [];
|
|
for(var station in data){
|
|
var newmarker = newOverlayPrototype({"lon":data[station].lon, "lat":data[station].lat}, station.id); // clone proto
|
|
markers.push(newmarker);
|
|
}
|
|
return markers;
|
|
}
|
|
}
|
|
|
|
function showInfoBox()
|
|
{
|
|
|
|
$('#hide_info').css('visibility', 'visible');
|
|
$('#map_info_box').css("width", '210px');
|
|
$('#hide_info_btn').removeClass("glyphicon-plus");
|
|
$('#hide_info_btn').addClass("glyphicon glyphicon-minus");
|
|
}
|
|
|
|
function hideInfoBox()
|
|
{
|
|
$('#hide_info').css('visibility', 'hidden');
|
|
$('#map_info_box').css("width", '1px');
|
|
$('#hide_info_btn').removeClass("glyphicon-minus");
|
|
$('#hide_info_btn').addClass("glyphicon glyphicon-plus");
|
|
|
|
}
|
|
function toggleInfoBox()
|
|
{
|
|
if($('#hide_info').css('visibility') === "hidden"){
|
|
showInfoBox();
|
|
} else {
|
|
hideInfoBox();
|
|
}
|
|
}
|
|
|
|
|
|
function invokeFilterControl(showMenu, filterEnv, callback){
|
|
var filterDialog = document.getElementById('filter-settings');
|
|
|
|
function parseParameterArray(input) {
|
|
if(!input || input.trim() == "") {
|
|
return null;
|
|
} else {
|
|
splitted = input.split(/[\s,]+/); // split to array trimmed
|
|
splitted = splitted.filter(function(el){return el.trim()}); // filter empty entries
|
|
return splitted;
|
|
}
|
|
}
|
|
|
|
function restyleCriticalValueFields(){
|
|
// highlight filters with critical values
|
|
if(Filters.tracklength.value != null && Filters.tracklength.enabled === true && Filters.tracklength.value < 10){
|
|
console.log("value is "+Filters.tracklength.value)
|
|
$("#filter-tracklength").addClass("highlight-filterfield");
|
|
} else {
|
|
$("#filter-tracklength").removeClass("highlight-filterfield");
|
|
}
|
|
}
|
|
|
|
function handleChange(event) {
|
|
|
|
function convertEmptyNumToNull(parsedNum){
|
|
return isNaN(parsedNum) ? null : parsedNum;
|
|
}
|
|
function convertEmptyStrToNull(str){
|
|
if(str === undefined || str === ""){
|
|
return null;
|
|
}
|
|
return str;
|
|
}
|
|
function splitRejoinCsv(str){
|
|
str = convertEmptyStrToNull(str);
|
|
if(str != null){
|
|
str = str.split(",").map(function(item){
|
|
return item.trim();
|
|
}).join(",");
|
|
str = str.toUpperCase();
|
|
}
|
|
return str;
|
|
}
|
|
|
|
var regularRefresh = false; // at end of this function, refresh only once by default
|
|
|
|
// prio css class is truncated when handlechange forces recreation of all features
|
|
|
|
// collect filter values
|
|
Filters.altitude.from = convertEmptyNumToNull($("#filter-alt-min").val());
|
|
Filters.altitude.to = convertEmptyNumToNull($("#filter-alt-max").val());
|
|
Filters.speed.from = convertEmptyNumToNull($("#filter-spd-min").val());
|
|
Filters.speed.to = convertEmptyNumToNull($("#filter-spd-max").val());
|
|
Filters.distance.from = convertEmptyNumToNull($("#filter-dis-min").val());
|
|
Filters.distance.to = convertEmptyNumToNull($("#filter-dis-max").val());
|
|
Filters.flight.value = parseParameterArray($("#filter-fli").val());
|
|
Filters.squawk.value = splitRejoinCsv($("#filter-squ").val());
|
|
Filters.orig.value = parseParameterArray($("#filter-org").val());
|
|
Filters.dest.value = parseParameterArray($("#filter-dst").val());
|
|
Filters.type.value = parseParameterArray($("#filter-typ").val());
|
|
Filters.prio.value = splitRejoinCsv($("#filter-prio").val());
|
|
Filters.tracklength.value = convertEmptyNumToNull(parseInt($("#filter-tracklength").val(), 10));
|
|
Filters.fleetwatch.value = parseParameterArray($("#filter-fleet").val());
|
|
|
|
parsedRefreshInterval = convertEmptyNumToNull(parseInt($("#refresh-interval").val(), 10));
|
|
if(refreshInterval != parsedRefreshInterval){ // did interval change?
|
|
clearTimeout(timers.refreshData); // clear timeout with old refresh interval (is replaced with refreshData with a new refreshInterval later)
|
|
refreshInterval = parsedRefreshInterval;
|
|
regularRefresh = true; // since refreshInterval changed, we need to get a new loop in place
|
|
}
|
|
|
|
Filters.srcpref.value = $("#preferred-source").val() || null;
|
|
|
|
Filters.activeSources.value = $('#filter-sources').serializeArray().map(function(src){
|
|
return src.value;
|
|
});
|
|
|
|
// auto-enable on input when terminated with keypress (enter/return)
|
|
if(event != null && event.keyCode == 13){
|
|
var baseid = $(event.target).attr('id');
|
|
$('#'+baseid+"-enabled").prop('checked', true);
|
|
}
|
|
|
|
// collect filter enable switches
|
|
Filters.prio.enabled = $('#filter-prio-enabled').is(':checked');
|
|
Filters.altitude.enabled = $('#filter-alt-enabled').is(':checked');
|
|
Filters.speed.enabled = $('#filter-spd-enabled').is(':checked');
|
|
Filters.distance.enabled = $('#filter-dis-enabled').is(':checked');
|
|
Filters.gndexcl.enabled = $('#filter-gnd-enabled').is(':checked');
|
|
Filters.flight.enabled = $('#filter-fli-enabled').is(':checked');
|
|
Filters.squawk.enabled = $('#filter-squ-enabled').is(':checked');
|
|
Filters.orig.enabled = $('#filter-org-enabled').is(':checked');
|
|
Filters.dest.enabled = $('#filter-dst-enabled').is(':checked');
|
|
Filters.type.enabled = $('#filter-typ-enabled').is(':checked');
|
|
Filters.fleetwatch.enabled = $('#filter-fleet-enabled').is(':checked');
|
|
|
|
if(Filters.srcpref.value != ""){
|
|
Filters.srcpref.enabled = true;
|
|
}
|
|
// check whether some activeSources are set
|
|
if(Filters.activeSources.value){
|
|
Filters.activeSources.enabled = true;
|
|
} else {
|
|
Filters.activeSources.enabled = false; // setting was not set, must be default/disabled
|
|
}
|
|
{
|
|
// check whether set of active sources equals the default set
|
|
var activeSources = Filters.activeSources.value.sort();
|
|
if(activeSources.length==defaultActiveSourceIds.length && activeSources.every(function(v,i){return v === defaultActiveSourceIds[i]})){
|
|
Filters.activeSources.enabled = false; // user selected default set, so disable filter highlight indicator
|
|
}
|
|
}
|
|
|
|
restyleCriticalValueFields();
|
|
|
|
|
|
// try persisting filter settings
|
|
try {
|
|
localStorage.setItem("ol-filters",JSON.stringify(Filters));
|
|
localStorage.setItem("ol-refreshInterval",refreshInterval);
|
|
} catch (e){}
|
|
|
|
callback(filterEnv);
|
|
|
|
|
|
var changedFilterId = undefined;
|
|
if(event){
|
|
changedFilterId = domIdToFilterKey.get(event.target.id); // e.g. event.target.id=activeSources
|
|
}
|
|
if( (changedFilterId && /* we need to know where the event came from, else force refresh */
|
|
!event.target.id.includes("enabled") /* enable-checkbox was not triggered (if so, we need refresh */
|
|
) && (
|
|
Filters[changedFilterId].enabled == false /* filter now OFF, any input doesn't change view */
|
|
)){
|
|
return;
|
|
}
|
|
if( (changedFilterId && /* we need to know where the event came from, else force refresh */
|
|
event.target.id.includes("enabled") /*&&Filters[changedFilterId].enabled == true*/ ) && /* enable-checkbox WAS used to turn filter on*/
|
|
!Filters[changedFilterId].value) /* but the field that was turned on, contains no data, so no refresh needed*/
|
|
{return;}
|
|
|
|
|
|
if(showMenu && !staticModeEnabled){ // only refresh map if menu is opened by user (and map is initialized)
|
|
if(historyModeEnabled && !historyTimerangeCurrentlyDisplayed) return;
|
|
|
|
removeAllPlaneAndTrackFeatures();
|
|
|
|
console.log("removed all plane and track ft");
|
|
|
|
var timerId;
|
|
timerId = setTimeout(
|
|
function(){
|
|
return refreshData(regularRefresh /*bring potentially new regular refresh interval in place */, historyTimerangeCurrentlyDisplayed, null, timerId);
|
|
},
|
|
0 // don't wait
|
|
);
|
|
if(regularRefresh){
|
|
timers.refreshData = timerId; // set timer id only if we actually are creating a new refresh *loop*, not just oneshot
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// if filterdialog has not been shown so far, initialize it
|
|
if (filterDialog !== null && !filterEnv.contains(filterDialog)) {
|
|
filterEnv.appendChild(filterDialog); // make filterDialog child of button
|
|
|
|
|
|
// Register change handlers for input(textfield) and change (checkboxes)
|
|
$( "#filter-prio, #filter-prio-enabled, " +
|
|
"#filter-alt-min, #filter-alt-max, #filter-alt-enabled, " +
|
|
"#filter-spd-min, #filter-spd-max, #filter-spd-enabled, " +
|
|
"#filter-dis-min, #filter-dis-max, #filter-dis-enabled, " +
|
|
"#filter-gnd-enabled, " +
|
|
"#filter-fli, #filter-fli-enabled, "+
|
|
"#filter-squ, #filter-squ-enabled, "+
|
|
"#filter-org, #filter-org-enabled, "+
|
|
"#filter-dst, #filter-dst-enabled, "+
|
|
"#filter-typ, #filter-typ-enabled, "+
|
|
"#filter-tracklength, "+
|
|
"#filter-fleet, #filter-fleet-enabled, "+
|
|
".filtersource,"+
|
|
"#preferred-source, #refresh-interval").change(handleChange).on('input', handleChange).on('keypress', handleChange);
|
|
|
|
|
|
$('.filter-close').click(function(e) {
|
|
$("#filter-settings").hide();
|
|
});
|
|
$('.filter-clear').click(function() {
|
|
$("#filter-prio, " +
|
|
"#filter-alt-min, #filter-alt-max, " +
|
|
"#filter-spd-min, #filter-spd-max, " +
|
|
"#filter-dis-min, #filter-dis-max, " +
|
|
"#filter-fli, #filter-squ, #filter-org, #filter-dst, #filter-typ, #filter-tracklength, #filter-fleet").val('');
|
|
|
|
$("#filter-prio-enabled" +
|
|
"#filter-alt-enabled, #filter-spd-enabled, " +
|
|
"#filter-dis-enabled, #filter-fli-enabled, " +
|
|
"#filter-org-enabled, #filter-dst-enabled, " +
|
|
"#filter-typ-enabled, #filter-tracklength-enabled, " +
|
|
"#filter-fleet-enabled, #filter-squ-enabled").prop('checked', false);
|
|
|
|
$("#filter-gnd-enabled").prop('checked', true);
|
|
|
|
defaultActiveSourceIds.forEach(function(src){
|
|
$('#filter-sources input[value='+src+']').prop('checked', true);
|
|
});
|
|
|
|
document.getElementById('preferred-source').selectedIndex=0;
|
|
document.getElementById('refresh-interval').selectedIndex=1;
|
|
handleChange();
|
|
});
|
|
|
|
$("#filter-apply").click(function() {
|
|
$("#filter-settings").hide();
|
|
});
|
|
}
|
|
if(showMenu){
|
|
$('#filter-settings').show();
|
|
}
|
|
|
|
// restore filter values
|
|
$("#filter-alt-min").val(Filters.altitude.from);
|
|
$("#filter-alt-max").val(Filters.altitude.to);
|
|
$("#filter-spd-min").val(Filters.speed.from);
|
|
$("#filter-spd-max").val(Filters.speed.to);
|
|
$("#filter-dis-min").val(Filters.distance.from);
|
|
$("#filter-dis-max").val(Filters.distance.to);
|
|
if(Filters.flight.value){
|
|
$("#filter-fli").val(Filters.flight.value.join(','));
|
|
}
|
|
if(Filters.orig.value){
|
|
$("#filter-org").val(Filters.orig.value.join(','));
|
|
}
|
|
if(Filters.dest.value){
|
|
$("#filter-dst").val(Filters.dest.value.join(','));
|
|
}
|
|
if(Filters.type.value){
|
|
$("#filter-typ").val(Filters.type.value.join(','));
|
|
}
|
|
$("#filter-prio").val(Filters.prio.value);
|
|
$("#filter-squ").val(Filters.squawk.value);
|
|
|
|
$("#filter-tracklength").val(Filters.tracklength.value);
|
|
if(Filters.fleetwatch.value){
|
|
$("#filter-fleet").val(Filters.fleetwatch.value.join(','));
|
|
}
|
|
$("#preferred-source").val(Filters.srcpref.value);
|
|
|
|
|
|
// restore enabled flags
|
|
$("#filter-prio-enabled").prop('checked', Filters.prio.enabled);
|
|
$("#filter-alt-enabled").prop('checked', Filters.altitude.enabled);
|
|
$("#filter-spd-enabled").prop('checked', Filters.speed.enabled);
|
|
$("#filter-dis-enabled").prop('checked', Filters.distance.enabled);
|
|
$("#filter-gnd-enabled").prop('checked', Filters.gndexcl.enabled);
|
|
$("#filter-fli-enabled").prop('checked', Filters.flight.enabled);
|
|
$("#filter-squ-enabled").prop('checked', Filters.squawk.enabled);
|
|
$("#filter-org-enabled").prop('checked', Filters.orig.enabled);
|
|
$("#filter-dst-enabled").prop('checked', Filters.dest.enabled);
|
|
$("#filter-typ-enabled").prop('checked', Filters.type.enabled);
|
|
$("#filter-fleet-enabled").prop('checked', Filters.fleetwatch.enabled);
|
|
|
|
if(Filters.srcpref.value != ""){
|
|
Filters.srcpref.enabled = true;
|
|
}
|
|
if(Filters.activeSources.value && Filters.activeSources.value.length < sourceIds.length){
|
|
|
|
sourceIds.forEach(function(src){
|
|
var setChecked = false;
|
|
if(Filters.activeSources.value.indexOf(src) >= 0){
|
|
setChecked = true;
|
|
}
|
|
$('#filter-sources input[value='+src+']').prop('checked', setChecked);
|
|
});
|
|
|
|
restyleCriticalValueFields();
|
|
}
|
|
|
|
$("#refresh-interval").val(refreshInterval);
|
|
//handleChange(); // call handleChange to initialize
|
|
}
|
|
|
|
|
|
|
|
function getTimeString(isLocal) {
|
|
var hours;
|
|
var minutes;
|
|
var date = new Date();
|
|
var zone_offset = date.getTimezoneOffset();
|
|
|
|
if(isLocal)
|
|
{
|
|
hours = date.getHours();
|
|
minutes = date.getMinutes();
|
|
}
|
|
else
|
|
{
|
|
hours = date.getUTCHours();
|
|
minutes = date.getUTCMinutes();
|
|
}
|
|
|
|
if(hours < 10) {hours = '0'+hours;}
|
|
if(minutes < 10) {minutes = '0'+minutes;}
|
|
|
|
var zeit_string = hours + ':' + minutes;
|
|
|
|
return zeit_string;
|
|
}
|
|
|
|
// disable input of negative values in matching input fields
|
|
function registerPositiveNumberInputHandlers() {
|
|
|
|
$("input.positive-numeric-only").on("keydown", function(e) {
|
|
var char = e.originalEvent.key.replace(/[^0-9^.^,]/, "");
|
|
if (char.length == 0 && !(e.originalEvent.ctrlKey || e.originalEvent.metaKey)) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
$("input.positive-numeric-only").bind("paste", function(e) {
|
|
var numbers = e.originalEvent.clipboardData
|
|
.getData("text")
|
|
.replace(/[^0-9^.^,]/g, "");
|
|
e.preventDefault();
|
|
var the_val = parseInt(numbers);
|
|
});
|
|
|
|
$("input.positive-numeric-only").focusout(function(e) {
|
|
if (!isNaN(this.value) && this.value.length != 0) {
|
|
this.value = parseInt(this.value);
|
|
} else {
|
|
this.value = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
// check type of each 'value' of the filters and reset it to default if it has got the wrong one
|
|
function sanitizeFilterValueTypes(defaultFilters, loadedFilters){
|
|
for (var key in loadedFilters) {
|
|
if(defaultFilters[key] === undefined){
|
|
delete loadedFilters[key];
|
|
continue; // filter stored in user's LS is not part of defaultfilters anymore, skip it.
|
|
}
|
|
if(Array.isArray(loadedFilters[key].value) != Array.isArray(defaultFilters[key].value) ){
|
|
loadedFilters[key].value = defaultFilters[key].value;
|
|
}
|
|
}
|
|
return loadedFilters;
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
initialize_olmap();
|
|
});
|