/********************************************************************
* 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 = '';
} else if(ac.vrt > 10){
vRateIndicator = '';
} else if(ac.vrt < -10){
vRateIndicator = '';
} 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('
'+title+'');
$(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 = '';
var icon_locked = '';
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: '',
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: '',
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: '',
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: `
`,
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: '© MapTiler'
})
})
})
]
});
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: '© ESRI'
}),
})
}) //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 http://www.openaip.net/'
}),
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 http://www.openaip.net/'
}),
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 http://www.openaip.net/'
}),
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 http://www.openaip.net/'
}),
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 https://openflightmaps.org/'
}),
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 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();
});