function timeout(ms, promise) { var timerPromise = new Promise(function(resolve, reject) { var start = new Date(); if(ms<0) {/*console.log("graceful timeout ignore");*/ return;} setTimeout(function() { var now = new Date(); reject(new Error('Request Timeout after '+ms+'ms')); }, ms) }) return Promise.race([timerPromise, promise]) } // you can pass a json object that's used instead of an http request to get the data function updateAircraftFeatures(forceFetchAll, timerange, json, useEarliestTimestamp){ return new Promise(function(resolve, reject){ if(updateAcFeaturesRunning){console.log("refusing double-start of updateAcFeatures"); resolve(false);} if(historyModeEnabled && !staticModeEnabled && !timerange){console.log("cannot get ac features without timerange in history mode"); resolve(false);} if(staticModeEnabled && !json){reject("Cannot run updateAcFeatures in static mode without prepared JSON");} updateAcFeaturesRunning = true; var processJson = function(json) { var outstandingAircraftObjectRefreshes = new Map(); return new Promise(function(resolve, reject){ if(json === undefined) reject('no input json defined'); if(!rediscoverMode){ inLastUpdate.clear(); // clear Map } json.forEach( function(plane){ if(Object.keys(plane).length === 0){ resolve(false); // don't process empty aircraft objects } inLastUpdate.set(plane.hex, "1"); //include ac in inLastUpdate Map (there is no false state) var planeLocal = planes.get(plane.hex); // reference to plane in local Map planesAvailable.set(plane.hex, true); if(plane.fli){ plane.fli = $.trim(plane.fli); // remove spaces from flight number } if(planeLocal != undefined && planeLocal.features.get("acIcon")){ // check if there is any position info for that aircraft already //updating existing plane obj /* in case of multiple datapoints returned for the same aircraft, prefer either the oldest or newest timestamp: * overwrite plane info only if new datapoint is *newer* than the stored one (default) * or if new datapoint is *older* than the stored one (option, when going back in history) */ if( (!useEarliestTimestamp && (plane.uti > planeLocal.uti)) || (useEarliestTimestamp && (plane.uti < planeLocal.uti)) ){ Object.assign(planeLocal, plane); // merge/update cached plane info } else { // do not overwrite promise if aircraft information was not updated // the aircraft is not older/newer than our stored one, and displayed on the map, don't need to call updatePlaneFeature if(planeLocal.features.get("acIcon")){ return; } } } else { if(!planeLocal){ return; // skip processing this aircraft -> see #80 //reject("aircraft hex "+plane.hex+" was not found in plane Map, did you miss running updateAvailablePlanes?"); } // aircraft in aircraft map is only a stub Object.assign(planeLocal, plane); } outstandingAircraftObjectRefreshes.set( plane.hex, new Promise(function(resolve){updatePlaneFeature(plane); resolve(true)}) // update single airplane on layer ); }); if(planes.get(highlightedPlane)){ highlightedPlaneSrc = planes.get(highlightedPlane).src; // update the latest source the highlighted aircraft was discovered from } else { highlightedPlaneSrc = undefined; } // if not forced to fetch whole view, update the number of aircrafts in view if(!forceFetchAll){ AircraftsVisible.inView = inLastUpdate.size; } /* Remove idle planes from map and cache */ acTracksToRemove = new Map(); planes.forEach(function(plane, hex, planes) { // if(!planesAvailable.get(hex)){console.log(hex,"is not in global sight? ", planesAvailable.get(hex));} if (planesAvailable.get(hex) !== true) { // plane is not in global sight anymore if(hex === highlightedPlane){ highlightedPlane = undefined; // remove highlight } cleanTracksFromOtherSrc(hex, null); // delete flightpaths from any source for that hex if(plane.features){ removePlaneFeatures(plane); } planes.delete(hex); } }); resolve(Promise.all(outstandingAircraftObjectRefreshes)); }); } if(json){ resolve( new Promise(function(resolve){ resolve(updateAcFeaturesRunning = true); }).then( function(){return processJson(json);} ).then(function(){ return updateAcFeaturesRunning = false; }) ); } else { var extentDefinition = ol_getviewportExtent(); var baseurl=rcd_globals.aircraft_endpoint_prefix+'aircraftlist.json?'; if(historyModeEnabled){ baseurl=rcd_globals.history_endpoint_prefix+'history.json?'; } // try to rediscover aircraft that's being tracked by the user var rediscoverMode = false; // flag that indicates if we want to find a specific ICAO (e.g. when feature aircraftlist->track aircraft is used) if( highlightedPlane != undefined && !inLastUpdate.get(highlightedPlane) && Filters.activeSources.value.includes(highlightedPlaneSrc) && !constrainingFiltersEnabled // don't rediscover if user specified filters that could exclude the highlightedPlane from the view (highlightedplane would be shown even if it doesn't match the filter!) ){ forceFetchAll = true; // FIXME: sould be renamed to "ignoreExtent" rediscoverMode = true; console.log("rediscovering aircraft"); } if(forceFetchAll){ extentDefinition = undefined; // force fetching all available planes, not only from the extent } var paramObj = filtersetToParams(Filters); delete paramObj.tracklength; paramObj = $.extend( paramObj, extentDefinition ); if(!historyModeEnabled){ paramObj.knownposonly = 1; // this param is not supported in history mode since only positioned aircrafts are stored } else { paramObj = $.extend( paramObj, { 'timeframe.from': timerange.from, 'timeframe.to': timerange.to } ); console.log("[updateAcFeatures] fetching range from "+moment.unix(timerange.from).utc().toString()+" "+timerange.from+" to "+moment.unix(timerange.to).utc().toString()+" "+timerange.to); } var params=decodeURIComponent($.param(paramObj)); /* Temporarily disable filtering by ICAO or sources when forced to fetch whole view (callback wants to search all available aircrafts) */ if(rediscoverMode && !historyModeEnabled){ console.log("Rediscovery fetch"); params += "&icao="+highlightedPlane; } else { params += serializeFilterArrays(); } var url=baseurl+params; var fetchOpts = {credentials: 'include'}; resolve( new Promise(function(resolve, reject){ resolve(updateAcFeaturesRunning = true); }).then( function(){ return timeout(-1, // give any load more time (infinite), TODO: show warning that refresh rate is set too high fetch(url, fetchOpts).then(function(data) { if (!data.ok) { throw Error(data.statusText); } return data.json(); }, dumpError).then(function(data){return processJson(data);}, function(err) { //reject if(!err.message.includes("Failed to fetch")){ console.log("[updAcFeatures] A problem occurred fetching "+url+": "+err.message); } else { console.log(err.message); } }) ); // return timeouting promise }).finally(function(){ updateAcFeaturesRunning = false; }) ); } }); // end promise } function updateStatistics(){ getJSON( rcd_globals.mapinfo_endpoint_prefix+"mapinfo.json", function(stat_data) { AircraftsVisible.receivable = replaceUnavailable(stat_data["dbsize"]); AircraftsVisible.valid = replaceUnavailable(stat_data["watched"]); AircraftsVisible.countBySource = { "A" : replaceUnavailable(stat_data["num_adsb"]), "M" : replaceUnavailable(stat_data["num_mlat"]), "L" : replaceUnavailable(stat_data["num_flarm"]) // OGN and FLAwA are external sources for which no ac count exists! }; }); } // update the available aircraft Map // returns: promise function updateAvailablePlanes(timerange, json){ return new Promise(function(resolve,reject){ var baseurl=rcd_globals.aircraft_endpoint_prefix+'aircraftlist.json?'; if(historyModeEnabled){ baseurl=rcd_globals.history_endpoint_prefix+'history.json?'; } var params = {}; if(!historyModeEnabled){ params = { 'select': "avail", 'knownposonly': "1", } } // We fetch non-extent limited aircraftlist and cache it for later recycling by updateAircraftFeatures else if(!staticModeEnabled) { // using no timerange if(!timerange) {reject("no timerange passed to updateAvailPlanes in non-static history mode");} params = $.extend( params, {'timeframe.from': timerange.from, 'timeframe.to': timerange.to} ); console.log("[updateAvailPlanes] fetching range from "+moment.unix(timerange.from).utc().toString()+" to "+moment.unix(timerange.to).utc().toString()); } else { /* in static mode, we use prepared json as input, so no timerange data is needed */} var fetchOpts = {credentials: 'include'}; function processAircraftsAvailableResponse(json) { if(!json){ console.log("No aircraft in specified timerange, continuing"); resolve(false); } // update number of planes AircraftsVisible.located = json.length; var pa = new Map(); if(json === undefined) reject(new Error("no json passed to processAircraftsAvailableResponse")); json.forEach(function(plane){ pa.set(plane.hex, true); if(!planes.has(plane.hex)){ acObj = { features: new Map([ [ "fpaths", new Map() ] ]) }; planes.set(plane.hex, acObj); } }); planesAvailable = pa; } if(!staticModeEnabled){ resolve( fetch(baseurl+$.param(params), fetchOpts).then(function(data) { return data.json(); }) .then(processAircraftsAvailableResponse, function(err) { // reject if(!err.message.includes("Failed to fetch")){ console.log("[updateAvailPlanes] A problem occurred: "+err.message); } }) ); } else { resolve(Promise.resolve(processAircraftsAvailableResponse(json))); } }); } function ol_getviewportExtent(){ var viewport = {}; wrapLon = function(value){ var worlds = Math.floor((value + 180) / 360); return value - (worlds * 360); } var extent = map.getView().calculateExtent(map.getSize()); var bottomLeft = ol.extent.getBottomLeft(extent); var topRight = ol.extent.getTopRight(extent); /* Extend viewport to include incoming and outgoing aircrafts (to prevent "corner-hangers" The Pixels might not be available yet (map is not yet loaded asynchronously, so this extent expansion is optional. */ var paddingPixels = 60; // 60 pixels // unit = Meter for EPSG 3857 var pixelPerUnit = 1/map.getView().getResolution() ; var paddingUnits = paddingPixels / pixelPerUnit; var paddingDegrees = paddingUnits / ol.proj.Units.METERS_PER_UNIT.degrees; // transform to WGS84 bottomLeft = ol.proj.transform(bottomLeft, 'EPSG:3857', 'EPSG:4326'); topRight = ol.proj.transform(topRight, 'EPSG:3857', 'EPSG:4326'); bottomLeft[0] -= paddingDegrees; bottomLeft[1] -= paddingDegrees; topRight[0] += paddingDegrees; topRight[1] += paddingDegrees; viewport.swlat = bottomLeft[1]; viewport.swlon = wrapLon(bottomLeft[0]); viewport.nelat = topRight[1]; viewport.nelon = wrapLon(topRight[0]); var point = new ol.geom.Point(map.getView().getCenter()); // get center point.transform("EPSG:900913","EPSG:4326");// transform center to wgs84 viewport.latcntr = point.getCoordinates()[1]; viewport.loncntr = point.getCoordinates()[0]; return { vpn: viewport.nelat, vpe: viewport.nelon, vps: viewport.swlat, vpw: viewport.swlon, vplat: viewport.latcntr, vplon: viewport.loncntr }; } function filtersetToParams(filters){ var filterParams = {}; Object.keys(filters).forEach(function(key){ var filter = filters[key]; // arrays are handled separately since they may contain duplicate keys if( ( (filter.enabled && !filter.invert) || (filter.invert===true && !filter.enabled)) && !(Array.isArray(filter.value))){ if(filter.value || filter.value === 0){ // 0 is also an allowed value // this is a single value filterParams[key] = filters[key].value; } else if(filter != null && (filter.from || filter.to)) { //this is regarded as a [from, to] pair Object.keys(filter).forEach(function(subkey){ if( subkey !== "enabled" && // this attribute is just a flag of whether filter is enabled subkey !== "domnodes" && // domnodes attribute is used otherwise (in UI) (filter[subkey] != null) && (filter[subkey] != "") // don't send empty filters ) { filterParams[key+'.'+subkey] = filter[subkey]; } }); } } }); return filterParams; } function serializeFilterArrays(){ // possible improvement: pass {key: [1,2,3]} -> .param() -> key=1&key=2... (less boilerplate) var params = ""; if(Filters.fleetwatch.enabled && Array.isArray(Filters.fleetwatch.value) && parseInt(Filters.fleetwatch.value.length) > 0){ params += "&icao="; params += Filters.fleetwatch.value.join("&icao="); } params += serializeSrcExclFilter(); /*if(Filters.showGnd.enabled === false && Filters.showGnd.value === 1 ){ params += "&gndexcl=1"; }*/ if(Filters.type.enabled && Array.isArray(Filters.type.value) && parseInt(Filters.type.value.length) > 0){ params += "&type="; params += Filters.type.value.join("&type="); } if(Filters.flight.enabled && Array.isArray(Filters.flight.value) && parseInt(Filters.flight.value.length) > 0){ params += "&flight="; params += Filters.flight.value.join("&flight="); } if(Filters.orig.enabled && Array.isArray(Filters.orig.value) && parseInt(Filters.orig.value.length) > 0){ params += "&orig="; params += Filters.orig.value.join("&orig="); } if(Filters.dest.enabled && Array.isArray(Filters.dest.value) && parseInt(Filters.dest.value.length) > 0){ params += "&dest="; params += Filters.dest.value.join("&dest="); } return params; } function serializeSrcExclFilter(){ // possible improvement: pass {key: [1,2,3]} -> .param() -> key=1&key=2... var params=""; if(Array.isArray(Filters.activeSources.value)){ var disabledSources = $(sourceIds).not(Filters.activeSources.value).get(); if(disabledSources.length > 0){ params += "&srcexcl="; params += disabledSources.join("&srcexcl="); } } return params; }