/******************************************************************** * 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(); });