Files
eacp_webapp/htdocs/js/ol-model_updaters.js
T
2026-06-16 11:24:50 +08:00

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;
}