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

680 lines
27 KiB
JavaScript

var playbackRunning = false;
var playbackSpeedFactor = 1;
var refreshIntervalSecs = 1;
var historyTimerangeCurrentlyDisplayed = null;
var persistPaths = false;
// return object with attributes matching the filter being excluded/dropped
var filter_object = function(obj, filter, invertMatch) {
var filteredObj = {};
for (key in obj) {
if(invertMatch){
if (obj.hasOwnProperty(key) && !filter.test(key)) {
filteredObj[key] = obj[key];
}
} else {
if (obj.hasOwnProperty(key) && filter.test(key)) {
filteredObj.key = obj[key];
}
}
}
return filteredObj;
}
function playbackChangeState(state) {
playbackRunning = state;
if(state){
$('#control-play').prop('disabled', true);
$('#control-pause').prop('disabled', false);
} else {
$('#control-play').prop('disabled', false);
$('#control-pause').prop('disabled', true);
}
}
// filter out k/v pairs from query/hash part of url which match pattern
function filter_hashPartUrl(str, filter){
filteredComponents = [];
location.hash.replace('#', '').split('&').forEach(function(item){
var key = item.split('=')[0];
var val = item.split('=')[1];
if(!filter.test(key)){
filteredComponents.push(item);
}
});
return filteredComponents.join('&');
}
// downloads static data, locks filters and interaction and
function initStaticView(urlHashPartStr) {
prepareHistoryUIElements();
$('#data-loading').show();
var urlHashFiltered = filter_hashPartUrl(urlHashPartStr, /mode/);
// timeout is larger than server's timeout returning 5xx when it fails delivering
timeout(600000, fetch(rcd_globals.history_endpoint_prefix+'staticdata.json?'+urlHashFiltered, customRequestOptions)).then(function(data){
if (!data.ok) {
throw Error(data.statusText);
}
return data.json().then(function(json){
if(Object.keys(json).length === 0) rcToast("Response from API was empty. Either the timeframe you requested was very small or the server hasn't stored the requested data anymore", true);
return json;
});
}).then(function(json) {
aircraftListJson = []; // fed into updateAircraftFeatures
flightPathsJson = []; // fed into updateTrack
// generate a `fake` aircraftist.json from the last positions of the aircrafts
json.forEach(function (ac){
// FIXME: we need dynamic metadata based on where in the user clicks!
var ac_metainfo = filter_object(ac, /flights/, true);
var lastFlight = ac.flights.slice(-1)[0]; // slicing returns an array (len 1)
var ac_metainfo_flight = filter_object(lastFlight, /track/, true);
var lastTrack = lastFlight.track.slice(-1)[0];
var lastTrackSlice = lastTrack.slice(-1)[0]; //last/newest track point of last path is used as position of icon
var lastTrackPoint = { //last/newest track point of last path is used as position of icon
'lat': lastTrackSlice[0],
'lon': lastTrackSlice[1],
'alt': lastTrackSlice[2],
'spd': lastTrackSlice[3],
'uti': lastTrackSlice[4],
'trk': lastTrackSlice[5],
'vrt': lastTrackSlice[6],
'src': lastTrackSlice[7],
'gda': lastTrackSlice[8],
};
Object.assign(ac_metainfo, lastTrackPoint); //merge metadata from last trackpoint to flight object
Object.assign(ac_metainfo, ac_metainfo_flight);
aircraftListJson.push(ac_metainfo);
//ac.src = lastTrackPoint.src; //assume src of all flights for trackJson to be the one of the last point
/*ac_flightsbundle = {
icao: ac.hex,
src: ac.src, // assume source is aircraft-specific, does not change when between flights
flights: []
};
ac.flights.forEach(function(flight){
// flight-specific info, independent of aircraft
var flight_info = {
fli: flight.fli,
cou: flight.cou,
dst: flight.dst,
org: flight.org,
fpth: []
};
console.log(flight);
flight.track.forEach(function(point){
flight_info.fpth.push({
uti: point.uti,
lat: point.lat,
lon: point.lon,
alt: point.alt,
gnd: point.gnd
});
});
ac_flightsbundle.flights.push(flight_info);
});*/
//console.log(ac);
flightPathsJson.push(ac);
});
updateAvailablePlanes(null, aircraftListJson)
.then(function(){ return updateAircraftFeatures(
false, // no forcefetch
null, // no timerange needed with prepared json
aircraftListJson // prepared json
);}) // populate aircrafts on map
.then(function(){return updateTrack(trackSource, flightPathsJson);});
$('#data-loading').hide();
instantiateDisabledSlider();
}).catch(function(err) {
if(!err.message.includes("Failed to fetch")){
console.log("[staticdownload] A problem occurred: "+err.message);
rcToast("Couldn't fetch static view from API, you may try reloading the page when the server is available again (Error was: "+err.message+")", true);
$('#data-loading').hide();
$('#data-loading-error').show();
}
});
}
function prepareHistoryUIElements(){
// scale mapcontainer to size that controls have space
$(".container-fluid-map").addClass("mapcontainer_timecontrols");
$('.historycontrol').show();
// disable playback and pause button per default
$('#control-play').prop('disabled', true);
$('#control-pause').prop('disabled', true);
$('#refresh-interval-cell').hide();
$('.control-staticdownload').prop('disabled', true);
}
function instantiateDisabledSlider(){
// sliderhull, the object in the DOM is replaced after this call with an object with the new id specified as data-slider-id
var slider = new Slider('#sliderHull', {
formatter: function(value) {
// read unix timestamp in seconds and print out time highlighted
var str = moment(value, "X").utc().format("YYYY-MM-DD HH:*B*mm:ss*BB*");
str = str.replace("*B*", ""); // originally meant to insert tags to style time, but it's not working
str = str.replace("*BB*", " UTC");
return str;
},
tooltip: "hide",
handle: "sqare"
});
slider.disable();
return slider;
}
function initInteractiveHistoryControls(){
prepareHistoryUIElements();
var slider = instantiateDisabledSlider();
//restore playback speed from localStorage
parseAndSetPlaybackspeed(localStorage.getItem("ol_playbackspeed"));
// restore whether to accumulate flight points
persistPaths = (localStorage.getItem("ol_persistPaths") === 'true');
if(persistPaths === true){
$("#control-persist").addClass("active");
}
function computeRangeObject(minDate, maxDate){
var lda = {};
// if minDate is after start of maxDate day, set minDate as start of selected timerange
// (our available daterange doesnt even span a day, thus selecting a day would fail)
// attention: instantiate moment before using a variable since startOf etc. actions aren't const
if(minDate.diff(moment(maxDate).startOf('day')) > 0){
lda.begin = minDate;
lda.end = maxDate;
} else {
lda.begin = moment(maxDate).startOf('day');
lda.end = moment(maxDate);
}
return {
'Latest Day available': [
lda.begin,
lda.end
],
'Today': [
moment().utc().startOf('day'),
moment().utc().endOf('day')
],
'Yesterday': [
moment().utc().subtract(1, 'days').startOf('day'),
moment().utc().subtract(1, 'days').endOf('day')
],
'This Week': [
moment().utc().startOf('isoWeek'),
moment().utc().endOf('isoWeek')
],
'Last Week': [
moment().utc().subtract(1, 'week').startOf('isoWeek'),
moment().utc().subtract(1, 'week').endOf('isoWeek')
],
'Last 7 Days': [
moment().utc().subtract(7, 'days').add(1, 'seconds'),
moment().utc()
],
'This Month': [
moment().utc().startOf('month'),
moment().utc()
],
'Last Month': [
moment().utc().subtract(1, 'month').startOf('month'),
moment().utc().subtract(1, 'month').endOf('month'),
]
};
}
var dateRangePicker_options = {
"showDropdowns": true,
"timePicker": true,
"timePicker24Hour": true,
"timePickerSeconds": true,
timePickerIncrement: 1,
"autoApply": false,
singleDatePicker: false,
alwaysShowCalendars: true,
"ranges": computeRangeObject(moment().utc().startOf('day'), moment().utc()),
locale: {
format: 'YYYY-MM-DD'
},
enableOnReadonly: false,
isInvalidDate: function(){return true}, // set all dates to invalid to prevent user from selecting something before daterange is fetched (because disabling doesn't work)
"drops": "up",
'opens':"center",
autoUpdateInput: false
};
$('#control-timerange').daterangepicker(dateRangePicker_options);
var onDaterangepickerApply = function(ev, picker) {
// daterangepicker does not have utc support: https://github.com/dangrossman/bootstrap-daterangepicker/issues/343
var startDateOffset = picker.startDate.utcOffset()
var utcStartDate = moment.utc(picker.startDate).utcOffset(-startDateOffset, true).utc()
var endDateOffset = picker.endDate.utcOffset()
var utcEndDate = moment.utc(picker.endDate).utcOffset(-endDateOffset, true).utc()
playbackChangeState(false); // if playback is running, make it stop since the range may have changed
// get daterange in secs
//var startSecsUnix = Math.floor(($('#control-timerange').data('daterangepicker').startDate - startDateOffset) / 1000);
//var endSecsUnix = Math.floor(($('#control-timerange').data('daterangepicker').endDate -endDateOffset) / 1000);
var startSecsUnix = utcStartDate.unix();
var endSecsUnix = utcEndDate.unix();
//a date was picked
/*console.log(utcStartDate.toISOString(true), startSecsUnix);
console.log(utcEndDate.toISOString(true), endSecsUnix);*/
// set slider boundaries
slider.setAttribute('min', startSecsUnix);
slider.setAttribute('max', endSecsUnix);
slider.setValue(startSecsUnix);
slider.enable(); // enable slider because date was chosen
// display initial situation for first second in range
nextTimestep(false, false) // just run refresh of data once, don't iterate, don't loop
.then(function(){
updateSliderTooltipMode(slider, 'show'); // set default mode to show tooltip on hover
$('.control-staticdownload').prop('disabled', false); //allow staticdownload button
}
);
// save selected date to Localstorage
try {
localStorage.setItem("ol_history_selectedrange.from",startSecsUnix);
localStorage.setItem("ol_history_selectedrange.to",endSecsUnix);
} catch (e){}
var selectedLength = moment.duration(utcEndDate.diff(utcStartDate)).as('seconds');
var threshold = 60
var tdiff = selectedLength - threshold
if(tdiff < 0){
rcToast("Attention: The timerange you selected comprises just "+selectedLength+" seconds")
}
};
// returns false if there was no date found in localstorage
var initializeDaterangePicker = function(ev, picker, chainPromise){
console.log("updating available daterange");
timeout(2000, fetch(rcd_globals.history_endpoint_prefix+'timelimits.json', customRequestOptions)
).then(function(data){
return data.json();
}).then(function(json) {
var lsDatePreset = false;
$('#control-timerange').data('daterangepicker').remove();
dateRangePicker_options["minDate"]= moment.unix(json.begin).utc();
dateRangePicker_options["maxDate"]= moment.unix(json.end).utc();
// get saved timerange from localstorage
try {
dateRangePicker_options["startDate"] = moment.unix(parseInt(localStorage.getItem("ol_history_selectedrange.from"))).utc();
dateRangePicker_options["endDate"] = moment.unix(parseInt(localStorage.getItem("ol_history_selectedrange.to"))).utc();
if(dateRangePicker_options["startDate"].isValid() && dateRangePicker_options["endDate"].isValid()){
lsDatePreset = true;
} else {
console.log(localStorage.getItem("ol_history_selectedrange.from"), dateRangePicker_options["startDate"]);
console.log(localStorage.getItem("ol_history_selectedrange.to"), dateRangePicker_options["endDate"]);
}
} catch (e){}
if(!dateRangePicker_options["startDate"]._i || !dateRangePicker_options["endDate"]._i){
console.log("setting default startenddate, localstorage not set");
dateRangePicker_options["startDate"]= moment.unix(json.end).utc().startOf("day");
dateRangePicker_options["endDate"]= moment.unix(json.end).utc().endOf("day");
}
dateRangePicker_options.isInvalidDate=undefined; // enable selecting dates again
dateRangePicker_options.ranges = computeRangeObject(moment.unix(json.begin).utc(), moment.unix(json.end).utc());
$("#control-timerange").daterangepicker(dateRangePicker_options);
if(chainPromise === undefined){ // if undefined, we're in automatic mode; else interactive mode
$('#control-timerange').trigger('click');
}
$("#control-timerange").on('show.daterangepicker', initializeDaterangePicker);
$('#control-timerange').on('apply.daterangepicker', onDaterangepickerApply);
return lsDatePreset;
}).catch(function(err) {
if(!err.message.includes("Failed to fetch")){
console.log("[timelimits] A problem occurred: "+err.message);
rcToast("Couldn't fetch available timerange from API: "+err.message);
$('#control-timerange').data('daterangepicker').hide();
}
return false;
}).then(chainPromise);
};
$('#control-timerange').on('show.daterangepicker', initializeDaterangePicker);
/*
TODOS:
playpause into one and replace pause button with reset button
*/
var iteratingLoopsActive = 0;
/*
increments the current timestep/range displayed and calls refreshData afterwards
incrementing the time and slider is inhibited if 'iterate' is set to false.
looping infinitely is controlled via the loop flag
default is: iterate: true; loop: true
*/
function nextTimestep(iterate, loop){
if(iterate == undefined) iterate = true;
if(loop == undefined) loop = true;
return new Promise(function(resolve, reject){
console.log("iterating: "+iterate+" loop:"+loop);
if((!playbackRunning && iterate) || iteratingLoopsActive > 1) {
console.log("nextTimestep bailout: pbrunning="+playbackRunning+" iter:"+iterate+" loopsactive:"+iteratingLoopsActive);
iteratingLoopsActive -= 1;
resolve(false); // stop playback
return;
}
// number of seconds to include in request (include more than one second so aircrafts are more likely to be in consecutive reqs)
var sampleWindowWidthSecs = 4;
var stepBackInHistory = false;
if(historyTimerangeCurrentlyDisplayed && historyTimerangeCurrentlyDisplayed.from > (slider.getValue()-sampleWindowWidthSecs/2)){
stepBackInHistory = true;
}
historyTimerangeCurrentlyDisplayed = {
from: slider.getValue()-sampleWindowWidthSecs/2,
to: slider.getValue()+sampleWindowWidthSecs/2
};
var begunAtMs, waitingTimeLeft;
// start refresh promise chain
resolve(
new Promise(function(resolve){
begunAtMs = new Date().getTime();
resolve(true);
})
.then(function(){return refreshData(null, {
from: historyTimerangeCurrentlyDisplayed.from,
to: historyTimerangeCurrentlyDisplayed.to
}, stepBackInHistory);})
.then(function(refreshedSuccessfully){
var nowMs = new Date().getTime();
var elapsed = nowMs - begunAtMs;
waitingTimeLeft = refreshIntervalSecs * 1000 - elapsed;
if(waitingTimeLeft < 0){
waitingTimeLeft = 0; // we have no time left, refresh ASAP
}
console.log("waitleft ",waitingTimeLeft);
// if refresh wasn't successful, pass flag to next timestep not to iterate but continue looping!
if(!refreshedSuccessfully){
setTimeout(function(){return nextTimestep(false /*iterate*/, true /*loop*/);}, waitingTimeLeft);
return;
}
// increment slider after choosing the range to fetch!
if (iterate){
var increment = playbackSpeedFactor * refreshIntervalSecs;
// check if next iteration will be out of slider boundary
if(iterate && (slider.getAttribute('max') < slider.getValue()+increment)){
console.log("Terminating playback");
loop = false;
playbackChangeState(false); // make this the last iteration
iteratingLoopsActive -= 1; // since there will be no further iteration, this loop ends
updateSliderTooltipMode(slider, 'show');
}
console.log("slider before increment "+slider.getValue());
slider.setValue(slider.getValue() + increment);
console.log("slider after increment "+slider.getValue());
}
if(loop) setTimeout(function(){return nextTimestep();}, waitingTimeLeft);
}, function(err){
console.log("Something failed when fetching: ", err, ";");
if(err.match(/.*[0-9]{3}.*/)){
console.log("retrying");
setTimeout(function(){return nextTimestep(false /*iterate*/, true /*loop*/);}, waitingTimeLeft);
}
})
);
}); // return promise
}
function updateSliderTooltipMode(slider, mode){
var oldSliderValue = slider.getValue(); // save slider value to restore it after re-rendering
// turn on tooltip display
slider.setAttribute('tooltip', mode);
// make sure tooltip is really rendered
slider.refresh();
slider.relayout();
slider.setValue(oldSliderValue); // restore slider state
// enable slider
slider.enable();
}
$('#control-play').on('click', function(){
if(!playbackRunning){
console.log("play clicked, continuing playback");
updateSliderTooltipMode(slider, 'always');
// start playback
playbackChangeState(true);
iteratingLoopsActive += 1;
nextTimestep();
}
});
$('#control-pause').on('click', function(){
playbackChangeState(false);
updateSliderTooltipMode(slider, 'show'); // show tooltip only on demand
});
function generateTargetUrl(prefix){
var targeturl = prefix;
if(targeturl.slice(-1) != "&" && targeturl.slice(-1) != "?") { targeturl += "&";}
targeturl += $.param( // append parameters to hash-part of url
// serialize filter and timerange parameters (latter from state of timeslider)
$.extend(
//icao,
filtersetToParams(Filters),
ol_getviewportExtent(),
{
'timeframe.from': slider.getAttribute('min'),
'timeframe.to': slider.getAttribute('max'),
'staticmode' : 1,
}
)
) + serializeFilterArrays();
return targeturl;
}
$('#control-staticdownload').on('click', function(){
window.open(generateTargetUrl(window.location.href), '_blank');
});
$('#control-staticdownload-raw').on('click', function(){
var prefix = rcd_globals.history_endpoint_prefix+"staticdata.json?";
console.log(generateTargetUrl(prefix))
window.open(generateTargetUrl(prefix), '_blank');
});
$('#control-persist').on('click', function(){
$(this).toggleClass("active");
persistPaths = $(this).hasClass("active");
try{
localStorage.setItem("ol_persistPaths", persistPaths);
}catch{}
if(getActiveDisplaystyle().drawAllTracks === true){
removeAllPlaneAndTrackFeatures();
} else {
cleanTracksFromOtherSrc(highlightedPlane, null);
}
//FIXME toggle off-> remove flightpaths
});
/* prevent play/pause buttons being activated by the keyboard since this is handled via a custom spacebar handler */
var preventFocusClick = function(event) {
if(event.keyCode == 9) return true; // allow tab-focussing across button
else event.preventDefault();
}
$('#control-play').on('keydown', preventFocusClick);
$('#control-pause').on('keydown', preventFocusClick);
function parseAndSetPlaybackspeed(str, savePermanently){
var parsedPlaybackSpeedFactor = parseInt(str, 10);
if(parsedPlaybackSpeedFactor != NaN && parsedPlaybackSpeedFactor>0){
playbackSpeedFactor = parsedPlaybackSpeedFactor; // only update speedfactor if it is valid globally, else stay with default init
}
// update label of dropdown button
$("#playbackspeed-button").html(playbackSpeedFactor+'x <span class="caret"></span>');
if(savePermanently){
try {
localStorage.setItem("ol_playbackspeed", playbackSpeedFactor);
} catch(e){}
}
}
$("#playbackspeed-dropdown li").click(function () {
parseAndSetPlaybackspeed(
$(this).attr('data-value'),
true // save to LocalStorage
);
});
var playbackWasRunningOnSlideStart = false;
$("#slider").on('slideStart', function(ev, picker){
if(playbackRunning) playbackWasRunningOnSlideStart = true;
else playbackWasRunningOnSlideStart = false;
playbackRunning=false;
console.log("pausing playback");
});
$("#slider").on('slideStop', function(ev, picker){
if(playbackWasRunningOnSlideStart){
playbackChangeState(true);
iteratingLoopsActive += 1;
nextTimestep();
console.log("continuing playback");
} else {
nextTimestep(false, false); // just run refresh of data once, don't iterate, don't loop
}
});
// bind spacebar to pause, resume
$(':root').on('keyup', function(event){
if(!event) var event = window.event; // cross-browser shenanigans
if(event.keyCode === 32) { // this is the spacebar
if(playbackRunning){
// pause
console.log("pausing playback");
playbackChangeState(false);
updateSliderTooltipMode(slider, 'show'); // show tooltip only on demand
} else {
console.log("continuing playback");
// resume
if($('#control-play').prop('disabled')){
return; // no daterange loaded, won't start
}
updateSliderTooltipMode(slider, 'always');
playbackChangeState(true);
iteratingLoopsActive += 1;
nextTimestep();
console.log("continuing playback");
}
}
return true; // treat all other keys normally;
});
// try to restore last entered daterange and jump to begin
initializeDaterangePicker(undefined, undefined,
function(dateRangePreset){
if(dateRangePreset === true){
onDaterangepickerApply(undefined, $('#control-timerange').data('daterangepicker'));
}
}
);
}
function zoomToHashExtent(map){
var hash = {};
location.hash.replace('#', '').split('&').forEach(function(item){
hash[item.split('=')[0]] = item.split('=')[1];
});
if(!(hash.vpn && hash.vpe && hash.vps && hash.vpw)){
return;
}
var rcExtent = { // extent definition, radarcape style
nelat: parseFloat(hash.vpn),
nelon: parseFloat(hash.vpe),
swlat: parseFloat(hash.vps),
swlon: parseFloat(hash.vpw)
}
wrapLon = function(value){
var worlds = Math.floor((value + 180) / 360);
return value - (worlds * 360);
}
var olExtent = [
wrapLon(rcExtent.swlon), // minx
rcExtent.swlat, //miny
wrapLon(rcExtent.nelon), // maxx
rcExtent.nelat // maxy
];
historyTimerangeCurrentlyDisplayed = {
from: parseInt(hash["timeframe.from"]),
to: parseInt(hash["timeframe.to"])
};
if(JSON.stringify(historyTimerangeCurrentlyDisplayed) == localStorage.getItem("ol-lastStaticTimerangeDisplayed")){
// the user is viewing the same timerange as before, so we may restore their center and zoomlevel
// (this is already done in default behaviour!)
return;
} else {
// user views another timerange, update last timerange displayed
localStorage.setItem("ol-lastStaticTimerangeDisplayed", JSON.stringify(historyTimerangeCurrentlyDisplayed));
}
console.log("zooming to hash extent "+olExtent);
var extent = ol.proj.transformExtent(olExtent, 'EPSG:4326', 'EPSG:3857');
map.getView().fit(extent, {duration: 3000, constrainResolution: false});
}