Merge branch 'progress-observer' of git://github.com/aheadley/Minecraft-Overviewer into progress-observer
This commit is contained in:
@@ -181,13 +181,28 @@ the form ``key = value``. Two items take a different form:, ``worlds`` and
|
|||||||
This specifies the number of worker processes to spawn on the local machine
|
This specifies the number of worker processes to spawn on the local machine
|
||||||
to do work. It defaults to the number of CPU cores you have, if not
|
to do work. It defaults to the number of CPU cores you have, if not
|
||||||
specified.
|
specified.
|
||||||
|
|
||||||
This can also be specified with :option:`--processes <-p>`
|
This can also be specified with :option:`--processes <-p>`
|
||||||
|
|
||||||
e.g.::
|
e.g.::
|
||||||
|
|
||||||
processes = 2
|
processes = 2
|
||||||
|
|
||||||
|
.. _observer:
|
||||||
|
|
||||||
|
``observer = <observer object>``
|
||||||
|
This lets you configure how the progress of the render is reported. The
|
||||||
|
default is to display a progress bar, unless run on Windows or with stderr
|
||||||
|
redirected to a file. The default value will probably be fine for most
|
||||||
|
people, but advanced users may want to make their own progress reporter (for
|
||||||
|
a web service or something like that) or you may want to force a particular
|
||||||
|
observer to be used. The observer object is expected to have at least ``start``,
|
||||||
|
``add``, ``update``, and ``finish`` methods.
|
||||||
|
|
||||||
|
e.g.::
|
||||||
|
|
||||||
|
observer = ProgressBarObserver()
|
||||||
|
|
||||||
.. _outputdir:
|
.. _outputdir:
|
||||||
|
|
||||||
|
|
||||||
@@ -253,7 +268,7 @@ values. The valid configuration keys are listed below.
|
|||||||
This is which rendermode to use for this render. There are many rendermodes
|
This is which rendermode to use for this render. There are many rendermodes
|
||||||
to choose from. This can either be a rendermode object, or a string, in
|
to choose from. This can either be a rendermode object, or a string, in
|
||||||
which case the rendermode object by that name is used.
|
which case the rendermode object by that name is used.
|
||||||
|
|
||||||
e.g.::
|
e.g.::
|
||||||
|
|
||||||
"rendermode": "normal",
|
"rendermode": "normal",
|
||||||
@@ -291,7 +306,7 @@ values. The valid configuration keys are listed below.
|
|||||||
Selecting this rendermode doesn't automatically render your nether
|
Selecting this rendermode doesn't automatically render your nether
|
||||||
dimension. Be sure to also set the
|
dimension. Be sure to also set the
|
||||||
:ref:`dimension<option_dimension>` option to 'nether'.
|
:ref:`dimension<option_dimension>` option to 'nether'.
|
||||||
|
|
||||||
``"nether_lighting"``
|
``"nether_lighting"``
|
||||||
Similar to "nether" but with blocky lighting.
|
Similar to "nether" but with blocky lighting.
|
||||||
|
|
||||||
@@ -302,9 +317,9 @@ values. The valid configuration keys are listed below.
|
|||||||
A cave render with depth tinting (blocks are tinted with a color
|
A cave render with depth tinting (blocks are tinted with a color
|
||||||
dependent on their depth, so it's easier to tell overlapping caves
|
dependent on their depth, so it's easier to tell overlapping caves
|
||||||
apart)
|
apart)
|
||||||
|
|
||||||
**Default:** ``"normal"``
|
**Default:** ``"normal"``
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The value for the 'rendermode' key can be either a *string* or
|
The value for the 'rendermode' key can be either a *string* or
|
||||||
@@ -313,8 +328,8 @@ values. The valid configuration keys are listed below.
|
|||||||
objects. See :ref:`customrendermodes` for more information.
|
objects. See :ref:`customrendermodes` for more information.
|
||||||
|
|
||||||
``northdirection``
|
``northdirection``
|
||||||
This is direction that north will be rendered. This north direction will
|
This is direction that north will be rendered. This north direction will
|
||||||
match the established north direction in the game where the sun rises in the
|
match the established north direction in the game where the sun rises in the
|
||||||
east and sets in the west.
|
east and sets in the west.
|
||||||
|
|
||||||
Here are the valid north directions:
|
Here are the valid north directions:
|
||||||
@@ -327,27 +342,27 @@ values. The valid configuration keys are listed below.
|
|||||||
**Default:** ``"upper-left"``
|
**Default:** ``"upper-left"``
|
||||||
|
|
||||||
``rerenderprob``
|
``rerenderprob``
|
||||||
This is the probability that a tile will be rerendered even though there may
|
This is the probability that a tile will be rerendered even though there may
|
||||||
have been no changes to any blocks within that tile. Its value should be a
|
have been no changes to any blocks within that tile. Its value should be a
|
||||||
floating point number between 0.0 and 1.0.
|
floating point number between 0.0 and 1.0.
|
||||||
|
|
||||||
**Default:** ``0``
|
**Default:** ``0``
|
||||||
|
|
||||||
``imgformat``
|
``imgformat``
|
||||||
This is which image format to render the tiles into. Its value should be a
|
This is which image format to render the tiles into. Its value should be a
|
||||||
string containing "png", "jpg", or "jpeg".
|
string containing "png", "jpg", or "jpeg".
|
||||||
|
|
||||||
**Default:** ``"png"``
|
**Default:** ``"png"``
|
||||||
|
|
||||||
``imgquality``
|
``imgquality``
|
||||||
This is the image quality used when saving the tiles into the JPEG image
|
This is the image quality used when saving the tiles into the JPEG image
|
||||||
format. Its value should be an integer between 0 and 100.
|
format. Its value should be an integer between 0 and 100.
|
||||||
|
|
||||||
**Default:** ``95``
|
**Default:** ``95``
|
||||||
|
|
||||||
``bgcolor``
|
``bgcolor``
|
||||||
This is the background color to be displayed behind the map. Its value
|
This is the background color to be displayed behind the map. Its value
|
||||||
should be either a string in the standard HTML color syntax or a 4-tuple in
|
should be either a string in the standard HTML color syntax or a 4-tuple in
|
||||||
the format of (r,b,g,a). The alpha entry should be set to 0.
|
the format of (r,b,g,a). The alpha entry should be set to 0.
|
||||||
|
|
||||||
**Default:** ``#1a1a1a``
|
**Default:** ``#1a1a1a``
|
||||||
@@ -414,7 +429,7 @@ values. The valid configuration keys are listed below.
|
|||||||
configuration file. If you use :option:`--forcerender`, then all 3 of those
|
configuration file. If you use :option:`--forcerender`, then all 3 of those
|
||||||
renders get re-rendered completely. However, if you just need one of them
|
renders get re-rendered completely. However, if you just need one of them
|
||||||
re-rendered, that's unnecessary extra work.
|
re-rendered, that's unnecessary extra work.
|
||||||
|
|
||||||
If you set ``'forcerender': True,`` on just one of those renders, then just
|
If you set ``'forcerender': True,`` on just one of those renders, then just
|
||||||
that one gets re-rendered completely. The other two render normally (only
|
that one gets re-rendered completely. The other two render normally (only
|
||||||
tiles that need updating are rendered).
|
tiles that need updating are rendered).
|
||||||
@@ -454,7 +469,7 @@ values. The valid configuration keys are listed below.
|
|||||||
|
|
||||||
``markers``
|
``markers``
|
||||||
This controls the display of markers, signs, and other points of interest
|
This controls the display of markers, signs, and other points of interest
|
||||||
in the output HTML. It should be a list of filter functions.
|
in the output HTML. It should be a list of filter functions.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -462,7 +477,7 @@ values. The valid configuration keys are listed below.
|
|||||||
markers and signs on our map, you must also run the genPO script. See
|
markers and signs on our map, you must also run the genPO script. See
|
||||||
the :doc:`Signs and markers<signs>` section for more details and documenation.
|
the :doc:`Signs and markers<signs>` section for more details and documenation.
|
||||||
|
|
||||||
|
|
||||||
**Default:** ``[]`` (an empty list)
|
**Default:** ``[]`` (an empty list)
|
||||||
|
|
||||||
.. _customrendermodes:
|
.. _customrendermodes:
|
||||||
@@ -532,7 +547,7 @@ EdgeLines
|
|||||||
the background.
|
the background.
|
||||||
|
|
||||||
**Options**
|
**Options**
|
||||||
|
|
||||||
opacity
|
opacity
|
||||||
The darkness of the edge lines, from 0.0 to 1.0. Default: 0.15
|
The darkness of the edge lines, from 0.0 to 1.0. Default: 0.15
|
||||||
|
|
||||||
@@ -588,9 +603,9 @@ MineralOverlay
|
|||||||
Color the map according to what minerals can be found
|
Color the map according to what minerals can be found
|
||||||
underneath. Either use this on top of other modes, or on top of
|
underneath. Either use this on top of other modes, or on top of
|
||||||
ClearBase to create a pure overlay.
|
ClearBase to create a pure overlay.
|
||||||
|
|
||||||
**Options**
|
**Options**
|
||||||
|
|
||||||
minerals
|
minerals
|
||||||
A list of (blockid, (r, g, b)) tuples to use as colors. If not
|
A list of (blockid, (r, g, b)) tuples to use as colors. If not
|
||||||
provided, a default list of common minerals is used.
|
provided, a default list of common minerals is used.
|
||||||
|
|||||||
@@ -34,3 +34,22 @@ a ``python2.6`` package. To do this, add the following line to your
|
|||||||
|
|
||||||
Then run ``apt-get update`` and ``apt-get install minecraft-overviewer`` and
|
Then run ``apt-get update`` and ``apt-get install minecraft-overviewer`` and
|
||||||
you're all set! See you at the :doc:`running` page!
|
you're all set! See you at the :doc:`running` page!
|
||||||
|
|
||||||
|
CentOS / RHEL / Fedora
|
||||||
|
======================
|
||||||
|
We also provide a RPM repository with pre-built packages for users on RPM-based
|
||||||
|
distros. Note that on CentOS 5, the `EPEL <http://fedoraproject.org/wiki/EPEL>`_
|
||||||
|
repository is required to get Python 2.6 . To add the Overviewer repository to
|
||||||
|
YUM, just run
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
wget -O /etc/yum.repos.d/overviewer.repo http://overviewer.org/rpms/overviewer.repo
|
||||||
|
|
||||||
|
Then to install Overviewer run
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
yum install Minecraft-Overviewer
|
||||||
|
|
||||||
|
After that head to the :doc:`running` page!
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from overviewer_core import textures
|
|||||||
from overviewer_core import optimizeimages, world
|
from overviewer_core import optimizeimages, world
|
||||||
from overviewer_core import configParser, tileset, assetmanager, dispatcher
|
from overviewer_core import configParser, tileset, assetmanager, dispatcher
|
||||||
from overviewer_core import cache
|
from overviewer_core import cache
|
||||||
|
from overviewer_core import observer
|
||||||
|
|
||||||
helptext = """
|
helptext = """
|
||||||
%prog [--rendermodes=...] [options] <World> <Output Dir>
|
%prog [--rendermodes=...] [options] <World> <Output Dir>
|
||||||
@@ -53,7 +54,7 @@ def main():
|
|||||||
cpus = multiprocessing.cpu_count()
|
cpus = multiprocessing.cpu_count()
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
cpus = 1
|
cpus = 1
|
||||||
|
|
||||||
#avail_rendermodes = c_overviewer.get_render_modes()
|
#avail_rendermodes = c_overviewer.get_render_modes()
|
||||||
avail_north_dirs = ['lower-left', 'upper-left', 'upper-right', 'lower-right', 'auto']
|
avail_north_dirs = ['lower-left', 'upper-left', 'upper-right', 'lower-right', 'auto']
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ def main():
|
|||||||
# Options that only apply to the config-less render usage
|
# Options that only apply to the config-less render usage
|
||||||
parser.add_option("--rendermodes", dest="rendermodes", action="store",
|
parser.add_option("--rendermodes", dest="rendermodes", action="store",
|
||||||
help="If you're not using a config file, specify which rendermodes to render with this option. This is a comma-separated list.")
|
help="If you're not using a config file, specify which rendermodes to render with this option. This is a comma-separated list.")
|
||||||
|
|
||||||
# Useful one-time render modifiers:
|
# Useful one-time render modifiers:
|
||||||
parser.add_option("--forcerender", dest="forcerender", action="store_true",
|
parser.add_option("--forcerender", dest="forcerender", action="store_true",
|
||||||
help="Force re-rendering the entire map.")
|
help="Force re-rendering the entire map.")
|
||||||
@@ -162,7 +163,7 @@ def main():
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
list_worlds()
|
list_worlds()
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# This section does some sanity checking on the command line options passed
|
# This section does some sanity checking on the command line options passed
|
||||||
# in. It checks to see if --config was given that no worldname/destdir were
|
# in. It checks to see if --config was given that no worldname/destdir were
|
||||||
@@ -178,7 +179,7 @@ def main():
|
|||||||
logging.error("Cannot specify both --config AND a world + output directory on the command line.")
|
logging.error("Cannot specify both --config AND a world + output directory on the command line.")
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if not options.config and len(args) < 2:
|
if not options.config and len(args) < 2:
|
||||||
logging.error("You must specify both the world directory and an output directory")
|
logging.error("You must specify both the world directory and an output directory")
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
@@ -207,7 +208,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
worldpath, destdir = map(os.path.expanduser, args)
|
worldpath, destdir = map(os.path.expanduser, args)
|
||||||
logging.debug("Using %r as the world directory", worldpath)
|
logging.debug("Using %r as the world directory", worldpath)
|
||||||
logging.debug("Using %r as the output directory", destdir)
|
logging.debug("Using %r as the output directory", destdir)
|
||||||
|
|
||||||
mw_parser.set_config_item("worlds", {'world': worldpath})
|
mw_parser.set_config_item("worlds", {'world': worldpath})
|
||||||
mw_parser.set_config_item("outputdir", destdir)
|
mw_parser.set_config_item("outputdir", destdir)
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
logging.exception("An error was encountered with your configuration. See the info below.")
|
logging.exception("An error was encountered with your configuration. See the info below.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# Final validation steps and creation of the destination directory
|
# Final validation steps and creation of the destination directory
|
||||||
@@ -357,7 +358,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
w = world.World(render['world'])
|
w = world.World(render['world'])
|
||||||
worldcache[render['world']] = w
|
worldcache[render['world']] = w
|
||||||
|
|
||||||
# find or create the textures object
|
# find or create the textures object
|
||||||
texopts = util.dict_subset(render, ["texturepath", "bgcolor", "northdirection"])
|
texopts = util.dict_subset(render, ["texturepath", "bgcolor", "northdirection"])
|
||||||
texopts_key = tuple(texopts.items())
|
texopts_key = tuple(texopts.items())
|
||||||
@@ -385,12 +386,12 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
# If a crop is requested, wrap the regionset here
|
# If a crop is requested, wrap the regionset here
|
||||||
if "crop" in render:
|
if "crop" in render:
|
||||||
rset = world.CroppedRegionSet(rset, *render['crop'])
|
rset = world.CroppedRegionSet(rset, *render['crop'])
|
||||||
|
|
||||||
# If this is to be a rotated regionset, wrap it in a RotatedRegionSet
|
# If this is to be a rotated regionset, wrap it in a RotatedRegionSet
|
||||||
# object
|
# object
|
||||||
if (render['northdirection'] > 0):
|
if (render['northdirection'] > 0):
|
||||||
rset = world.RotatedRegionSet(rset, render['northdirection'])
|
rset = world.RotatedRegionSet(rset, render['northdirection'])
|
||||||
logging.debug("Using RegionSet %r", rset)
|
logging.debug("Using RegionSet %r", rset)
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# Do the final prep and create the TileSet object
|
# Do the final prep and create the TileSet object
|
||||||
@@ -411,25 +412,14 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
|
|
||||||
# Output initial static data and configuration
|
# Output initial static data and configuration
|
||||||
assetMrg.initialize(tilesets)
|
assetMrg.initialize(tilesets)
|
||||||
|
|
||||||
# multiprocessing dispatcher
|
# multiprocessing dispatcher
|
||||||
if config['processes'] == 1:
|
if config['processes'] == 1:
|
||||||
dispatch = dispatcher.Dispatcher()
|
dispatch = dispatcher.Dispatcher()
|
||||||
else:
|
else:
|
||||||
dispatch = dispatcher.MultiprocessingDispatcher(local_procs=config['processes'])
|
dispatch = dispatcher.MultiprocessingDispatcher(
|
||||||
last_status_print = time.time()
|
local_procs=config['processes'])
|
||||||
def print_status(phase, completed, total):
|
dispatch.render_all(tilesets, config['observer'])
|
||||||
# phase is ignored. it's always zero?
|
|
||||||
if (total == 0):
|
|
||||||
percent = 100
|
|
||||||
logging.info("Rendered %d of %d tiles. %d%% complete", completed, total, percent)
|
|
||||||
elif total == None:
|
|
||||||
logging.info("Rendered %d tiles.", completed)
|
|
||||||
else:
|
|
||||||
percent = int(100* completed/total)
|
|
||||||
logging.info("Rendered %d of %d. %d%% complete", completed, total, percent)
|
|
||||||
|
|
||||||
dispatch.render_all(tilesets, print_status)
|
|
||||||
dispatch.close()
|
dispatch.close()
|
||||||
|
|
||||||
assetMrg.finalize(tilesets)
|
assetMrg.finalize(tilesets)
|
||||||
@@ -447,7 +437,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
|||||||
|
|
||||||
def list_worlds():
|
def list_worlds():
|
||||||
"Prints out a brief summary of saves found in the default directory"
|
"Prints out a brief summary of saves found in the default directory"
|
||||||
print
|
print
|
||||||
worlds = world.get_worlds()
|
worlds = world.get_worlds()
|
||||||
if not worlds:
|
if not worlds:
|
||||||
print 'No world saves found in the usual place'
|
print 'No world saves found in the usual place'
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import multiprocessing.managers
|
|||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
import Queue
|
import Queue
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
|
|
||||||
from signals import Signal
|
from signals import Signal
|
||||||
|
|
||||||
class Dispatcher(object):
|
class Dispatcher(object):
|
||||||
@@ -32,15 +30,15 @@ class Dispatcher(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Dispatcher, self).__init__()
|
super(Dispatcher, self).__init__()
|
||||||
|
|
||||||
# list of (tileset, workitem) tuples
|
# list of (tileset, workitem) tuples
|
||||||
# keeps track of dispatched but unfinished jobs
|
# keeps track of dispatched but unfinished jobs
|
||||||
self._running_jobs = []
|
self._running_jobs = []
|
||||||
# list of (tileset, workitem, dependencies) tuples
|
# list of (tileset, workitem, dependencies) tuples
|
||||||
# keeps track of jobs waiting to run after dependencies finish
|
# keeps track of jobs waiting to run after dependencies finish
|
||||||
self._pending_jobs = []
|
self._pending_jobs = []
|
||||||
|
|
||||||
def render_all(self, tilesetlist, status_callback):
|
def render_all(self, tilesetlist, observer):
|
||||||
"""Render all of the tilesets in the given
|
"""Render all of the tilesets in the given
|
||||||
tilesetlist. status_callback is called periodically to update
|
tilesetlist. status_callback is called periodically to update
|
||||||
status. The callback should take the following arguments:
|
status. The callback should take the following arguments:
|
||||||
@@ -48,10 +46,10 @@ class Dispatcher(object):
|
|||||||
be none if there is no useful estimate.
|
be none if there is no useful estimate.
|
||||||
"""
|
"""
|
||||||
# TODO use status callback
|
# TODO use status callback
|
||||||
|
|
||||||
# setup tilesetlist
|
# setup tilesetlist
|
||||||
self.setup_tilesets(tilesetlist)
|
self.setup_tilesets(tilesetlist)
|
||||||
|
|
||||||
# iterate through all possible phases
|
# iterate through all possible phases
|
||||||
num_phases = [tileset.get_num_phases() for tileset in tilesetlist]
|
num_phases = [tileset.get_num_phases() for tileset in tilesetlist]
|
||||||
for phase in xrange(max(num_phases)):
|
for phase in xrange(max(num_phases)):
|
||||||
@@ -62,7 +60,7 @@ class Dispatcher(object):
|
|||||||
def make_work_iterator(tset, p):
|
def make_work_iterator(tset, p):
|
||||||
return ((tset, workitem) for workitem in tset.iterate_work_items(p))
|
return ((tset, workitem) for workitem in tset.iterate_work_items(p))
|
||||||
work_iterators.append(make_work_iterator(tileset, phase))
|
work_iterators.append(make_work_iterator(tileset, phase))
|
||||||
|
|
||||||
# keep track of total jobs, and how many jobs are done
|
# keep track of total jobs, and how many jobs are done
|
||||||
total_jobs = 0
|
total_jobs = 0
|
||||||
for tileset, phases in zip(tilesetlist, num_phases):
|
for tileset, phases in zip(tilesetlist, num_phases):
|
||||||
@@ -74,53 +72,30 @@ class Dispatcher(object):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
total_jobs += jobs_for_tileset
|
total_jobs += jobs_for_tileset
|
||||||
finished_jobs = 0
|
|
||||||
|
observer.start(total_jobs)
|
||||||
# do the first status update
|
|
||||||
self._status_update(status_callback, phase, finished_jobs, total_jobs, force=True)
|
|
||||||
|
|
||||||
# go through these iterators round-robin style
|
# go through these iterators round-robin style
|
||||||
for tileset, (workitem, deps) in util.roundrobin(work_iterators):
|
for tileset, (workitem, deps) in util.roundrobin(work_iterators):
|
||||||
self._pending_jobs.append((tileset, workitem, deps))
|
self._pending_jobs.append((tileset, workitem, deps))
|
||||||
finished_jobs += self._dispatch_jobs()
|
observer.add(self._dispatch_jobs())
|
||||||
self._status_update(status_callback, phase, finished_jobs, total_jobs)
|
|
||||||
|
|
||||||
# after each phase, wait for the work to finish
|
# after each phase, wait for the work to finish
|
||||||
while len(self._pending_jobs) > 0 or len(self._running_jobs) > 0:
|
while len(self._pending_jobs) > 0 or len(self._running_jobs) > 0:
|
||||||
finished_jobs += self._dispatch_jobs()
|
observer.add(self._dispatch_jobs())
|
||||||
self._status_update(status_callback, phase, finished_jobs, total_jobs)
|
|
||||||
|
observer.finish()
|
||||||
def _status_update(self, callback, phase, completed, total, force=False):
|
|
||||||
# always called with force=True at the beginning, so that can
|
|
||||||
# be used to set up state. After that, it is called after
|
|
||||||
# every _dispatch_jobs() often; this function is used to
|
|
||||||
# decide how often the actual status callback should be
|
|
||||||
# called.
|
|
||||||
if force:
|
|
||||||
self._last_status_update = completed
|
|
||||||
if callback:
|
|
||||||
callback(phase, completed, total)
|
|
||||||
return
|
|
||||||
|
|
||||||
if callback is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
update_interval = 100 # XXX arbitrary
|
|
||||||
if self._last_status_update < 0 or completed >= self._last_status_update + update_interval or completed < self._last_status_update:
|
|
||||||
self._last_status_update = completed
|
|
||||||
callback(phase, completed, total)
|
|
||||||
|
|
||||||
def _dispatch_jobs(self):
|
def _dispatch_jobs(self):
|
||||||
# helper function to dispatch pending jobs when their
|
# helper function to dispatch pending jobs when their
|
||||||
# dependencies are met, and to manage self._running_jobs
|
# dependencies are met, and to manage self._running_jobs
|
||||||
dispatched_jobs = []
|
dispatched_jobs = []
|
||||||
finished_jobs = []
|
finished_jobs = []
|
||||||
|
|
||||||
pending_jobs_nodeps = [(j[0], j[1]) for j in self._pending_jobs]
|
pending_jobs_nodeps = [(j[0], j[1]) for j in self._pending_jobs]
|
||||||
|
|
||||||
for pending_job in self._pending_jobs:
|
for pending_job in self._pending_jobs:
|
||||||
tileset, workitem, deps = pending_job
|
tileset, workitem, deps = pending_job
|
||||||
|
|
||||||
# see if any of the deps are in _running_jobs or _pending_jobs
|
# see if any of the deps are in _running_jobs or _pending_jobs
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
if (tileset, dep) in self._running_jobs or (tileset, dep) in pending_jobs_nodeps:
|
if (tileset, dep) in self._running_jobs or (tileset, dep) in pending_jobs_nodeps:
|
||||||
@@ -131,33 +106,33 @@ class Dispatcher(object):
|
|||||||
finished_jobs += self.dispatch(tileset, workitem)
|
finished_jobs += self.dispatch(tileset, workitem)
|
||||||
self._running_jobs.append((tileset, workitem))
|
self._running_jobs.append((tileset, workitem))
|
||||||
dispatched_jobs.append(pending_job)
|
dispatched_jobs.append(pending_job)
|
||||||
|
|
||||||
# make sure to at least get finished jobs, even if we don't
|
# make sure to at least get finished jobs, even if we don't
|
||||||
# submit any new ones...
|
# submit any new ones...
|
||||||
if len(dispatched_jobs) == 0:
|
if len(dispatched_jobs) == 0:
|
||||||
finished_jobs += self.dispatch(None, None)
|
finished_jobs += self.dispatch(None, None)
|
||||||
|
|
||||||
# clean out the appropriate lists
|
# clean out the appropriate lists
|
||||||
for job in finished_jobs:
|
for job in finished_jobs:
|
||||||
self._running_jobs.remove(job)
|
self._running_jobs.remove(job)
|
||||||
for job in dispatched_jobs:
|
for job in dispatched_jobs:
|
||||||
self._pending_jobs.remove(job)
|
self._pending_jobs.remove(job)
|
||||||
|
|
||||||
return len(finished_jobs)
|
return len(finished_jobs)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the Dispatcher. This should be called when you are
|
"""Close the Dispatcher. This should be called when you are
|
||||||
done with the dispatcher, to ensure that it cleans up any
|
done with the dispatcher, to ensure that it cleans up any
|
||||||
processes or connections it may still have around.
|
processes or connections it may still have around.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setup_tilesets(self, tilesetlist):
|
def setup_tilesets(self, tilesetlist):
|
||||||
"""Called whenever a new list of tilesets are being used. This
|
"""Called whenever a new list of tilesets are being used. This
|
||||||
lets subclasses distribute the whole list at once, instead of
|
lets subclasses distribute the whole list at once, instead of
|
||||||
for each work item."""
|
for each work item."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dispatch(self, tileset, workitem):
|
def dispatch(self, tileset, workitem):
|
||||||
"""Dispatch the given work item. The end result of this call
|
"""Dispatch the given work item. The end result of this call
|
||||||
should be running tileset.do_work(workitem) somewhere. This
|
should be running tileset.do_work(workitem) somewhere. This
|
||||||
@@ -176,31 +151,31 @@ class MultiprocessingDispatcherManager(multiprocessing.managers.BaseManager):
|
|||||||
workers access to the current tileset list.
|
workers access to the current tileset list.
|
||||||
"""
|
"""
|
||||||
def _get_job_queue(self):
|
def _get_job_queue(self):
|
||||||
return self.job_queue
|
return self.job_queue
|
||||||
def _get_results_queue(self):
|
def _get_results_queue(self):
|
||||||
return self.result_queue
|
return self.result_queue
|
||||||
def _get_signal_queue(self):
|
def _get_signal_queue(self):
|
||||||
return self.signal_queue
|
return self.signal_queue
|
||||||
def _get_tileset_data(self):
|
def _get_tileset_data(self):
|
||||||
return self.tileset_data
|
return self.tileset_data
|
||||||
|
|
||||||
def __init__(self, address=None, authkey=None):
|
def __init__(self, address=None, authkey=None):
|
||||||
self.job_queue = multiprocessing.Queue()
|
self.job_queue = multiprocessing.Queue()
|
||||||
self.result_queue = multiprocessing.Queue()
|
self.result_queue = multiprocessing.Queue()
|
||||||
self.signal_queue = multiprocessing.Queue()
|
self.signal_queue = multiprocessing.Queue()
|
||||||
|
|
||||||
self.tilesets = []
|
self.tilesets = []
|
||||||
self.tileset_version = 0
|
self.tileset_version = 0
|
||||||
self.tileset_data = [[], 0]
|
self.tileset_data = [[], 0]
|
||||||
|
|
||||||
self.register("get_job_queue", callable=self._get_job_queue)
|
self.register("get_job_queue", callable=self._get_job_queue)
|
||||||
self.register("get_result_queue", callable=self._get_results_queue)
|
self.register("get_result_queue", callable=self._get_results_queue)
|
||||||
self.register("get_signal_queue", callable=self._get_signal_queue)
|
self.register("get_signal_queue", callable=self._get_signal_queue)
|
||||||
self.register("get_tileset_data", callable=self._get_tileset_data, proxytype=multiprocessing.managers.ListProxy)
|
self.register("get_tileset_data", callable=self._get_tileset_data, proxytype=multiprocessing.managers.ListProxy)
|
||||||
|
|
||||||
super(MultiprocessingDispatcherManager, self).__init__(address=address, authkey=authkey)
|
super(MultiprocessingDispatcherManager, self).__init__(address=address, authkey=authkey)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_address(cls, address, authkey, serializer):
|
def from_address(cls, address, authkey, serializer):
|
||||||
"Required to be implemented to make multiprocessing happy"
|
"Required to be implemented to make multiprocessing happy"
|
||||||
c = cls(address=address, authkey=authkey)
|
c = cls(address=address, authkey=authkey)
|
||||||
@@ -218,7 +193,7 @@ class MultiprocessingDispatcherManager(multiprocessing.managers.BaseManager):
|
|||||||
data = self.get_tileset_data()
|
data = self.get_tileset_data()
|
||||||
data[0] = self.tilesets
|
data[0] = self.tilesets
|
||||||
data[1] = self.tileset_version
|
data[1] = self.tileset_version
|
||||||
|
|
||||||
|
|
||||||
class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
||||||
"""This class represents a single worker process. It is created
|
"""This class represents a single worker process. It is created
|
||||||
@@ -236,13 +211,13 @@ class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
|||||||
self.result_queue = manager.get_result_queue()
|
self.result_queue = manager.get_result_queue()
|
||||||
self.signal_queue = manager.get_signal_queue()
|
self.signal_queue = manager.get_signal_queue()
|
||||||
self.tileset_proxy = manager.get_tileset_data()
|
self.tileset_proxy = manager.get_tileset_data()
|
||||||
|
|
||||||
def update_tilesets(self):
|
def update_tilesets(self):
|
||||||
"""A convenience function to update our local tilesets to the
|
"""A convenience function to update our local tilesets to the
|
||||||
current version in use by the MultiprocessingDispatcher.
|
current version in use by the MultiprocessingDispatcher.
|
||||||
"""
|
"""
|
||||||
self.tilesets, self.tileset_version = self.tileset_proxy._getvalue()
|
self.tilesets, self.tileset_version = self.tileset_proxy._getvalue()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""The main work loop. Jobs are pulled from the job queue and
|
"""The main work loop. Jobs are pulled from the job queue and
|
||||||
executed, then the result is pushed onto the result
|
executed, then the result is pushed onto the result
|
||||||
@@ -252,10 +227,10 @@ class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
|||||||
"""
|
"""
|
||||||
# per-process job get() timeout
|
# per-process job get() timeout
|
||||||
timeout = 1.0
|
timeout = 1.0
|
||||||
|
|
||||||
# update our tilesets
|
# update our tilesets
|
||||||
self.update_tilesets()
|
self.update_tilesets()
|
||||||
|
|
||||||
# register for all available signals
|
# register for all available signals
|
||||||
def register_signal(name, sig):
|
def register_signal(name, sig):
|
||||||
def handler(*args, **kwargs):
|
def handler(*args, **kwargs):
|
||||||
@@ -263,7 +238,7 @@ class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
|||||||
sig.set_interceptor(handler)
|
sig.set_interceptor(handler)
|
||||||
for name, sig in Signal.signals.iteritems():
|
for name, sig in Signal.signals.iteritems():
|
||||||
register_signal(name, sig)
|
register_signal(name, sig)
|
||||||
|
|
||||||
# notify that we're starting up
|
# notify that we're starting up
|
||||||
self.result_queue.put(None, False)
|
self.result_queue.put(None, False)
|
||||||
while True:
|
while True:
|
||||||
@@ -272,15 +247,15 @@ class MultiprocessingDispatcherProcess(multiprocessing.Process):
|
|||||||
if job == None:
|
if job == None:
|
||||||
# this is a end-of-jobs sentinel
|
# this is a end-of-jobs sentinel
|
||||||
return
|
return
|
||||||
|
|
||||||
# unpack job
|
# unpack job
|
||||||
tv, ti, workitem = job
|
tv, ti, workitem = job
|
||||||
|
|
||||||
if tv != self.tileset_version:
|
if tv != self.tileset_version:
|
||||||
# our tilesets changed!
|
# our tilesets changed!
|
||||||
self.update_tilesets()
|
self.update_tilesets()
|
||||||
assert tv == self.tileset_version
|
assert tv == self.tileset_version
|
||||||
|
|
||||||
# do job
|
# do job
|
||||||
ret = self.tilesets[ti].do_work(workitem)
|
ret = self.tilesets[ti].do_work(workitem)
|
||||||
result = (ti, workitem, ret,)
|
result = (ti, workitem, ret,)
|
||||||
@@ -298,12 +273,12 @@ class MultiprocessingDispatcher(Dispatcher):
|
|||||||
the number of available CPUs is used instead.
|
the number of available CPUs is used instead.
|
||||||
"""
|
"""
|
||||||
super(MultiprocessingDispatcher, self).__init__()
|
super(MultiprocessingDispatcher, self).__init__()
|
||||||
|
|
||||||
# automatic local_procs handling
|
# automatic local_procs handling
|
||||||
if local_procs < 0:
|
if local_procs < 0:
|
||||||
local_procs = multiprocessing.cpu_count()
|
local_procs = multiprocessing.cpu_count()
|
||||||
self.local_procs = local_procs
|
self.local_procs = local_procs
|
||||||
|
|
||||||
self.outstanding_jobs = 0
|
self.outstanding_jobs = 0
|
||||||
self.num_workers = 0
|
self.num_workers = 0
|
||||||
self.manager = MultiprocessingDispatcherManager(address=address, authkey=authkey)
|
self.manager = MultiprocessingDispatcherManager(address=address, authkey=authkey)
|
||||||
@@ -311,63 +286,63 @@ class MultiprocessingDispatcher(Dispatcher):
|
|||||||
self.job_queue = self.manager.get_job_queue()
|
self.job_queue = self.manager.get_job_queue()
|
||||||
self.result_queue = self.manager.get_result_queue()
|
self.result_queue = self.manager.get_result_queue()
|
||||||
self.signal_queue = self.manager.get_signal_queue()
|
self.signal_queue = self.manager.get_signal_queue()
|
||||||
|
|
||||||
# create and fill the pool
|
# create and fill the pool
|
||||||
self.pool = []
|
self.pool = []
|
||||||
for i in xrange(self.local_procs):
|
for i in xrange(self.local_procs):
|
||||||
proc = MultiprocessingDispatcherProcess(self.manager)
|
proc = MultiprocessingDispatcherProcess(self.manager)
|
||||||
proc.start()
|
proc.start()
|
||||||
self.pool.append(proc)
|
self.pool.append(proc)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
# empty the queue
|
# empty the queue
|
||||||
self._handle_messages(timeout=0.0)
|
self._handle_messages(timeout=0.0)
|
||||||
while self.outstanding_jobs > 0:
|
while self.outstanding_jobs > 0:
|
||||||
self._handle_messages()
|
self._handle_messages()
|
||||||
|
|
||||||
# send of the end-of-jobs sentinel
|
# send of the end-of-jobs sentinel
|
||||||
for p in xrange(self.num_workers):
|
for p in xrange(self.num_workers):
|
||||||
self.job_queue.put(None, False)
|
self.job_queue.put(None, False)
|
||||||
|
|
||||||
# TODO better way to be sure worker processes get the message
|
# TODO better way to be sure worker processes get the message
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# and close the manager
|
# and close the manager
|
||||||
self.manager.shutdown()
|
self.manager.shutdown()
|
||||||
self.manager = None
|
self.manager = None
|
||||||
self.pool = None
|
self.pool = None
|
||||||
|
|
||||||
def setup_tilesets(self, tilesets):
|
def setup_tilesets(self, tilesets):
|
||||||
self.manager.set_tilesets(tilesets)
|
self.manager.set_tilesets(tilesets)
|
||||||
|
|
||||||
def dispatch(self, tileset, workitem):
|
def dispatch(self, tileset, workitem):
|
||||||
# handle the no-new-work case
|
# handle the no-new-work case
|
||||||
if tileset is None:
|
if tileset is None:
|
||||||
return self._handle_messages()
|
return self._handle_messages()
|
||||||
|
|
||||||
# create and submit the job
|
# create and submit the job
|
||||||
tileset_index = self.manager.tilesets.index(tileset)
|
tileset_index = self.manager.tilesets.index(tileset)
|
||||||
self.job_queue.put((self.manager.tileset_version, tileset_index, workitem), False)
|
self.job_queue.put((self.manager.tileset_version, tileset_index, workitem), False)
|
||||||
self.outstanding_jobs += 1
|
self.outstanding_jobs += 1
|
||||||
|
|
||||||
# make sure the queue doesn't fill up too much
|
# make sure the queue doesn't fill up too much
|
||||||
finished_jobs = self._handle_messages(timeout=0.0)
|
finished_jobs = self._handle_messages(timeout=0.0)
|
||||||
while self.outstanding_jobs > self.num_workers * 10:
|
while self.outstanding_jobs > self.num_workers * 10:
|
||||||
finished_jobs += self._handle_messages()
|
finished_jobs += self._handle_messages()
|
||||||
return finished_jobs
|
return finished_jobs
|
||||||
|
|
||||||
def _handle_messages(self, timeout=0.01):
|
def _handle_messages(self, timeout=0.01):
|
||||||
# work function: takes results out of the result queue and
|
# work function: takes results out of the result queue and
|
||||||
# keeps track of how many outstanding jobs remain
|
# keeps track of how many outstanding jobs remain
|
||||||
finished_jobs = []
|
finished_jobs = []
|
||||||
|
|
||||||
result_empty = False
|
result_empty = False
|
||||||
signal_empty = False
|
signal_empty = False
|
||||||
while not (result_empty and signal_empty):
|
while not (result_empty and signal_empty):
|
||||||
if not result_empty:
|
if not result_empty:
|
||||||
try:
|
try:
|
||||||
result = self.result_queue.get(False)
|
result = self.result_queue.get(False)
|
||||||
|
|
||||||
if result != None:
|
if result != None:
|
||||||
# completed job
|
# completed job
|
||||||
ti, workitem, ret = result
|
ti, workitem, ret = result
|
||||||
@@ -386,14 +361,14 @@ class MultiprocessingDispatcher(Dispatcher):
|
|||||||
name, args, kwargs = self.signal_queue.get(False)
|
name, args, kwargs = self.signal_queue.get(False)
|
||||||
# timeout should only apply once
|
# timeout should only apply once
|
||||||
timeout = 0.0
|
timeout = 0.0
|
||||||
|
|
||||||
sig = Signal.signals[name]
|
sig = Signal.signals[name]
|
||||||
sig.emit_intercepted(*args, **kwargs)
|
sig.emit_intercepted(*args, **kwargs)
|
||||||
except Queue.Empty:
|
except Queue.Empty:
|
||||||
signal_empty = True
|
signal_empty = True
|
||||||
|
|
||||||
return finished_jobs
|
return finished_jobs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start_manual_process(cls, address, authkey):
|
def start_manual_process(cls, address, authkey):
|
||||||
"""A convenience method to start up a manual process, possibly
|
"""A convenience method to start up a manual process, possibly
|
||||||
|
|||||||
164
overviewer_core/observer.py
Normal file
164
overviewer_core/observer.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# This file is part of the Minecraft Overviewer.
|
||||||
|
#
|
||||||
|
# Minecraft Overviewer is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
# your option) any later version.
|
||||||
|
#
|
||||||
|
# Minecraft Overviewer is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
# Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with the Overviewer. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import progressbar
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class Observer(object):
|
||||||
|
"""Base class that defines the observer interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._current_value = None
|
||||||
|
self._max_value = None
|
||||||
|
self.start_time = None
|
||||||
|
self.end_time = None
|
||||||
|
|
||||||
|
def start(self, max_value):
|
||||||
|
"""Signals the start of whatever process. Must be called before update
|
||||||
|
"""
|
||||||
|
self._set_max_value(max_value)
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.update(0)
|
||||||
|
return self
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
class LoggingObserver(Observer):
|
||||||
|
"""Simple observer that just outputs status through logging.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(Observer, self).__init__()
|
||||||
|
#this is an easy way to make the first update() call print a line
|
||||||
|
self.last_update = -101
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
logging.info("Rendered %d of %d. %d%% complete", self.get_max_value(),
|
||||||
|
self.get_max_value(), 100.0)
|
||||||
|
super(LoggingObserver, self).finish()
|
||||||
|
|
||||||
|
def update(self, current_value):
|
||||||
|
super(LoggingObserver, self).update(current_value)
|
||||||
|
if self._need_update():
|
||||||
|
logging.info("Rendered %d of %d. %d%% complete",
|
||||||
|
self.get_current_value(), self.get_max_value(),
|
||||||
|
self.get_percentage())
|
||||||
|
self.last_update = current_value
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
default_widgets = [
|
||||||
|
progressbar.Percentage(), ' ',
|
||||||
|
progressbar.Bar(marker='=', left='[', right=']'), ' ',
|
||||||
|
progressbar.CounterWidget(), ' ',
|
||||||
|
progressbar.GenericSpeed(format='%.2ft/s'), ' ',
|
||||||
|
progressbar.ETA(prefix='eta ')
|
||||||
|
]
|
||||||
|
class ProgressBarObserver(progressbar.ProgressBar, Observer):
|
||||||
|
"""Display progress through a progressbar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#the progress bar is only updated in increments of this for performance
|
||||||
|
UPDATE_INTERVAL = 25
|
||||||
|
|
||||||
|
def __init__(self, widgets=default_widgets, term_width=None, fd=sys.stderr):
|
||||||
|
super(ProgressBarObserver, self).__init__(widgets=widgets,
|
||||||
|
term_width=term_width, fd=fd)
|
||||||
|
self.last_update = 0 - (self.UPDATE_INTERVAL + 1)
|
||||||
|
|
||||||
|
def start(self, max_value):
|
||||||
|
self._set_max_value(max_value)
|
||||||
|
logging.info("Rendering %d total tiles." % max_value)
|
||||||
|
super(ProgressBarObserver, self).start()
|
||||||
|
|
||||||
|
def is_started(self):
|
||||||
|
return self.start_time is not None
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
self._end_time = time.time()
|
||||||
|
super(ProgressBarObserver, self).finish()
|
||||||
|
|
||||||
|
def update(self, current_value):
|
||||||
|
if super(ProgressBarObserver, self).update(current_value):
|
||||||
|
self.last_update = self.get_current_value()
|
||||||
|
|
||||||
|
percentage = Observer.get_percentage
|
||||||
|
|
||||||
|
def get_current_value(self):
|
||||||
|
return self.currval
|
||||||
|
|
||||||
|
def get_max_value(self):
|
||||||
|
return self.maxval
|
||||||
|
|
||||||
|
def _set_max_value(self, max_value):
|
||||||
|
self.maxval = max_value
|
||||||
|
|
||||||
|
def _need_update(self):
|
||||||
|
return self.get_current_value() - self.last_update > self.UPDATE_INTERVAL
|
||||||
399
overviewer_core/progressbar.py
Normal file
399
overviewer_core/progressbar.py
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
#
|
||||||
|
# progressbar - Text progressbar library for python.
|
||||||
|
# Copyright (c) 2005 Nilton Volpato
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
|
||||||
|
"""Text progressbar library for python.
|
||||||
|
|
||||||
|
This library provides a text mode progressbar. This is tipically used
|
||||||
|
to display the progress of a long running operation, providing a
|
||||||
|
visual clue that processing is underway.
|
||||||
|
|
||||||
|
The ProgressBar class manages the progress, and the format of the line
|
||||||
|
is given by a number of widgets. A widget is an object that may
|
||||||
|
display diferently depending on the state of the progress. There are
|
||||||
|
three types of widget:
|
||||||
|
- a string, which always shows itself;
|
||||||
|
- a ProgressBarWidget, which may return a diferent value every time
|
||||||
|
it's update method is called; and
|
||||||
|
- a ProgressBarWidgetHFill, which is like ProgressBarWidget, except it
|
||||||
|
expands to fill the remaining width of the line.
|
||||||
|
|
||||||
|
The progressbar module is very easy to use, yet very powerful. And
|
||||||
|
automatically supports features like auto-resizing when available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Nilton Volpato"
|
||||||
|
__author_email__ = "first-name dot last-name @ gmail.com"
|
||||||
|
__date__ = "2006-05-07"
|
||||||
|
__version__ = "2.2"
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
#
|
||||||
|
# 2006-05-07: v2.2 fixed bug in windows
|
||||||
|
# 2005-12-04: v2.1 autodetect terminal width, added start method
|
||||||
|
# 2005-12-04: v2.0 everything is now a widget (wow!)
|
||||||
|
# 2005-12-03: v1.0 rewrite using widgets
|
||||||
|
# 2005-06-02: v0.5 rewrite
|
||||||
|
# 2004-??-??: v0.1 first version
|
||||||
|
|
||||||
|
|
||||||
|
import sys, time
|
||||||
|
from array import array
|
||||||
|
try:
|
||||||
|
from fcntl import ioctl
|
||||||
|
import termios
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
import signal
|
||||||
|
|
||||||
|
class ProgressBarWidget(object):
|
||||||
|
"""This is an element of ProgressBar formatting.
|
||||||
|
|
||||||
|
The ProgressBar object will call it's update value when an update
|
||||||
|
is needed. It's size may change between call, but the results will
|
||||||
|
not be good if the size changes drastically and repeatedly.
|
||||||
|
"""
|
||||||
|
def update(self, pbar):
|
||||||
|
"""Returns the string representing the widget.
|
||||||
|
|
||||||
|
The parameter pbar is a reference to the calling ProgressBar,
|
||||||
|
where one can access attributes of the class for knowing how
|
||||||
|
the update must be made.
|
||||||
|
|
||||||
|
At least this function must be overriden."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ProgressBarWidgetHFill(object):
|
||||||
|
"""This is a variable width element of ProgressBar formatting.
|
||||||
|
|
||||||
|
The ProgressBar object will call it's update value, informing the
|
||||||
|
width this object must the made. This is like TeX \\hfill, it will
|
||||||
|
expand to fill the line. You can use more than one in the same
|
||||||
|
line, and they will all have the same width, and together will
|
||||||
|
fill the line.
|
||||||
|
"""
|
||||||
|
def update(self, pbar, width):
|
||||||
|
"""Returns the string representing the widget.
|
||||||
|
|
||||||
|
The parameter pbar is a reference to the calling ProgressBar,
|
||||||
|
where one can access attributes of the class for knowing how
|
||||||
|
the update must be made. The parameter width is the total
|
||||||
|
horizontal width the widget must have.
|
||||||
|
|
||||||
|
At least this function must be overriden."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ETA(ProgressBarWidget):
|
||||||
|
"Widget for the Estimated Time of Arrival"
|
||||||
|
def __init__(self, prefix='ETA: ', format='%H:%M:%S'):
|
||||||
|
self.format = format
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
def format_time(self, seconds):
|
||||||
|
return time.strftime(self.format, time.gmtime(seconds))
|
||||||
|
|
||||||
|
def update(self, pbar):
|
||||||
|
if pbar.currval == 0:
|
||||||
|
return self.prefix + '-' * len(self.format)
|
||||||
|
elif pbar.finished:
|
||||||
|
return 'Time: %s' % self.format_time(pbar.seconds_elapsed)
|
||||||
|
else:
|
||||||
|
eta = pbar.seconds_elapsed * pbar.maxval / pbar.currval - pbar.seconds_elapsed
|
||||||
|
return self.prefix + self.format_time(eta)
|
||||||
|
|
||||||
|
class GenericSpeed(ProgressBarWidget):
|
||||||
|
"Widget for showing the values/s"
|
||||||
|
def __init__(self, format='%6.2f ?/s'):
|
||||||
|
if callable(format):
|
||||||
|
self.format = format
|
||||||
|
else:
|
||||||
|
self.format = lambda speed: format % speed
|
||||||
|
def update(self, pbar):
|
||||||
|
if pbar.seconds_elapsed < 2e-6:
|
||||||
|
speed = 0.0
|
||||||
|
else:
|
||||||
|
speed = float(pbar.currval) / pbar.seconds_elapsed
|
||||||
|
return self.format(speed)
|
||||||
|
|
||||||
|
class FileTransferSpeed(ProgressBarWidget):
|
||||||
|
"Widget for showing the transfer speed (useful for file transfers)."
|
||||||
|
def __init__(self):
|
||||||
|
self.fmt = '%6.2f %s'
|
||||||
|
self.units = ['B','K','M','G','T','P']
|
||||||
|
def update(self, pbar):
|
||||||
|
if pbar.seconds_elapsed < 2e-6:#== 0:
|
||||||
|
bps = 0.0
|
||||||
|
else:
|
||||||
|
bps = float(pbar.currval) / pbar.seconds_elapsed
|
||||||
|
spd = bps
|
||||||
|
for u in self.units:
|
||||||
|
if spd < 1000:
|
||||||
|
break
|
||||||
|
spd /= 1000
|
||||||
|
return self.fmt % (spd, u+'/s')
|
||||||
|
|
||||||
|
class RotatingMarker(ProgressBarWidget):
|
||||||
|
"A rotating marker for filling the bar of progress."
|
||||||
|
def __init__(self, markers='|/-\\'):
|
||||||
|
self.markers = markers
|
||||||
|
self.curmark = -1
|
||||||
|
def update(self, pbar):
|
||||||
|
if pbar.finished:
|
||||||
|
return self.markers[0]
|
||||||
|
self.curmark = (self.curmark + 1)%len(self.markers)
|
||||||
|
return self.markers[self.curmark]
|
||||||
|
|
||||||
|
class Percentage(ProgressBarWidget):
|
||||||
|
"Just the percentage done."
|
||||||
|
def __init__(self, format='%3d%%'):
|
||||||
|
self.format = format
|
||||||
|
|
||||||
|
def update(self, pbar):
|
||||||
|
return self.format % pbar.percentage()
|
||||||
|
|
||||||
|
class CounterWidget(ProgressBarWidget):
|
||||||
|
"Simple display of (just) the current value"
|
||||||
|
def update(self, pbar):
|
||||||
|
return str(pbar.currval)
|
||||||
|
|
||||||
|
class FractionWidget(ProgressBarWidget):
|
||||||
|
def __init__(self, sep=' / '):
|
||||||
|
self.sep = sep
|
||||||
|
def update(self, pbar):
|
||||||
|
return '%2d%s%2d' % (pbar.currval, self.sep, pbar.maxval)
|
||||||
|
|
||||||
|
class Bar(ProgressBarWidgetHFill):
|
||||||
|
"The bar of progress. It will strech to fill the line."
|
||||||
|
def __init__(self, marker='#', left='|', right='|'):
|
||||||
|
self.marker = marker
|
||||||
|
self.left = left
|
||||||
|
self.right = right
|
||||||
|
def _format_marker(self, pbar):
|
||||||
|
if isinstance(self.marker, (str, unicode)):
|
||||||
|
return self.marker
|
||||||
|
else:
|
||||||
|
return self.marker.update(pbar)
|
||||||
|
def update(self, pbar, width):
|
||||||
|
percent = pbar.percentage()
|
||||||
|
cwidth = width - len(self.left) - len(self.right)
|
||||||
|
marked_width = int(percent * cwidth / 100)
|
||||||
|
m = self._format_marker(pbar)
|
||||||
|
bar = (self.left + (m*marked_width).ljust(cwidth) + self.right)
|
||||||
|
return bar
|
||||||
|
|
||||||
|
class ReverseBar(Bar):
|
||||||
|
"The reverse bar of progress, or bar of regress. :)"
|
||||||
|
def update(self, pbar, width):
|
||||||
|
percent = pbar.percentage()
|
||||||
|
cwidth = width - len(self.left) - len(self.right)
|
||||||
|
marked_width = int(percent * cwidth / 100)
|
||||||
|
m = self._format_marker(pbar)
|
||||||
|
bar = (self.left + (m*marked_width).rjust(cwidth) + self.right)
|
||||||
|
return bar
|
||||||
|
|
||||||
|
default_widgets = [Percentage(), ' ', Bar()]
|
||||||
|
class ProgressBar(object):
|
||||||
|
"""This is the ProgressBar class, it updates and prints the bar.
|
||||||
|
|
||||||
|
The term_width parameter may be an integer. Or None, in which case
|
||||||
|
it will try to guess it, if it fails it will default to 80 columns.
|
||||||
|
|
||||||
|
The simple use is like this:
|
||||||
|
>>> pbar = ProgressBar().start()
|
||||||
|
>>> for i in xrange(100):
|
||||||
|
... # do something
|
||||||
|
... pbar.update(i+1)
|
||||||
|
...
|
||||||
|
>>> pbar.finish()
|
||||||
|
|
||||||
|
But anything you want to do is possible (well, almost anything).
|
||||||
|
You can supply different widgets of any type in any order. And you
|
||||||
|
can even write your own widgets! There are many widgets already
|
||||||
|
shipped and you should experiment with them.
|
||||||
|
|
||||||
|
When implementing a widget update method you may access any
|
||||||
|
attribute or function of the ProgressBar object calling the
|
||||||
|
widget's update method. The most important attributes you would
|
||||||
|
like to access are:
|
||||||
|
- currval: current value of the progress, 0 <= currval <= maxval
|
||||||
|
- maxval: maximum (and final) value of the progress
|
||||||
|
- finished: True if the bar is have finished (reached 100%), False o/w
|
||||||
|
- start_time: first time update() method of ProgressBar was called
|
||||||
|
- seconds_elapsed: seconds elapsed since start_time
|
||||||
|
- percentage(): percentage of the progress (this is a method)
|
||||||
|
"""
|
||||||
|
def __init__(self, maxval=100, widgets=default_widgets, term_width=None,
|
||||||
|
fd=sys.stderr):
|
||||||
|
assert maxval > 0
|
||||||
|
self.maxval = maxval
|
||||||
|
self.widgets = widgets
|
||||||
|
self.fd = fd
|
||||||
|
self.signal_set = False
|
||||||
|
if term_width is None:
|
||||||
|
try:
|
||||||
|
self.handle_resize(None,None)
|
||||||
|
signal.signal(signal.SIGWINCH, self.handle_resize)
|
||||||
|
self.signal_set = True
|
||||||
|
except:
|
||||||
|
self.term_width = 79
|
||||||
|
else:
|
||||||
|
self.term_width = term_width
|
||||||
|
|
||||||
|
self.currval = 0
|
||||||
|
self.finished = False
|
||||||
|
self.start_time = None
|
||||||
|
self.seconds_elapsed = 0
|
||||||
|
|
||||||
|
def handle_resize(self, signum, frame):
|
||||||
|
h,w=array('h', ioctl(self.fd,termios.TIOCGWINSZ,'\0'*8))[:2]
|
||||||
|
self.term_width = w
|
||||||
|
|
||||||
|
def percentage(self):
|
||||||
|
"Returns the percentage of the progress."
|
||||||
|
return self.currval*100.0 / self.maxval
|
||||||
|
|
||||||
|
def _format_widgets(self):
|
||||||
|
r = []
|
||||||
|
hfill_inds = []
|
||||||
|
num_hfill = 0
|
||||||
|
currwidth = 0
|
||||||
|
for i, w in enumerate(self.widgets):
|
||||||
|
if isinstance(w, ProgressBarWidgetHFill):
|
||||||
|
r.append(w)
|
||||||
|
hfill_inds.append(i)
|
||||||
|
num_hfill += 1
|
||||||
|
elif isinstance(w, (str, unicode)):
|
||||||
|
r.append(w)
|
||||||
|
currwidth += len(w)
|
||||||
|
else:
|
||||||
|
weval = w.update(self)
|
||||||
|
currwidth += len(weval)
|
||||||
|
r.append(weval)
|
||||||
|
for iw in hfill_inds:
|
||||||
|
r[iw] = r[iw].update(self, (self.term_width-currwidth)/num_hfill)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _format_line(self):
|
||||||
|
return ''.join(self._format_widgets()).ljust(self.term_width)
|
||||||
|
|
||||||
|
def _need_update(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update(self, value):
|
||||||
|
"Updates the progress bar to a new value."
|
||||||
|
assert 0 <= value <= self.maxval
|
||||||
|
self.currval = value
|
||||||
|
if not self._need_update() or self.finished:
|
||||||
|
return False
|
||||||
|
if not self.start_time:
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.seconds_elapsed = time.time() - self.start_time
|
||||||
|
if value != self.maxval:
|
||||||
|
self.fd.write(self._format_line() + '\r')
|
||||||
|
else:
|
||||||
|
self.finished = True
|
||||||
|
self.fd.write(self._format_line() + '\n')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start measuring time, and prints the bar at 0%.
|
||||||
|
|
||||||
|
It returns self so you can use it like this:
|
||||||
|
>>> pbar = ProgressBar().start()
|
||||||
|
>>> for i in xrange(100):
|
||||||
|
... # do something
|
||||||
|
... pbar.update(i+1)
|
||||||
|
...
|
||||||
|
>>> pbar.finish()
|
||||||
|
"""
|
||||||
|
self.update(0)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
"""Used to tell the progress is finished."""
|
||||||
|
self.update(self.maxval)
|
||||||
|
if self.signal_set:
|
||||||
|
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
import os
|
||||||
|
|
||||||
|
def example1():
|
||||||
|
widgets = ['Test: ', Percentage(), ' ', Bar(marker=RotatingMarker()),
|
||||||
|
' ', ETA(), ' ', FileTransferSpeed()]
|
||||||
|
pbar = ProgressBar(widgets=widgets, maxval=10000000).start()
|
||||||
|
for i in range(1000000):
|
||||||
|
# do something
|
||||||
|
pbar.update(10*i+1)
|
||||||
|
pbar.finish()
|
||||||
|
print
|
||||||
|
|
||||||
|
def example2():
|
||||||
|
class CrazyFileTransferSpeed(FileTransferSpeed):
|
||||||
|
"It's bigger between 45 and 80 percent"
|
||||||
|
def update(self, pbar):
|
||||||
|
if 45 < pbar.percentage() < 80:
|
||||||
|
return 'Bigger Now ' + FileTransferSpeed.update(self,pbar)
|
||||||
|
else:
|
||||||
|
return FileTransferSpeed.update(self,pbar)
|
||||||
|
|
||||||
|
widgets = [CrazyFileTransferSpeed(),' <<<', Bar(), '>>> ', Percentage(),' ', ETA()]
|
||||||
|
pbar = ProgressBar(widgets=widgets, maxval=10000000)
|
||||||
|
# maybe do something
|
||||||
|
pbar.start()
|
||||||
|
for i in range(2000000):
|
||||||
|
# do something
|
||||||
|
pbar.update(5*i+1)
|
||||||
|
pbar.finish()
|
||||||
|
print
|
||||||
|
|
||||||
|
def example3():
|
||||||
|
widgets = [Bar('>'), ' ', ETA(), ' ', ReverseBar('<')]
|
||||||
|
pbar = ProgressBar(widgets=widgets, maxval=10000000).start()
|
||||||
|
for i in range(1000000):
|
||||||
|
# do something
|
||||||
|
pbar.update(10*i+1)
|
||||||
|
pbar.finish()
|
||||||
|
print
|
||||||
|
|
||||||
|
def example4():
|
||||||
|
widgets = ['Test: ', Percentage(), ' ',
|
||||||
|
Bar(marker='0',left='[',right=']'),
|
||||||
|
' ', ETA(), ' ', FileTransferSpeed()]
|
||||||
|
pbar = ProgressBar(widgets=widgets, maxval=500)
|
||||||
|
pbar.start()
|
||||||
|
for i in range(100,500+1,50):
|
||||||
|
time.sleep(0.2)
|
||||||
|
pbar.update(i)
|
||||||
|
pbar.finish()
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
example1()
|
||||||
|
example2()
|
||||||
|
example3()
|
||||||
|
example4()
|
||||||
|
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
|
|
||||||
from settingsValidators import *
|
from settingsValidators import *
|
||||||
import util
|
import util
|
||||||
|
from observer import ProgressBarObserver, LoggingObserver
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
# renders is a dictionary mapping strings to dicts. These dicts describe the
|
# renders is a dictionary mapping strings to dicts. These dicts describe the
|
||||||
# configuration for that render. Therefore, the validator for 'renders' is set
|
# configuration for that render. Therefore, the validator for 'renders' is set
|
||||||
@@ -76,7 +79,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
|
|||||||
"crop": Setting(required=False, validator=validateCrop, default=None),
|
"crop": Setting(required=False, validator=validateCrop, default=None),
|
||||||
"changelist": Setting(required=False, validator=validateStr, default=None),
|
"changelist": Setting(required=False, validator=validateStr, default=None),
|
||||||
"markers": Setting(required=False, validator=validateMarkers, default=[]),
|
"markers": Setting(required=False, validator=validateMarkers, default=[]),
|
||||||
|
|
||||||
# Remove this eventually (once people update their configs)
|
# Remove this eventually (once people update their configs)
|
||||||
"worldname": Setting(required=False, default=None,
|
"worldname": Setting(required=False, default=None,
|
||||||
validator=error("The option 'worldname' is now called 'world'. Please update your config files")),
|
validator=error("The option 'worldname' is now called 'world'. Please update your config files")),
|
||||||
@@ -93,3 +96,10 @@ processes = Setting(required=True, validator=int, default=-1)
|
|||||||
# memcached is an option, but unless your IO costs are really high, it just
|
# memcached is an option, but unless your IO costs are really high, it just
|
||||||
# ends up adding overhead and isn't worth it.
|
# ends up adding overhead and isn't worth it.
|
||||||
memcached_host = Setting(required=False, validator=str, default=None)
|
memcached_host = Setting(required=False, validator=str, default=None)
|
||||||
|
|
||||||
|
if platform.system() == 'Windows' or not sys.stderr.isatty():
|
||||||
|
obs = LoggingObserver()
|
||||||
|
else:
|
||||||
|
obs = ProgressBarObserver()
|
||||||
|
|
||||||
|
observer = Setting(required=True, validator=validateObserver, default=obs)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def validateRenderMode(mode):
|
|||||||
|
|
||||||
if isinstance(mode, rendermodes.RenderPrimitive):
|
if isinstance(mode, rendermodes.RenderPrimitive):
|
||||||
mode = [mode]
|
mode = [mode]
|
||||||
|
|
||||||
if not isinstance(mode, list):
|
if not isinstance(mode, list):
|
||||||
raise ValidationException("%r is not a valid list of rendermodes. It should be a list"% mode)
|
raise ValidationException("%r is not a valid list of rendermodes. It should be a list"% mode)
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ def validateImgQuality(qual):
|
|||||||
|
|
||||||
def validateBGColor(color):
|
def validateBGColor(color):
|
||||||
"""BG color must be an HTML color, with an option leading # (hash symbol)
|
"""BG color must be an HTML color, with an option leading # (hash symbol)
|
||||||
returns an (r,b,g) 3-tuple
|
returns an (r,b,g) 3-tuple
|
||||||
"""
|
"""
|
||||||
if type(color) == str:
|
if type(color) == str:
|
||||||
if color[0] != "#":
|
if color[0] != "#":
|
||||||
@@ -182,13 +182,19 @@ def validateCrop(value):
|
|||||||
value[1],value[3] = value[3],value[1]
|
value[1],value[3] = value[3],value[1]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def validateObserver(observer):
|
||||||
|
if all(map(lambda m: hasattr(observer, m), ['start', 'add', 'update', 'finish'])):
|
||||||
|
return observer
|
||||||
|
else:
|
||||||
|
raise ValidationException("%r does not look like an observer" % repr(observer))
|
||||||
|
|
||||||
def make_dictValidator(keyvalidator, valuevalidator):
|
def make_dictValidator(keyvalidator, valuevalidator):
|
||||||
"""Compose and return a dict validator -- a validator that validates each
|
"""Compose and return a dict validator -- a validator that validates each
|
||||||
key and value in a dictionary.
|
key and value in a dictionary.
|
||||||
|
|
||||||
The arguments are the validator function to use for the keys, and the
|
The arguments are the validator function to use for the keys, and the
|
||||||
validator function to use for the values.
|
validator function to use for the values.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def v(d):
|
def v(d):
|
||||||
newd = util.OrderedDict()
|
newd = util.OrderedDict()
|
||||||
|
|||||||
@@ -100,10 +100,10 @@ Bounds = namedtuple("Bounds", ("mincol", "maxcol", "minrow", "maxrow"))
|
|||||||
# 0
|
# 0
|
||||||
# Only render tiles that have chunks with a greater mtime than the last
|
# Only render tiles that have chunks with a greater mtime than the last
|
||||||
# render timestamp, and their ancestors.
|
# render timestamp, and their ancestors.
|
||||||
#
|
#
|
||||||
# In other words, only renders parts of the map that have changed since
|
# In other words, only renders parts of the map that have changed since
|
||||||
# last render, nothing more, nothing less.
|
# last render, nothing more, nothing less.
|
||||||
#
|
#
|
||||||
# This is the fastest option, but will not detect tiles that have e.g.
|
# This is the fastest option, but will not detect tiles that have e.g.
|
||||||
# been deleted from the directory tree, or pick up where a partial
|
# been deleted from the directory tree, or pick up where a partial
|
||||||
# interrupted render left off.
|
# interrupted render left off.
|
||||||
@@ -111,10 +111,10 @@ Bounds = namedtuple("Bounds", ("mincol", "maxcol", "minrow", "maxrow"))
|
|||||||
# 1
|
# 1
|
||||||
# For render-tiles, render all whose chunks have an mtime greater than
|
# For render-tiles, render all whose chunks have an mtime greater than
|
||||||
# the mtime of the tile on disk, and their composite-tile ancestors.
|
# the mtime of the tile on disk, and their composite-tile ancestors.
|
||||||
#
|
#
|
||||||
# Also check all other composite-tiles and render any that have children
|
# Also check all other composite-tiles and render any that have children
|
||||||
# with more rencent mtimes than itself.
|
# with more rencent mtimes than itself.
|
||||||
#
|
#
|
||||||
# This is slower due to stat calls to determine tile mtimes, but safe if
|
# This is slower due to stat calls to determine tile mtimes, but safe if
|
||||||
# the last render was interrupted.
|
# the last render was interrupted.
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class TileSet(object):
|
|||||||
|
|
||||||
outputdir is the absolute path to the tile output directory where the
|
outputdir is the absolute path to the tile output directory where the
|
||||||
tiles are saved. It is created if it doesn't exist
|
tiles are saved. It is created if it doesn't exist
|
||||||
|
|
||||||
Current valid options for the options dictionary are shown below. All
|
Current valid options for the options dictionary are shown below. All
|
||||||
the options must be specified unless they are not relevant. If the
|
the options must be specified unless they are not relevant. If the
|
||||||
given options do not conform to the specifications, behavior is
|
given options do not conform to the specifications, behavior is
|
||||||
@@ -200,10 +200,10 @@ class TileSet(object):
|
|||||||
0
|
0
|
||||||
Only render tiles that have chunks with a greater mtime than
|
Only render tiles that have chunks with a greater mtime than
|
||||||
the last render timestamp, and their ancestors.
|
the last render timestamp, and their ancestors.
|
||||||
|
|
||||||
In other words, only renders parts of the map that have changed
|
In other words, only renders parts of the map that have changed
|
||||||
since last render, nothing more, nothing less.
|
since last render, nothing more, nothing less.
|
||||||
|
|
||||||
This is the fastest option, but will not detect tiles that have
|
This is the fastest option, but will not detect tiles that have
|
||||||
e.g. been deleted from the directory tree, or pick up where a
|
e.g. been deleted from the directory tree, or pick up where a
|
||||||
partial interrupted render left off.
|
partial interrupted render left off.
|
||||||
@@ -212,13 +212,13 @@ class TileSet(object):
|
|||||||
"check-tiles" mode. For render-tiles, render all whose chunks
|
"check-tiles" mode. For render-tiles, render all whose chunks
|
||||||
have an mtime greater than the mtime of the tile on disk, and
|
have an mtime greater than the mtime of the tile on disk, and
|
||||||
their upper-tile ancestors.
|
their upper-tile ancestors.
|
||||||
|
|
||||||
Also check all other upper-tiles and render any that have
|
Also check all other upper-tiles and render any that have
|
||||||
children with more rencent mtimes than itself.
|
children with more rencent mtimes than itself.
|
||||||
|
|
||||||
Also remove tiles and directory trees that do exist but
|
Also remove tiles and directory trees that do exist but
|
||||||
shouldn't.
|
shouldn't.
|
||||||
|
|
||||||
This is slower due to stat calls to determine tile mtimes, but
|
This is slower due to stat calls to determine tile mtimes, but
|
||||||
safe if the last render was interrupted.
|
safe if the last render was interrupted.
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ class TileSet(object):
|
|||||||
rendermode
|
rendermode
|
||||||
Perhaps the most important/relevant option: a string indicating the
|
Perhaps the most important/relevant option: a string indicating the
|
||||||
render mode to render. This rendermode must have already been
|
render mode to render. This rendermode must have already been
|
||||||
registered with the C extension module.
|
registered with the C extension module.
|
||||||
|
|
||||||
rerenderprob
|
rerenderprob
|
||||||
A floating point number between 0 and 1 indicating the probability
|
A floating point number between 0 and 1 indicating the probability
|
||||||
@@ -319,9 +319,9 @@ class TileSet(object):
|
|||||||
"to date.",
|
"to date.",
|
||||||
self.options['name'],
|
self.options['name'],
|
||||||
)
|
)
|
||||||
logging.warning("You won't get percentage progress for "+
|
logging.warning("The total tile count will be (possibly "+
|
||||||
"this run only, because I don't know how many tiles "+
|
"wildly) inaccurate, because I don't know how many "+
|
||||||
"need rendering. I'll be checking them as I go")
|
"tiles need rendering. I'll be checking them as I go")
|
||||||
self.options['renderchecks'] = 1
|
self.options['renderchecks'] = 1
|
||||||
else:
|
else:
|
||||||
logging.debug("No rendercheck mode specified for %s. "+
|
logging.debug("No rendercheck mode specified for %s. "+
|
||||||
@@ -389,7 +389,7 @@ class TileSet(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def get_phase_length(self, phase):
|
def get_phase_length(self, phase):
|
||||||
"""Returns the number of work items in a given phase, or None if there
|
"""Returns the number of work items in a given phase, or None if there
|
||||||
is no good estimate.
|
is no good estimate.
|
||||||
@@ -397,7 +397,8 @@ class TileSet(object):
|
|||||||
# Yeah functional programming!
|
# Yeah functional programming!
|
||||||
return {
|
return {
|
||||||
0: lambda: self.dirtytree.count_all(),
|
0: lambda: self.dirtytree.count_all(),
|
||||||
1: lambda: None,
|
#there is no good way to guess this so just give total count
|
||||||
|
1: lambda: (4**(self.treedepth+1)-1)/3,
|
||||||
2: lambda: self.dirtytree.count_all(),
|
2: lambda: self.dirtytree.count_all(),
|
||||||
}[self.options['renderchecks']]()
|
}[self.options['renderchecks']]()
|
||||||
|
|
||||||
@@ -514,7 +515,7 @@ class TileSet(object):
|
|||||||
path = self.options.get('name'),
|
path = self.options.get('name'),
|
||||||
base = '',
|
base = '',
|
||||||
bgcolor = bgcolorformat(self.options.get('bgcolor')),
|
bgcolor = bgcolorformat(self.options.get('bgcolor')),
|
||||||
world = self.options.get('worldname_orig') +
|
world = self.options.get('worldname_orig') +
|
||||||
(" - " + self.options.get('dimension') if self.options.get('dimension') != 'default' else ''),
|
(" - " + self.options.get('dimension') if self.options.get('dimension') != 'default' else ''),
|
||||||
last_rendertime = self.max_chunk_mtime,
|
last_rendertime = self.max_chunk_mtime,
|
||||||
imgextension = self.imgextension,
|
imgextension = self.imgextension,
|
||||||
@@ -586,7 +587,7 @@ class TileSet(object):
|
|||||||
curdepth = self.config['zoomLevels']
|
curdepth = self.config['zoomLevels']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if curdepth == 1:
|
if curdepth == 1:
|
||||||
# Skip a depth 1 tree. A depth 1 tree pretty much can't happen, so
|
# Skip a depth 1 tree. A depth 1 tree pretty much can't happen, so
|
||||||
# when we detect this it usually means the tree is actually empty
|
# when we detect this it usually means the tree is actually empty
|
||||||
@@ -726,7 +727,7 @@ class TileSet(object):
|
|||||||
|
|
||||||
if chunkmtime > max_chunk_mtime:
|
if chunkmtime > max_chunk_mtime:
|
||||||
max_chunk_mtime = chunkmtime
|
max_chunk_mtime = chunkmtime
|
||||||
|
|
||||||
# Convert to diagonal coordinates
|
# Convert to diagonal coordinates
|
||||||
chunkcol, chunkrow = convert_coords(chunkx, chunkz)
|
chunkcol, chunkrow = convert_coords(chunkx, chunkz)
|
||||||
|
|
||||||
@@ -774,7 +775,7 @@ class TileSet(object):
|
|||||||
dirty.add(tile.path)
|
dirty.add(tile.path)
|
||||||
|
|
||||||
t = int(time.time()-stime)
|
t = int(time.time()-stime)
|
||||||
logging.debug("Finished chunk scan for %s. %s chunks scanned in %s second%s",
|
logging.debug("Finished chunk scan for %s. %s chunks scanned in %s second%s",
|
||||||
self.options['name'], chunkcount, t,
|
self.options['name'], chunkcount, t,
|
||||||
"s" if t != 1 else "")
|
"s" if t != 1 else "")
|
||||||
|
|
||||||
@@ -844,10 +845,10 @@ class TileSet(object):
|
|||||||
|
|
||||||
# Create the actual image now
|
# Create the actual image now
|
||||||
img = Image.new("RGBA", (384, 384), self.options['bgcolor'])
|
img = Image.new("RGBA", (384, 384), self.options['bgcolor'])
|
||||||
|
|
||||||
# we'll use paste (NOT alpha_over) for quadtree generation because
|
# we'll use paste (NOT alpha_over) for quadtree generation because
|
||||||
# this is just straight image stitching, not alpha blending
|
# this is just straight image stitching, not alpha blending
|
||||||
|
|
||||||
for path in quadPath_filtered:
|
for path in quadPath_filtered:
|
||||||
try:
|
try:
|
||||||
quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
|
quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
|
||||||
@@ -866,7 +867,7 @@ class TileSet(object):
|
|||||||
img.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0)
|
img.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0)
|
||||||
else: # png
|
else: # png
|
||||||
img.save(tmppath, "png")
|
img.save(tmppath, "png")
|
||||||
|
|
||||||
if self.options['optimizeimg']:
|
if self.options['optimizeimg']:
|
||||||
optimize_image(tmppath, imgformat, self.options['optimizeimg'])
|
optimize_image(tmppath, imgformat, self.options['optimizeimg'])
|
||||||
|
|
||||||
@@ -962,7 +963,7 @@ class TileSet(object):
|
|||||||
## Which chunk this is:
|
## Which chunk this is:
|
||||||
#draw.text((96,48), "C: %s,%s" % (chunkx, chunkz), fill='red')
|
#draw.text((96,48), "C: %s,%s" % (chunkx, chunkz), fill='red')
|
||||||
#draw.text((96,96), "c,r: %s,%s" % (col, row), fill='red')
|
#draw.text((96,96), "c,r: %s,%s" % (col, row), fill='red')
|
||||||
|
|
||||||
# Save them
|
# Save them
|
||||||
with FileReplacer(imgpath) as tmppath:
|
with FileReplacer(imgpath) as tmppath:
|
||||||
if self.imgextension == 'jpg':
|
if self.imgextension == 'jpg':
|
||||||
@@ -1015,7 +1016,7 @@ class TileSet(object):
|
|||||||
if e.errno != errno.ENOENT:
|
if e.errno != errno.ENOENT:
|
||||||
raise
|
raise
|
||||||
tile_mtime = 0
|
tile_mtime = 0
|
||||||
|
|
||||||
max_chunk_mtime = max(c[5] for c in get_chunks_by_tile(tileobj, self.regionset))
|
max_chunk_mtime = max(c[5] for c in get_chunks_by_tile(tileobj, self.regionset))
|
||||||
|
|
||||||
if tile_mtime > 120 + max_chunk_mtime:
|
if tile_mtime > 120 + max_chunk_mtime:
|
||||||
@@ -1041,7 +1042,7 @@ class TileSet(object):
|
|||||||
# This doesn't need rendering. Return mtime to parent in case
|
# This doesn't need rendering. Return mtime to parent in case
|
||||||
# its mtime is less, indicating the parent DOES need a render
|
# its mtime is less, indicating the parent DOES need a render
|
||||||
yield path, max_chunk_mtime, False
|
yield path, max_chunk_mtime, False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# A composite-tile.
|
# A composite-tile.
|
||||||
render_me = False
|
render_me = False
|
||||||
@@ -1134,7 +1135,7 @@ def convert_coords(chunkx, chunkz):
|
|||||||
"""Takes a coordinate (chunkx, chunkz) where chunkx and chunkz are
|
"""Takes a coordinate (chunkx, chunkz) where chunkx and chunkz are
|
||||||
in the chunk coordinate system, and figures out the row and column
|
in the chunk coordinate system, and figures out the row and column
|
||||||
in the image each one should be. Returns (col, row)."""
|
in the image each one should be. Returns (col, row)."""
|
||||||
|
|
||||||
# columns are determined by the sum of the chunk coords, rows are the
|
# columns are determined by the sum of the chunk coords, rows are the
|
||||||
# difference
|
# difference
|
||||||
# change this function, and you MUST change unconvert_coords
|
# change this function, and you MUST change unconvert_coords
|
||||||
@@ -1142,7 +1143,7 @@ def convert_coords(chunkx, chunkz):
|
|||||||
|
|
||||||
def unconvert_coords(col, row):
|
def unconvert_coords(col, row):
|
||||||
"""Undoes what convert_coords does. Returns (chunkx, chunkz)."""
|
"""Undoes what convert_coords does. Returns (chunkx, chunkz)."""
|
||||||
|
|
||||||
# col + row = chunkz + chunkz => (col + row)/2 = chunkz
|
# col + row = chunkz + chunkz => (col + row)/2 = chunkz
|
||||||
# col - row = chunkx + chunkx => (col - row)/2 = chunkx
|
# col - row = chunkx + chunkx => (col - row)/2 = chunkx
|
||||||
return ((col - row) / 2, (col + row) / 2)
|
return ((col - row) / 2, (col + row) / 2)
|
||||||
@@ -1156,7 +1157,7 @@ def unconvert_coords(col, row):
|
|||||||
def get_tiles_by_chunk(chunkcol, chunkrow):
|
def get_tiles_by_chunk(chunkcol, chunkrow):
|
||||||
"""For the given chunk, returns an iterator over Render Tiles that this
|
"""For the given chunk, returns an iterator over Render Tiles that this
|
||||||
chunk touches. Iterates over (tilecol, tilerow)
|
chunk touches. Iterates over (tilecol, tilerow)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# find tile coordinates. Remember tiles are identified by the
|
# find tile coordinates. Remember tiles are identified by the
|
||||||
# address of the chunk in their upper left corner.
|
# address of the chunk in their upper left corner.
|
||||||
@@ -1187,7 +1188,7 @@ def get_chunks_by_tile(tile, regionset):
|
|||||||
|
|
||||||
This function is expected to return the chunk sections in the correct order
|
This function is expected to return the chunk sections in the correct order
|
||||||
for rendering, i.e. back to front.
|
for rendering, i.e. back to front.
|
||||||
|
|
||||||
Returns an iterator over chunks tuples where each item is
|
Returns an iterator over chunks tuples where each item is
|
||||||
(col, row, chunkx, chunky, chunkz, mtime)
|
(col, row, chunkx, chunky, chunkz, mtime)
|
||||||
"""
|
"""
|
||||||
@@ -1238,7 +1239,7 @@ class RendertileSet(object):
|
|||||||
"""This object holds a set of render-tiles using a quadtree data structure.
|
"""This object holds a set of render-tiles using a quadtree data structure.
|
||||||
It is typically used to hold tiles that need rendering. This implementation
|
It is typically used to hold tiles that need rendering. This implementation
|
||||||
collapses subtrees that are completely in or out of the set to save memory.
|
collapses subtrees that are completely in or out of the set to save memory.
|
||||||
|
|
||||||
Each instance of this class is a node in the tree, and therefore each
|
Each instance of this class is a node in the tree, and therefore each
|
||||||
instance is the root of a subtree.
|
instance is the root of a subtree.
|
||||||
|
|
||||||
@@ -1251,7 +1252,7 @@ class RendertileSet(object):
|
|||||||
level; level 1 nodes keep track of leaf image state. Level 2 nodes keep
|
level; level 1 nodes keep track of leaf image state. Level 2 nodes keep
|
||||||
track of level 1 state, and so fourth.
|
track of level 1 state, and so fourth.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__slots__ = ("depth", "children")
|
__slots__ = ("depth", "children")
|
||||||
def __init__(self, depth):
|
def __init__(self, depth):
|
||||||
@@ -1323,10 +1324,10 @@ class RendertileSet(object):
|
|||||||
|
|
||||||
def add(self, path):
|
def add(self, path):
|
||||||
"""Marks the requested leaf node as in this set
|
"""Marks the requested leaf node as in this set
|
||||||
|
|
||||||
Path is an iterable of integers representing the path to the leaf node
|
Path is an iterable of integers representing the path to the leaf node
|
||||||
that is to be added to the set
|
that is to be added to the set
|
||||||
|
|
||||||
"""
|
"""
|
||||||
path = list(path)
|
path = list(path)
|
||||||
assert len(path) == self.depth
|
assert len(path) == self.depth
|
||||||
@@ -1591,7 +1592,7 @@ class RenderTile(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compute_path(cls, col, row, depth):
|
def compute_path(cls, col, row, depth):
|
||||||
"""Constructor that takes a col,row of a tile and computes the path.
|
"""Constructor that takes a col,row of a tile and computes the path.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert col % 2 == 0
|
assert col % 2 == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user