432 lines
17 KiB
JavaScript
432 lines
17 KiB
JavaScript
|
|
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;
|
|
}
|