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