diff --git a/docs/config.rst b/docs/config.rst index 09eb6a0..9f79560 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -216,6 +216,8 @@ the form ``key = value``. Two items take a different form:, ``worlds`` and **You must specify at least one render** +.. _outputdir: + ``outputdir = ""`` This is the path to the output directory where the rendered tiles will be saved. @@ -250,11 +252,61 @@ the form ``key = value``. Two items take a different form:, ``worlds`` and observer to be used. The observer object is expected to have at least ``start``, ``add``, ``update``, and ``finish`` methods. - e.g.:: + If you want to specify an observer manually, try something like: + :: + from observer import ProgressBarObserver() observer = ProgressBarObserver() -.. _outputdir: + There are currently three observers available: ``LoggingObserver``, + ``ProgressBarObserver`` and ``JSObserver``. + + ``LoggingObserver`` + This gives the normal/older style output and is the default when output + is redirected to a file or when running on Windows + + ``ProgressBarObserver`` + This is used by default when the output is a terminal. Displays a text based + progress bar and some statistics. + + ``JSObserver(outputdir[, minrefresh][, messages])`` + This will display render progress on the output map in the bottom right + corner of the screen. ``JSObserver``. + + * ``outputdir="`` + Progress information won't be written to file or requested by your + web browser more frequently than this interval. + + * ``messages=dict(totalTiles=, renderCompleted=, renderProgress=)`` + Customises messages displayed in browser. All three messages must be + defined as follows: + + * ``totalTiles="Rendering %d tiles"`` + The ``%d`` format string will be replaced with the total number of + tiles to be rendered. + + * ``renderCompleted="Render completed in %02d:%02d:%02d"`` + The three format strings will be replaced with the number of hours. + minutes and seconds taken to complete this render. + + * ``renderProgress="Rendered %d of %d tiles (%d%%)"`` + The three format strings will be replaced with the number of tiles + completed, the total number of tiles and the percentage complete + + Format strings are explained here: http://docs.python.org/library/stdtypes.html#string-formatting + All format strings must be present in your custom messages. + + :: + + from observer import JSObserver + observer = JSObserver(outputdir, 10) + .. _renderdict: diff --git a/overviewer_core/data/js_src/util.js b/overviewer_core/data/js_src/util.js index 81cc5c0..96e3965 100644 --- a/overviewer_core/data/js_src/util.js +++ b/overviewer_core/data/js_src/util.js @@ -58,6 +58,10 @@ overviewer.util = { var coordsdiv = new overviewer.views.CoordboxView({tagName: 'DIV'}); coordsdiv.render(); + var progressdiv = new overviewer.views.ProgressView({tagName: 'DIV'}); + progressdiv.render(); + progressdiv.updateProgress(); + if (overviewer.collections.haveSigns) { var signs = new overviewer.views.SignControlView(); signs.registerEvents(signs); diff --git a/overviewer_core/data/js_src/views.js b/overviewer_core/data/js_src/views.js index 179d0b0..4a0e48d 100644 --- a/overviewer_core/data/js_src/views.js +++ b/overviewer_core/data/js_src/views.js @@ -137,7 +137,34 @@ overviewer.views.CoordboxView = Backbone.View.extend({ } }); - +overviewer.views.ProgressView = Backbone.View.extend({ + initialize: function() { + this.el.id = 'progressDiv'; + this.el.innerHTML = 'Current Render Progress'; + overviewer.map.controls[google.maps.ControlPosition.BOTTOM_RIGHT].push(this.el); + this.el.hidden = true; + $.ajaxSetup({cache: false}); + }, + updateProgress: function() { + e = this; + $.getJSON('progress.json', null, function(d){ + if (!(d == null||d=='')) { + e.el.hidden = false; + e.el.innerHTML = d['message']; + if (d.update > 0) { + setTimeout("e.updateProgress()", d.update); + } else { + setTimeout("e.updateProgress()", 60000); + e.el.innerHTML="Hidden - d.update < 0"; + e.el.hidden = true; + } + } else { + e.el.innerHTML="Hidden - !!d==false"; + e.el.hidden = true; + } + }); + } +}); /* GoogleMapView is responsible for dealing with the GoogleMaps API to create the */ diff --git a/overviewer_core/data/web_assets/overviewer.css b/overviewer_core/data/web_assets/overviewer.css index 831a138..fff3458 100644 --- a/overviewer_core/data/web_assets/overviewer.css +++ b/overviewer_core/data/web_assets/overviewer.css @@ -97,7 +97,7 @@ body { } -#link, #coordsDiv { +#link, #coordsDiv, #progressDiv { background-color: #fff; /* fallback */ background-color: rgba(255,255,255,0.55); border: 1px solid rgb(0, 0, 0); diff --git a/overviewer_core/observer.py b/overviewer_core/observer.py index c58a0d9..b409fd4 100644 --- a/overviewer_core/observer.py +++ b/overviewer_core/observer.py @@ -17,6 +17,8 @@ import time import logging import progressbar import sys +import os +import json class Observer(object): """Base class that defines the observer interface. @@ -164,3 +166,183 @@ class ProgressBarObserver(progressbar.ProgressBar, Observer): def _need_update(self): return self.get_current_value() - self.last_update > self.UPDATE_INTERVAL + +class JSObserver(Observer): + """Display progress on index.html using JavaScript + """ + + def __init__(self, outputdir, minrefresh=5, messages=False): + """Initialise observer + outputdir must be set to the map output directory path + minrefresh specifies the minimum gap between requests, in seconds [optional] + messages is a dictionary which allows the displayed messages to be customised [optional] + """ + self.last_update = -11 + self.last_update_time = -1 + self._current_value = -1 + self.minrefresh = 1000*minrefresh + self.json = dict() + + if (messages == False): + self.messages=dict(totalTiles="Rendering %d tiles", renderCompleted="Render completed in %02d:%02d:%02d", renderProgress="Rendered %d of %d tiles (%d%%)") + elif (isinstance(messages, dict)): + if ('totalTiles' in messages and 'renderCompleted' in messages and 'renderProgress' in messages): + self.messages = messages + else: + raise Exception("JSObserver: messages parameter must be a dictionary with three entries: totalTiles, renderCompleted and renderProgress") + else: + raise Exception("JSObserver: messages parameter must be a dictionary with three entries: totalTiles, renderCompleted and renderProgress") + if not os.path.exists(outputdir): + raise Exception("JSObserver: Output directory specified (%s) doesn't appear to exist. This should be the same as the Overviewer output directory") + + self.logfile = open(os.path.join(outputdir, "progress.json"), "w+", 0) + self.json["message"]="" + self.json["update"]=self.minrefresh + self.json["messageTime"]=time.time() + json.dump(self.json, self.logfile) + self.logfile.flush() + + def start(self, max_value): + self.logfile.seek(0) + self.logfile.truncate() + self.json["message"] = self.messages["totalTiles"] % (max_value) + self.json["update"] = self.minrefresh + self.json["messageTime"] = time.time() + json.dump(self.json, self.logfile) + self.logfile.flush() + self.start_time=time.time() + self._set_max_value(max_value) + + def is_started(self): + return self.start_time is not None + + def finish(self): + """Signals the end of the processes, should be called after the + process is done. + """ + self.end_time = time.time() + duration = self.end_time - self.start_time + self.logfile.seek(0) + self.logfile.truncate() + hours = duration // 3600 + duration = duration % 3600 + minutes = duration // 60 + seconds = duration % 60 + self.json["message"] = self.messages["renderCompleted"] % (hours, minutes, seconds) + self.json["update"] = -1 # Initially this was set to False, but that runs into some JS strangeness. -1 is less nice, but works + self.json["messageTime"] = time.time() + json.dump(self.json, self.logfile) + self.logfile.close() + + def is_finished(self): + return self.end_time is not None + + def is_running(self): + return self.is_started() and not self.is_finished() + + def add(self, amount): + """Shortcut to update by increments instead of absolute values. Zero + amounts are ignored. + """ + if amount: + self.update(self.get_current_value() + amount) + + def update(self, current_value): + """Set the progress value. Should be between 0 and max_value. Returns + whether this update is actually displayed. + """ + self._current_value = current_value + if self._need_update(): + refresh = max(1500*(time.time() - self.last_update_time), self.minrefresh) // 1 + self.logfile.seek(0) + self.logfile.truncate() + self.json["message"] = self.messages["renderProgress"] % (self.get_current_value(), self.get_max_value(), self.get_percentage()) + self.json["update"] = refresh + self.json["messageTime"] = time.time() + json.dump(self.json, self.logfile) + self.logfile.flush() + self.last_update_time = time.time() + self.last_update = current_value + return True + return False + + def get_percentage(self): + """Get the current progress percentage. Assumes 100% if max_value is 0 + """ + if self.get_max_value() is 0: + return 100.0 + else: + return self.get_current_value() * 100.0 / self.get_max_value() + + def get_current_value(self): + return self._current_value + + def get_max_value(self): + return self._max_value + + def _set_max_value(self, max_value): + self._max_value = max_value + + def _need_update(self): + cur_val = self.get_current_value() + if cur_val < 100: + return cur_val - self.last_update > 10 + elif cur_val < 500: + return cur_val - self.last_update > 50 + else: + return cur_val - self.last_update > 100 + +class MultiplexingObserver(Observer): + """Combine multiple observers into one. + """ + def __init__(self, *components): + self.components = components + super(MultiplexingObserver, self).__init__() + + def start(self, max_value): + for o in self.components: + o.start(max_value) + super(MultiplexingObserver, self).start(max_value) + + def finish(self): + for o in self.components: + o.finish() + super(MultiplexingObserver, self).finish() + + def update(self, current_value): + for o in self.components: + o.update(current_value) + super(MultiplexingObserver, self).update(current_value) + +class ServerAnnounceObserver(Observer): + """Send the output to a Minecraft server via FIFO or stdin""" + def __init__(self, target='/dev/null', pct_interval=10): + self.pct_interval = pct_interval + self.target_handle = open(target, 'w') + self.last_update = 0 + super(ServerAnnounceObserver, self).__init__() + + def start(self, max_value): + self._send_output('Starting render of %d total tiles' % max_value) + super(ServerAnnounceObserver, self).start(max_value) + + def finish(self): + self._send_output('Render complete!') + super(ServerAnnounceObserver, self).finish() + self.target_handle.close() + + def update(self, current_value): + super(ServerAnnounceObserver, self).update(current_value) + if self._need_update(current_value): + self._send_output('Rendered %d of %d tiles, %d%% complete' % + (self.get_current_value(), self.get_max_value(), + self.get_percentage())) + + def _need_update(self): + return self.get_percentage() - \ + (self.last_update * 100.0 / self.get_max_value()) >= self.pct_interval + + def _send_output(self, output): + self.target_handle.write('say %s\n' % output) + self.target_handle.flush() + diff --git a/overviewer_core/settingsDefinition.py b/overviewer_core/settingsDefinition.py index 8ffe738..c73c67d 100644 --- a/overviewer_core/settingsDefinition.py +++ b/overviewer_core/settingsDefinition.py @@ -45,7 +45,7 @@ from settingsValidators import * import util -from observer import ProgressBarObserver, LoggingObserver +from observer import ProgressBarObserver, LoggingObserver, JSObserver import platform import sys