diff options
author | Kristian Lyngstol <kristian@bohemians.org> | 2016-05-28 16:42:32 +0200 |
---|---|---|
committer | Kristian Lyngstol <kristian@bohemians.org> | 2016-05-28 16:42:32 +0200 |
commit | e5323ecaf854924cdc6226a5db716d35ae347e9b (patch) | |
tree | 91962661cd90886a8ffd7cd8b1debf50c8e77042 | |
parent | 6fbb4fcf563a4cbdd11932e415bd50d7d6279e3b (diff) |
Fix numerous time-travel issues in front and API
Fixes #69 #11 #5
Introduces nmsTime which unifies the time travel code a bit. It still needs
some work, but this is much better.
All conversion is now done by native JavaScript methods, freeing us from
the hell that is parsing it ourself.
One thing should be added: The backend should discard any now=values that
are not 5-minute intervals. We don't want to kill the cache and the
database by extension.
Still need to re-implement the "replay event" shorthand, but that ties in
to #54
-rwxr-xr-x | include/nms/web.pm | 2 | ||||
-rw-r--r-- | web/index.html | 7 | ||||
-rw-r--r-- | web/js/nms-data.js | 3 | ||||
-rw-r--r-- | web/js/nms-map.js | 14 | ||||
-rw-r--r-- | web/js/nms-time.js | 150 | ||||
-rw-r--r-- | web/js/nms.js | 273 |
6 files changed, 198 insertions, 251 deletions
diff --git a/include/nms/web.pm b/include/nms/web.pm index 8a20f50..18fe919 100755 --- a/include/nms/web.pm +++ b/include/nms/web.pm @@ -67,7 +67,7 @@ sub setwhen { $offset = $_[1]; } if (defined($get_params{'now'})) { - $now = db_safe_quote('now') . "::timestamp with time zone "; + $now = "timestamp with time zone 'epoch' + " . db_safe_quote('now') . " * INTERVAL '1 second' "; $cc{'max-age'} = "3600"; } $now = "(" . $now . " - '" . $offset . "'::interval)"; diff --git a/web/index.html b/web/index.html index 1bb9f91..593452b 100644 --- a/web/index.html +++ b/web/index.html @@ -56,8 +56,8 @@ <li><a href="#disco" onclick="setUpdater(handler_disco)">DISCO</a></li> <li class="divider"> </li> <li class="dropdown-header">Time</li> - <li><a href="#" onclick="toggleLayer('nowPickerBox');startNowPicker();">Travel in time</a></li> - <li><a href="#" onclick="nms.playback.startReplay('2016-03-21T09:00:00','2016-03-27T12:00:00');" title="Replay from opening 120 minutes per second">Replay TG</a></li> + <li><a href="#" onclick="toggleLayer('nowPickerBox');nmsTime.startNowPicker();">Travel in time</a></li> + <li><a href="#" onclick="nmsTime.replayEvent();" title="Replay from opening 120 minutes per second">Replay event</a></li> <li class="divider"> </li> <li class="dropdown-header">View</li> <li><a href="#" onclick="toggleNightMode()">Toggle Night Mode</a></li> @@ -235,7 +235,7 @@ <div class="form-group"> <input type="text" class="form-control" placeholder="YYYY-MM-DDThh:mm:ss" id="nowPicker"> <div class="button-group"> - <button class="btn btn-primary" onclick="nms.playback.setNow(document.getElementById('nowPicker').dataset.iso);hideLayer('nowPickerBox');">Travel</button> + <button class="btn btn-primary" onclick="nmsTime.setNow(document.getElementById('nowPicker').dataset.iso);hideLayer('nowPickerBox');nmsTime.updateData();">Travel</button> <button class="btn btn-danger" onclick="startNowPicker(Date.now());nms.playback.setNow(false);nms.playback.play();">Back to reality</button> <button class="btn btn-info" data-toggle="button" onclick="toggleLayer('nowPickerInfo');">Info</button> </div> @@ -301,6 +301,7 @@ <script type="text/javascript" src="js/nms-admin-pane.js"></script> <script type="text/javascript" src="js/nms-oplog.js"></script> <script type="text/javascript" src="js/nms-search.js"></script> + <script type="text/javascript" src="js/nms-time.js"></script> <script src="js/jquery.datetimepicker.full.js" type="text/javascript"></script> <script type="text/javascript"> initNMS(); diff --git a/web/js/nms-data.js b/web/js/nms-data.js index 14e5fed..e3c4106 100644 --- a/web/js/nms-data.js +++ b/web/js/nms-data.js @@ -231,7 +231,8 @@ nmsData._genericUpdater = function(name, cacheok) { dataType: "json", success: function (data, textStatus, jqXHR) { if (nmsData[name] == undefined || nmsData[name]['hash'] != data['hash']) { - nmsData._last = data['time']; + if (name == "ping") + nmsData._last = data['time']; nmsData.old[name] = nmsData[name]; nmsData[name] = data; nmsMap.drawNow(); diff --git a/web/js/nms-map.js b/web/js/nms-map.js index ef86102..26782f8 100644 --- a/web/js/nms-map.js +++ b/web/js/nms-map.js @@ -180,6 +180,19 @@ nmsMap._resizeEvent = function() { nmsMap.drawNow = function () { var now = nmsData.now; + var ctx = nmsMap._c.top.ctx; + if (nmsTime.isRealTime()) { + if (!nmsMap._nowCleared) { + ctx.save(); + ctx.scale(this.scale, this.scale); + ctx.clearRect(0,0,800,100); + ctx.restore(); + nmsMap._nowCleared = true; + nmsMap._lastNow = undefined; + } + return true; + } + nmsMap._nowCleared = false; if(String(now).indexOf('T') == -1) { //If now does not contain 'T' we assume its in epoch format now = new Date(nmsData.now * 1000); } else { @@ -192,7 +205,6 @@ nmsMap.drawNow = function () } nmsMap.stats.nows++; - var ctx = nmsMap._c.top.ctx; ctx.save(); ctx.scale(this.scale, this.scale); ctx.font = (2 * this._settings.fontSize) + "px " + this._settings.fontFace; diff --git a/web/js/nms-time.js b/web/js/nms-time.js new file mode 100644 index 0000000..e857956 --- /dev/null +++ b/web/js/nms-time.js @@ -0,0 +1,150 @@ +"use strict"; + +/* + * Deals with controlling time. + * + * More specifically: replaying of past events, fast forwarding, pausing, + * etc. + * + * The interface is a bit bloated at the moment, though. + */ +var nmsTime = nmsTime || { + _now: undefined, + _handle: undefined +} + +nmsTime.replayEvent = function() { + throw "Not yet implemented."; +} + +nmsTime.isRealTime = function() { + if (nmsTime._now == undefined && nmsTime._handle == undefined) + return true; + return false; +} + +nmsTime.startNowPicker = function () { + $.datetimepicker.setLocale('no'); + $('#nowPicker').datetimepicker('destroy'); + var now; + if (nmsTime._now == undefined) + now = new Date(); + else + now = nmsTime._now; + now.setSeconds(0); + now.setMilliseconds(0); + var datepicker = $('#nowPicker').datetimepicker({ + value: now, + step: 5, + mask:false, + inline:true, + todayButton: true, + validateOnBlur:false, + dayOfWeekStart:1, + maxDate:'+1970/01/01', + onSelectDate: function(ct,$i){ + document.getElementById('nowPicker').dataset.iso = new Date(ct.valueOf()); + }, + onSelectTime: function(ct,$i){ + document.getElementById('nowPicker').dataset.iso = new Date(ct.valueOf()); + }, + onGenerate: function(ct,$i){ + document.getElementById('nowPicker').dataset.iso = new Date(ct.valueOf()); + } + }); +} + +nmsTime.setNow = function(now) { + var newDate = new Date(now); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + newDate.setMinutes(newDate.getMinutes() - newDate.getMinutes()%5); + nmsTime._now = newDate; +} + +nmsTime._updateData = function() { + nmsData.now = nmsTime._now.getTime() / 1000; +} + +nmsTime.realTime = function() { + nmsTime.stopPlayback(); + nmsTime._now = undefined; + nmsData.now = undefined; +} + +/* + * Step a fixed amount of time, measured in minutes. + * + * Try to align this to whole 5 minutes. It will be enforced in future + * backend versions to avoid bloating the cache and thus also stressing the + * database + */ +nmsTime.step = function(amount) { + if (nmsTime._now == null) + throw "Stepping without nmsTime._now"; + if (amount == 0 || amount == undefined) + throw "Invalid step"; + if (nmsTime._now.getTime() + (amount * 1000 * 60 ) > Date.now()) { + nmsTime.realTime(); + return; + } + nmsTime._now.setMinutes(nmsTime._now.getMinutes() + amount); + nmsTime._updateData(); +} + +/* + * Step based on key-press. Same as step() but stops playback if it's + * active and allows you to rewind from a "live" map. + */ +nmsTime.stepKey = function(amount) { + nmsTime.stopPlayback(); + if (nmsTime._now == undefined) { + nmsTime.setNow(Date.now()); + } + nmsTime.step(amount); +} + +/* + * Target of setInterval() when replaying. + */ +nmsTime._tick = function() { + nmsTime.step(nmsTime._speed); +} + +/* + * We now have a time (presumably), start playback. + * + * Aborts if the time provided is greater than real time. + * + * Gondul does not _yet_ support fast forwarding into the future. + */ +nmsTime.startPlayback = function(speed) { + if (nmsTime._handle) + nmsTime.stopPlayback(); + if (nmsTime._now.getTime() > Date.now()) { + nmsTime.stopPlayback(); + return; + } + nmsTime._speed = speed; + nmsTime._handle = setInterval(nmsTime._tick,1000); +} + +nmsTime.togglePause = function() { + if (nmsTime._handle) { + nmsTime.stopPlayback(); + } else { + if (nmsTime.isRealTime()) { + nmsTime.setNow(Date.now()); + nmsTime._updateData(); + } else { + nmsTime.startPlayback(nmsTime._speed ? nmsTime._speed : 5); + } + } +} + +nmsTime.stopPlayback = function() { + if (nmsTime._handle) + clearInterval(nmsTime._handle); + nmsTime._handle = undefined; +} + diff --git a/web/js/nms.js b/web/js/nms.js index eb0942a..9727c09 100644 --- a/web/js/nms.js +++ b/web/js/nms.js @@ -11,8 +11,6 @@ * - Move all pure UI stuff into nmsUi: nightMode, vertical mode, * menushowing, * - Get rid of "tvmode". As in: complete the merge - * - Move all time-travel related code out into a separate entity. - * - Remove nms.now: it belongs in nmsData. * - nms.timers probably also deserves to die. It used to do a lot more, * now it's just leftovers. */ @@ -21,24 +19,16 @@ var nms = { get nightMode() { return this._nightMode; }, set nightMode(val) { if (val != this._nightMode) { this._nightMode = val; setNightMode(val); } }, /* - * FIXME: This should be slightly smarter. - */ - _now: false, - get now() { return this._now }, - set now(v) { this._now = v; nmsData.now = v; }, - /* * Various setInterval() handlers. See nmsTimer() for how they are * used. * * FIXME: Should just stop using these. */ timers: { - playback:false, tvmode: false }, menuShowing:true, - _startTime:0, get uptime() { return (Date.now() - this._startTime)/1000; }, @@ -94,16 +84,6 @@ var nms = { 'Escape':hideWindow, '?':toggleHelp }, - /* - * Playback controllers and variables - */ - playback:{ - startTime: false, - stopTime: false, - playing: false, - replayTime: 0, - replayIncrement: 60 * 60 - }, tvmode: { handlers: [], currentIndex: 0, @@ -116,8 +96,8 @@ var nms = { /* * Returns a handler object. * - * This might seem a bit much for 'setInterval()' etc, but it's really more - * about self-documentation and predictable ways of configuring timers. + * FIXME: This is legacy-stuff, should get rid of it. DO NOT use this for + * new code. */ function nmsTimer(handler, interval, name, description) { this.handler = handler; @@ -174,193 +154,8 @@ function toggleNightMode() } /* - * Parse 'now' from user-input. - * - * Should probably just use stringToEpoch() instead, but alas, not yet. - */ -function parseNow(now) -{ - if (Date.parse(now)) { - // Adjust for timezone when converting from epoch (UTC) to string (local) - var d = new Date(now); - var timezoneOffset = d.getTimezoneOffset() * -60000; - var d = new Date(Date.parse(now) - timezoneOffset); - var str = d.getFullYear() + "-" + ("00" + (parseInt(d.getMonth())+1)).slice(-2) + "-" + ("00" + d.getDate()).slice(-2) + "T"; - str += ("00" + d.getHours()).slice(-2) + ":" + ("00" + d.getMinutes()).slice(-2) + ":" + ("00" + d.getSeconds()).slice(-2); - return str; - - } - if (now == "") - return ""; - return false; -} - -/* - * Convert back and forth between epoch. - * - * There's no particular reason why I use seconds instead of javascript - * microseconds, except to leave the mark of a C coder on this javascript - * project. - */ -function stringToEpoch(t) -{ - var foo = t.toString(); -// foo = foo.replace('T',' '); - var ret = new Date(Date.parse(foo)); - return parseInt(parseInt(ret.valueOf()) / 1000); -} - -/* - * Have to pad with zeroes to avoid "17:5:0" instead of the conventional - * and more readable "17:05:00". I'm sure there's a better way, but this - * works just fine. - */ -function epochToString(t) -{ - // Adjust for timezone when converting from epoch (UTC) to string (local) - var date = new Date(parseInt(t) * parseInt(1000)); - var timezoneOffset = date.getTimezoneOffset() * -60; - t = t - timezoneOffset; - - date = new Date(parseInt(t) * parseInt(1000)); - var str = date.getFullYear() + "-"; - if (parseInt(date.getMonth()) < 9) - str += "0"; - str += (parseInt(date.getMonth())+1) + "-"; - if (date.getDate() < 10) - str += "0"; - str += date.getDate() + "T"; - if (date.getHours() < 10) - str += "0"; - str += date.getHours() + ":"; - if (date.getMinutes() < 10) - str += "0"; - str += date.getMinutes() + ":"; - if (date.getSeconds() < 10) - str += "0"; - str += date.getSeconds(); - - return str; -} - -function localEpochToString(t) { - var d = new Date(parseInt(t) * parseInt(1000)); - var timezoneOffset = d.getTimezoneOffset() * -60; - t = t + timezoneOffset; - - return epochToString(t); -} - -/* - * Start replaying historical data. - */ -nms.playback.startReplay = function(startTime,stopTime) { - if(!startTime || !stopTime) - return false; - - nms.playback.pause(); - nms.playback.startTime = stringToEpoch(startTime); - nms.playback.stopTime = stringToEpoch(stopTime); - nms.now = epochToString(nms.playback.startTime); - nms.playback.play(); -}; - -/* - * Pause playback - */ -nms.playback.pause = function() { - nms.timers.playback.stop(); - nms.playback.playing = false; -}; - -/* - * Start playback - */ -nms.playback.play = function() { - nms.playback.tick(); - nms.timers.playback.start(); - nms.playback.playing = true; -}; - -/* - * Toggle playback - */ -nms.playback.toggle = function() { - if(nms.playback.playing) { - nms.playback.pause(); - } else { - nms.playback.play(); - } -}; - -/* - * Jump to place in time - */ -nms.playback.setNow = function(now) { - nms.now = parseNow(now); - - nms.playback.stopTime = false; - nms.playback.startTime = false; - nms.playback.tick(); -}; - -/* - * Step forwards or backwards in timer - */ -nms.playback.stepTime = function(n) -{ - var now = getNowEpoch(); - var newtime = parseInt(now) + parseInt(n); - nms.now = epochToString(parseInt(newtime)); - - if(!nms.playback.playing) - nms.playback.tick(); -}; - -/* - * Ticker to trigger updates, and advance time if replaying - * - * This is run on a timer (nms.timers.tick) every second while unpaused - */ -nms.playback.tick = function() -{ - nms.playback.replayTime = getNowEpoch(); - - // If outside start-/stopTime, remove limits and pause playback - if (nms.playback.stopTime && (nms.playback.replayTime >= nms.playback.stopTime || nms.playback.replayTime < nms.playback.startTime)) { - nms.playback.stopTime = false; - nms.playback.startTime = false; - nms.playback.pause(); - return; - } - - // If past actual datetime, go live - if (nms.playback.replayTime > parseInt(Date.now() / 1000)) { - nms.now = false; - } - - // If we are still replaying, advance time - if(nms.now !== false && nms.playback.playing) { - nms.playback.stepTime(nms.playback.replayIncrement); - } -}; - -/* - * Helper function for safely getting a valid now-epoch - */ -function getNowEpoch() { - if (nms.now && nms.now != 0) - return stringToEpoch(nms.now); - else - return parseInt(Date.now() / 1000); -} - -/* * There are 4 legend-bars. This is a helper-function to set the color and * description/name for each one. Used from handler init-functions. - * - * FIXME: Should be smarter, possibly use a canvas-writer so we can get - * proper text (e.g.: not black text on dark blue). */ function setLegend(x,color,name) { @@ -379,7 +174,10 @@ function setLegend(x,color,name) * Start TV-mode * * Loops trough a list of views/updaters at a set interval. - * Arguments: array of views, interval in seconds, use nightmode, hide menus + * Arguments: array of views, interval in seconds + * + * FIXME: this is getting gradually stripped down from the original, so far + * we're not quite there yet with merging it with the regular code paths. */ nms.tvmode.start = function(views,interval) { nms.tvmode.handlers = []; @@ -413,6 +211,11 @@ nms.tvmode.stop = function() { } } +/* + * Used when changing handler to ensure that the new handler is listed in + * the anchor. The anchor can contain a comma-separated list of views and + * we only overwrite it if the new view isn't present. + */ function ensureAnchorHas(view) { try { var views = document.location.hash.slice(1); @@ -424,8 +227,12 @@ function ensureAnchorHas(view) { document.location.hash = view; return false; } + /* * Change map handler (e.g., change from uplink map to ping map) + * + * stopTv esnures that we don't conflict with the tvmode thing. If a + * user-initiated map is selected, tvmode is disabled. */ function setUpdater(fo, stopTv = true) { @@ -457,6 +264,12 @@ function toggleLayer(layer) { l.style.display = 'none'; } +function hideLayer(layer) { + var l = document.getElementById(layer); + l.style.display = 'none'; +} + + function toggleConnect() { toggleLayer("linkCanvas"); } @@ -550,7 +363,7 @@ function getInitialConfig() { * yet. */ function initNMS() { - nms.timers.playback = new nmsTimer(nms.playback.tick, 1000, "Playback ticker", "Handler used to advance time"); + // Only used for dev-purposes now. Accessible through nms.uptime. nms._startTime = Date.now(); // Public @@ -583,7 +396,6 @@ function initNMS() { restoreSettings(); nmsMap.init(); detectHandler(); - nms.playback.play(); setupKeyhandler(); nmsSearch.init(); } @@ -675,23 +487,22 @@ function moveTimeFromKey(e,key) { switch(key) { case 'h': - nms.playback.stepTime(-3600); + nmsTime.stepKey(-60); break; case 'j': - nms.playback.stepTime(-300); + nmsTime.stepKey(-5); break; case 'k': - nms.playback.stepTime(300); + nmsTime.stepKey(5); break; case 'l': - nms.playback.stepTime(3600); + nmsTime.stepKey(60); break; case 'p': - nms.playback.toggle(); + nmsTime.togglePause(); break; case 'r': - nms.playback.setNow(); - nms.playback.play(); + nmsTime.realTime(); break; } return true; @@ -795,34 +606,6 @@ function restoreSettings() } /* - * Time travel gui - */ -function startNowPicker(now) { - $.datetimepicker.setLocale('no'); - $('#nowPicker').datetimepicker('destroy'); - if(!now && nms.now) - now = nms.now; - var datepicker = $('#nowPicker').datetimepicker({ - value: now, - mask:false, - inline:true, - todayButton: false, - validateOnBlur:false, - dayOfWeekStart:1, - maxDate:'+1970/01/01', - onSelectDate: function(ct,$i){ - document.getElementById('nowPicker').dataset.iso = localEpochToString(ct.valueOf()/1000); - }, - onSelectTime: function(ct,$i){ - document.getElementById('nowPicker').dataset.iso = localEpochToString(ct.valueOf()/1000); - }, - onGenerate: function(ct,$i){ - document.getElementById('nowPicker').dataset.iso = localEpochToString(ct.valueOf()/1000); - } - }); -} - -/* * Test if the entire path specified in the arrary "ar" exists under the * specified root. * |