Merge branch 'master' into snapshot
This commit is contained in:
116
docs/config.rst
116
docs/config.rst
@@ -545,18 +545,92 @@ values. The valid configuration keys are listed below.
|
||||
**Default:** ``95``
|
||||
|
||||
``optimizeimg``
|
||||
|
||||
.. warning::
|
||||
Using image optimizers will increase render times significantly.
|
||||
|
||||
This option specifies which additional tools overviewer should use to
|
||||
optimize the filesize of png tiles.
|
||||
The tools used must be placed somewhere, where overviewer can find them, for
|
||||
example the "PATH" environment variable or a directory like /usr/bin.
|
||||
This should be an integer between 0 and 3.
|
||||
* ``1 - Use pngcrush``
|
||||
* ``2 - Use advdef``
|
||||
* ``3 - Use pngcrush and advdef (Not recommended)``
|
||||
Using this option may significantly increase render time, but will make
|
||||
the resulting tiles smaller, with lossless image quality.
|
||||
|
||||
**Default:** ``0``
|
||||
The option is a list of Optimizer objects, which are then executed in
|
||||
the order in which they're specified::
|
||||
|
||||
from optimizeimages import pngnq, optipng
|
||||
worlds["world"] = "/path/to/world"
|
||||
|
||||
renders["daytime"] = {
|
||||
"world":"world",
|
||||
"title":"day",
|
||||
"rendermode":smooth_lighting,
|
||||
"optimizeimg":[pngnq(sampling=1), optipng(olevel=3)],
|
||||
}
|
||||
|
||||
Here is a list of supported image optimization programs:
|
||||
|
||||
``pngnq``
|
||||
pngnq quantizes 32-bit RGBA images into 8-bit RGBA palette PNGs. This is
|
||||
lossy, but reduces filesize significantly. Available settings:
|
||||
|
||||
``sampling``
|
||||
An integer between ``1`` and ``10``, ``1`` samples all pixels, is slow and yields
|
||||
the best quality. Higher values sample less of the image, which makes
|
||||
the process faster, but less accurate.
|
||||
|
||||
**Default:** ``3``
|
||||
|
||||
``dither``
|
||||
Either the string ``"n"`` for no dithering, or ``"f"`` for Floyd
|
||||
Steinberg dithering. Dithering helps eliminate colorbanding, sometimes
|
||||
increasing visual quality.
|
||||
|
||||
.. warning::
|
||||
With pngnq version 1.0 (which is what Ubuntu 12.04 ships), the
|
||||
dithering option is broken. Only the default, no dithering,
|
||||
can be specified on those systems.
|
||||
|
||||
**Default:** ``"n"``
|
||||
|
||||
.. warning::
|
||||
Because of several PIL bugs, only the most zoomed in level has transparency
|
||||
when using pngnq. The other zoom levels have all transparency replaced by
|
||||
black. This is *not* pngnq's fault, as pngnq supports multiple levels of
|
||||
transparency just fine, it's PIL's fault for not even reading indexed
|
||||
PNGs correctly.
|
||||
|
||||
``optipng``
|
||||
optipng tunes the deflate algorithm and removes unneeded channels from the PNG,
|
||||
producing a smaller, lossless output image. It was inspired by pngcrush.
|
||||
Available settings:
|
||||
|
||||
``olevel``
|
||||
An integer between ``0`` (few optimizations) and ``7`` (many optimizations).
|
||||
The default should be satisfactory for everyone, higher levels than the default
|
||||
see almost no benefit.
|
||||
|
||||
**Default:** ``2``
|
||||
|
||||
``pngcrush``
|
||||
pngcrush is very slow and not very good, you should use optipng in probably all cases.
|
||||
However, Overviewer still allows you to use it because we're nice people like that.
|
||||
Available settings:
|
||||
|
||||
``brute``
|
||||
Either ``True`` or ``False``. Cycles through all compression methods, and is very slow.
|
||||
|
||||
.. note::
|
||||
There is practically no reason to ever use this. optipng will beat pngcrush, and
|
||||
throwing more CPU time at pngcrush most likely won't help. If you think you need
|
||||
this option, then you are most likely wrong.
|
||||
|
||||
**Default:** ``False``
|
||||
|
||||
.. note::
|
||||
Don't forget to import the optimizers you use in your settings file, as shown in the
|
||||
example above.
|
||||
|
||||
**Default:** ``[]``
|
||||
|
||||
``bgcolor``
|
||||
This is the background color to be displayed behind the map. Its value
|
||||
@@ -566,13 +640,22 @@ values. The valid configuration keys are listed below.
|
||||
**Default:** ``#1a1a1a``
|
||||
|
||||
``defaultzoom``
|
||||
This value specifies the default zoom level that the map will be opened
|
||||
with. It has to be greater than 0.
|
||||
This value specifies the default zoom level that the map will be
|
||||
opened with. It has to be greater than 0, which corresponds to the
|
||||
most zoomed-out level. If you use ``minzoom`` or ``maxzoom``, it
|
||||
should be between those two.
|
||||
|
||||
**Default:** ``1``
|
||||
|
||||
``maxzoom``
|
||||
This specifies the maximum zoom allowed by the zoom control on the web page.
|
||||
This specifies the maximum, closest in zoom allowed by the zoom
|
||||
control on the web page. This is relative to 0, the farthest-out
|
||||
image, so setting this to 8 will allow you to zoom in at most 8
|
||||
times. This is *not* relative to ``minzoom``, so setting
|
||||
``minzoom`` will shave off even more levels. If you wish to
|
||||
specify how many zoom levels to leave off, instead of how many
|
||||
total to use, use a negative number here. For example, setting
|
||||
this to -2 will disable the two most zoomed-in levels.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -583,8 +666,9 @@ values. The valid configuration keys are listed below.
|
||||
**Default:** Automatically set to most detailed zoom level
|
||||
|
||||
``minzoom``
|
||||
This specifies the minimum zoom allowed by the zoom control on the web page. For
|
||||
example, setting this to 2 will disable the two most-zoomed out levels.
|
||||
This specifies the minimum, farthest away zoom allowed by the zoom
|
||||
control on the web page. For example, setting this to 2 will
|
||||
disable the two most zoomed-out levels.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -694,6 +778,14 @@ values. The valid configuration keys are listed below.
|
||||
'forcerender': True,
|
||||
}
|
||||
|
||||
``renderchecks``
|
||||
This is an integer, and functions as a more complex form of
|
||||
``forcerender``. Setting it to 1 enables :option:`--check-tiles`
|
||||
mode, setting it to 2 enables :option:`--forcerender`, and 3 tells
|
||||
Overviewer to keep this particular render in the output, but
|
||||
otherwise don't update it. It defaults to 0, which is the usual
|
||||
update checking mode.
|
||||
|
||||
``changelist``
|
||||
This is a string. It names a file where it will write out, one per line, the
|
||||
path to tiles that have been updated. You can specify the same file for
|
||||
|
||||
@@ -84,6 +84,8 @@ def main():
|
||||
help="Tries to locate the texture files. Useful for debugging texture problems.")
|
||||
parser.add_option("-V", "--version", dest="version",
|
||||
help="Displays version information and then exits", action="store_true")
|
||||
parser.add_option("--check-version", dest="checkversion",
|
||||
help="Fetchs information about the latest version of Overviewer", action="store_true")
|
||||
parser.add_option("--update-web-assets", dest='update_web_assets', action="store_true",
|
||||
help="Update web assets. Will *not* render tiles or update overviewerConfig.js")
|
||||
|
||||
@@ -141,8 +143,28 @@ def main():
|
||||
if options.verbose > 0:
|
||||
print("Python executable: %r" % sys.executable)
|
||||
print(sys.version)
|
||||
if not options.checkversion:
|
||||
return 0
|
||||
if options.checkversion:
|
||||
print("Currently running Minecraft Overviewer %s" % util.findGitVersion()),
|
||||
print("(%s)" % util.findGitHash()[:7])
|
||||
try:
|
||||
import urllib
|
||||
import json
|
||||
latest_ver = json.loads(urllib.urlopen("http://overviewer.org/download.json").read())['src']
|
||||
print("Latest version of Minecraft Overviewer %s (%s)" % (latest_ver['version'], latest_ver['commit'][:7]))
|
||||
print("See http://overviewer.org/downloads for more information")
|
||||
except Exception:
|
||||
print("Failed to fetch latest version info.")
|
||||
if options.verbose > 0:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("Re-run with --verbose for more details")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if options.pid:
|
||||
if os.path.exists(options.pid):
|
||||
try:
|
||||
@@ -318,19 +340,24 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
||||
"--check-tiles, and --no-tile-checks. These options conflict.")
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
def set_renderchecks(checkname, num):
|
||||
for name, render in config['renders'].iteritems():
|
||||
if render.get('renderchecks', 0) == 3:
|
||||
logging.warning(checkname + " ignoring render " + repr(name) + " since it's marked as \"don't render\".")
|
||||
else:
|
||||
render['renderchecks'] = num
|
||||
|
||||
if options.forcerender:
|
||||
logging.info("Forcerender mode activated. ALL tiles will be rendered")
|
||||
for render in config['renders'].itervalues():
|
||||
render['renderchecks'] = 2
|
||||
set_renderchecks("forcerender", 2)
|
||||
elif options.checktiles:
|
||||
logging.info("Checking all tiles for updates manually.")
|
||||
for render in config['renders'].itervalues():
|
||||
render['renderchecks'] = 1
|
||||
set_renderchecks("checktiles", 1)
|
||||
elif options.notilechecks:
|
||||
logging.info("Disabling all tile mtime checks. Only rendering tiles "+
|
||||
"that need updating since last render")
|
||||
for render in config['renders'].itervalues():
|
||||
render['renderchecks'] = 0
|
||||
set_renderchecks("notilechecks", 0)
|
||||
|
||||
if not config['renders']:
|
||||
logging.error("You must specify at least one render in your config file. See the docs if you're having trouble")
|
||||
@@ -514,6 +541,8 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
|
||||
if options.pid:
|
||||
os.remove(options.pid)
|
||||
|
||||
logging.info("Your render has been written to '%s', open index.html to view it" % destdir)
|
||||
|
||||
return 0
|
||||
|
||||
def list_worlds():
|
||||
|
||||
@@ -19,6 +19,7 @@ import logging
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import urllib2
|
||||
import Queue
|
||||
import multiprocessing
|
||||
|
||||
@@ -30,6 +31,8 @@ from overviewer_core import logger
|
||||
from overviewer_core import nbt
|
||||
from overviewer_core import configParser, world
|
||||
|
||||
UUID_LOOKUP_URL = 'https://sessionserver.mojang.com/session/minecraft/profile/'
|
||||
|
||||
def replaceBads(s):
|
||||
"Replaces bad characters with good characters!"
|
||||
bads = [" ", "(", ")"]
|
||||
@@ -132,7 +135,12 @@ def handlePlayers(rset, render, worldpath):
|
||||
dimension = int(mystdim.group(1))
|
||||
else:
|
||||
raise
|
||||
playerdir = os.path.join(worldpath, "players")
|
||||
playerdir = os.path.join(worldpath, "playerdata")
|
||||
useUUIDs = True
|
||||
if not os.path.isdir(playerdir):
|
||||
playerdir = os.path.join(worldpath, "players")
|
||||
useUUIDs = False
|
||||
|
||||
if os.path.isdir(playerdir):
|
||||
playerfiles = os.listdir(playerdir)
|
||||
playerfiles = [x for x in playerfiles if x.endswith(".dat")]
|
||||
@@ -152,6 +160,13 @@ def handlePlayers(rset, render, worldpath):
|
||||
logging.warning("Skipping bad player dat file %r", playerfile)
|
||||
continue
|
||||
playername = playerfile.split(".")[0]
|
||||
if useUUIDs:
|
||||
try:
|
||||
profile = json.loads(urllib2.urlopen(UUID_LOOKUP_URL + playername.replace('-','')).read())
|
||||
if 'name' in profile:
|
||||
playername = profile['name']
|
||||
except (ValueError, urllib2.URLError):
|
||||
logging.warning("Unable to get player name for UUID %s", playername)
|
||||
if isSinglePlayer:
|
||||
playername = 'Player'
|
||||
if data['Dimension'] == dimension:
|
||||
|
||||
@@ -119,7 +119,7 @@ overviewer.util = {
|
||||
zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
|
||||
} else {
|
||||
zoom = parseInt(zoom);
|
||||
if (zoom < 0 && zoom + overviewer.mapView.options.currentTileSet.get('maxZoom') >= 0) {
|
||||
if (zoom < 0) {
|
||||
// if zoom is negative, treat it as a "zoom out from max"
|
||||
zoom += overviewer.mapView.options.currentTileSet.get('maxZoom');
|
||||
} else {
|
||||
@@ -127,6 +127,13 @@ overviewer.util = {
|
||||
zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom');
|
||||
}
|
||||
}
|
||||
|
||||
// clip zoom
|
||||
if (zoom > overviewer.mapView.options.currentTileSet.get('maxZoom'))
|
||||
zoom = overviewer.mapView.options.currentTileSet.get('maxZoom');
|
||||
if (zoom < overviewer.mapView.options.currentTileSet.get('minZoom'))
|
||||
zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
|
||||
|
||||
overviewer.map.setZoom(zoom);
|
||||
}
|
||||
|
||||
@@ -512,9 +519,9 @@ overviewer.util = {
|
||||
}
|
||||
|
||||
|
||||
if (zoom == currTileset.get('maxZoom')) {
|
||||
if (zoom >= currTileset.get('maxZoom')) {
|
||||
zoom = 'max';
|
||||
} else if (zoom == currTileset.get('minZoom')) {
|
||||
} else if (zoom <= currTileset.get('minZoom')) {
|
||||
zoom = 'min';
|
||||
} else {
|
||||
// default to (map-update friendly) negative zooms
|
||||
@@ -556,7 +563,7 @@ overviewer.util = {
|
||||
zoom = tsetModel.get('minZoom');
|
||||
} else {
|
||||
zoom = parseInt(zoom);
|
||||
if (zoom < 0 && zoom + tsetModel.get('maxZoom') >= 0) {
|
||||
if (zoom < 0) {
|
||||
// if zoom is negative, treat it as a "zoom out from max"
|
||||
zoom += tsetModel.get('maxZoom');
|
||||
} else {
|
||||
@@ -565,6 +572,12 @@ overviewer.util = {
|
||||
}
|
||||
}
|
||||
|
||||
// clip zoom
|
||||
if (zoom > tsetModel.get('maxZoom'))
|
||||
zoom = tsetModel.get('maxZoom');
|
||||
if (zoom < tsetModel.get('minZoom'))
|
||||
zoom = tsetModel.get('minZoom');
|
||||
|
||||
overviewer.map.setCenter(latlngcoords);
|
||||
overviewer.map.setZoom(zoom);
|
||||
var locationmarker = new overviewer.views.LocationIconView();
|
||||
|
||||
@@ -345,7 +345,7 @@ class ServerAnnounceObserver(Observer):
|
||||
|
||||
def update(self, current_value):
|
||||
super(ServerAnnounceObserver, self).update(current_value)
|
||||
if self._need_update(current_value):
|
||||
if self._need_update():
|
||||
self._send_output('Rendered %d of %d tiles, %d%% complete' %
|
||||
(self.get_current_value(), self.get_max_value(),
|
||||
self.get_percentage()))
|
||||
|
||||
@@ -16,37 +16,103 @@
|
||||
import os
|
||||
import subprocess
|
||||
import shlex
|
||||
import logging
|
||||
|
||||
pngcrush = "pngcrush"
|
||||
optipng = "optipng"
|
||||
advdef = "advdef"
|
||||
class Optimizer:
|
||||
binaryname = ""
|
||||
|
||||
def check_programs(level):
|
||||
path = os.environ.get("PATH").split(os.pathsep)
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
def exists_in_path(prog):
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
return len(result) != 0
|
||||
def optimize(self, img):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
for prog,l in [(pngcrush,1), (advdef,2)]:
|
||||
if l <= level:
|
||||
if (not exists_in_path(prog)) and (not exists_in_path(prog + ".exe")):
|
||||
raise Exception("Optimization prog %s for level %d not found!" % (prog, l))
|
||||
def fire_and_forget(self, args):
|
||||
subprocess.check_call(args)
|
||||
|
||||
def optimize_image(imgpath, imgformat, optimizeimg):
|
||||
if imgformat == 'png':
|
||||
if optimizeimg >= 1:
|
||||
# we can't do an atomic replace here because windows is terrible
|
||||
# so instead, we make temp files, delete the old ones, and rename
|
||||
# the temp files. go windows!
|
||||
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
|
||||
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
|
||||
os.remove(imgpath)
|
||||
os.rename(imgpath+".tmp", imgpath)
|
||||
def check_availability(self):
|
||||
path = os.environ.get("PATH").split(os.pathsep)
|
||||
|
||||
if optimizeimg >= 2:
|
||||
# the "-nc" it's needed to no broke the transparency of tiles
|
||||
recompress_option = "-z2" if optimizeimg == 2 else "-z4"
|
||||
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE).communicate()[0]
|
||||
def exists_in_path(prog):
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
return len(result) != 0
|
||||
|
||||
if (not exists_in_path(self.binaryname)) and (not exists_in_path(self.binaryname + ".exe")):
|
||||
raise Exception("Optimization program '%s' was not found!" % self.binaryname)
|
||||
|
||||
|
||||
class NonAtomicOptimizer(Optimizer):
|
||||
def cleanup(self, img):
|
||||
os.rename(img + ".tmp", img)
|
||||
|
||||
def fire_and_forget(self, args, img):
|
||||
subprocess.check_call(args)
|
||||
self.cleanup(img)
|
||||
|
||||
class PNGOptimizer:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
class JPEGOptimizer:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
class pngnq(NonAtomicOptimizer, PNGOptimizer):
|
||||
binaryname = "pngnq"
|
||||
|
||||
def __init__(self, sampling=3, dither="n"):
|
||||
if sampling < 1 or sampling > 10:
|
||||
raise Exception("Invalid sampling value '%d' for pngnq!" % sampling)
|
||||
|
||||
if dither not in ["n", "f"]:
|
||||
raise Exception("Invalid dither method '%s' for pngnq!" % dither)
|
||||
|
||||
self.sampling = sampling
|
||||
self.dither = dither
|
||||
|
||||
def optimize(self, img):
|
||||
if img.endswith(".tmp"):
|
||||
extension = ".tmp"
|
||||
else:
|
||||
extension = ".png.tmp"
|
||||
|
||||
args = [self.binaryname, "-s", str(self.sampling), "-f", "-e", extension, img]
|
||||
# Workaround for poopbuntu 12.04 which ships an old broken pngnq
|
||||
if self.dither != "n":
|
||||
args.insert(1, "-Q")
|
||||
args.insert(2, self.dither)
|
||||
|
||||
NonAtomicOptimizer.fire_and_forget(self, args, img)
|
||||
|
||||
class pngcrush(NonAtomicOptimizer, PNGOptimizer):
|
||||
binaryname = "pngcrush"
|
||||
# really can't be bothered to add some interface for all
|
||||
# the pngcrush options, it sucks anyway
|
||||
def __init__(self, brute=False):
|
||||
self.brute = brute
|
||||
|
||||
def optimize(self, img):
|
||||
args = [self.binaryname, img, img + ".tmp"]
|
||||
if self.brute == True: # Was the user an idiot?
|
||||
args.insert(1, "-brute")
|
||||
|
||||
NonAtomicOptimizer.fire_and_forget(self, args, img)
|
||||
|
||||
class optipng(Optimizer, PNGOptimizer):
|
||||
binaryname = "optipng"
|
||||
|
||||
def __init__(self, olevel=2):
|
||||
self.olevel = olevel
|
||||
|
||||
def optimize(self, img):
|
||||
Optimizer.fire_and_forget(self, [self.binaryname, "-o" + str(self.olevel), "-quiet", img])
|
||||
|
||||
|
||||
def optimize_image(imgpath, imgformat, optimizers):
|
||||
for opt in optimizers:
|
||||
if imgformat == 'png':
|
||||
if isinstance(opt, PNGOptimizer):
|
||||
opt.optimize(imgpath)
|
||||
elif imgformat == 'jpg':
|
||||
if isinstance(opt, JPEGOptimizer):
|
||||
opt.optimize(imgpath)
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
from settingsValidators import *
|
||||
import util
|
||||
from observer import ProgressBarObserver, LoggingObserver, JSObserver
|
||||
from optimizeimages import pngnq, optipng, pngcrush
|
||||
import platform
|
||||
import sys
|
||||
|
||||
@@ -72,7 +73,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
|
||||
"imgquality": Setting(required=False, validator=validateImgQuality, default=95),
|
||||
"bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"),
|
||||
"defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1),
|
||||
"optimizeimg": Setting(required=True, validator=validateOptImg, default=0),
|
||||
"optimizeimg": Setting(required=True, validator=validateOptImg, default=[]),
|
||||
"nomarkers": Setting(required=False, validator=validateBool, default=None),
|
||||
"texturepath": Setting(required=False, validator=validateTexturePath, default=None),
|
||||
"renderchecks": Setting(required=False, validator=validateInt, default=None),
|
||||
|
||||
@@ -5,7 +5,9 @@ from collections import namedtuple
|
||||
|
||||
import rendermodes
|
||||
import util
|
||||
from optimizeimages import Optimizer
|
||||
from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT
|
||||
import logging
|
||||
|
||||
class ValidationException(Exception):
|
||||
pass
|
||||
@@ -155,8 +157,20 @@ def validateBGColor(color):
|
||||
return color
|
||||
|
||||
|
||||
def validateOptImg(opt):
|
||||
return bool(opt)
|
||||
def validateOptImg(optimizers):
|
||||
if isinstance(optimizers, (int, long)):
|
||||
from optimizeimages import pngcrush
|
||||
logging.warning("You're using a deprecated definition of optimizeimg. We'll do what you say for now, but please fix this as soon as possible.")
|
||||
optimizers = [pngcrush()]
|
||||
if not isinstance(optimizers, list):
|
||||
raise ValidationException("optimizeimg is not a list. Make sure you specify them like [foo()], with square brackets.")
|
||||
for opt in optimizers:
|
||||
if not isinstance(opt, Optimizer):
|
||||
raise ValidationException("Invalid Optimizer!")
|
||||
|
||||
opt.check_availability()
|
||||
|
||||
return optimizers
|
||||
|
||||
def validateTexturePath(path):
|
||||
# Expand user dir in directories strings
|
||||
|
||||
@@ -24,6 +24,7 @@ import functools
|
||||
import time
|
||||
import errno
|
||||
import stat
|
||||
import platform
|
||||
from collections import namedtuple
|
||||
from itertools import product, izip, chain
|
||||
|
||||
@@ -129,6 +130,14 @@ Bounds = namedtuple("Bounds", ("mincol", "maxcol", "minrow", "maxrow"))
|
||||
# slowest, but SHOULD be specified if this is the first render because
|
||||
# the scan will forgo tile stat calls. It's also useful for changing
|
||||
# texture packs or other options that effect the output.
|
||||
|
||||
# 3
|
||||
# A very special mode. Using this will not actually render
|
||||
# anything, but will leave this tileset in the resulting
|
||||
# map. Useful for renders that you want to keep, but not
|
||||
# update. Since this mode is so simple, it's left out of the
|
||||
# rest of this discussion.
|
||||
|
||||
#
|
||||
# For 0 our caller has explicitly requested not to check mtimes on disk to
|
||||
# speed things up. So the mode 0 chunk scan only looks at chunk mtimes and the
|
||||
@@ -237,6 +246,13 @@ class TileSet(object):
|
||||
useful for changing texture packs or other options that effect
|
||||
the output.
|
||||
|
||||
3
|
||||
A very special mode. Using this will not actually render
|
||||
anything, but will leave this tileset in the resulting
|
||||
map. Useful for renders that you want to keep, but not
|
||||
update. Since this mode is so simple, it's left out of the
|
||||
rest of this discussion.
|
||||
|
||||
imgformat
|
||||
A string indicating the output format. Must be one of 'png' or
|
||||
'jpeg'
|
||||
@@ -246,11 +262,7 @@ class TileSet(object):
|
||||
relevant in jpeg mode.
|
||||
|
||||
optimizeimg
|
||||
an integer indiating optimizations to perform on png outputs. 0
|
||||
indicates no optimizations. Only relevant in png mode.
|
||||
1 indicates pngcrush is run on all output images
|
||||
2 indicates pngcrush and advdef are run on all output images with advdef -z2
|
||||
3 indicates pngcrush and advdef are run on all output images with advdef -z4
|
||||
A list of optimizer instances to use.
|
||||
|
||||
rendermode
|
||||
Perhaps the most important/relevant option: a string indicating the
|
||||
@@ -389,6 +401,11 @@ class TileSet(object):
|
||||
attribute for later use in iterate_work_items()
|
||||
|
||||
"""
|
||||
|
||||
# skip if we're told to
|
||||
if self.options['renderchecks'] == 3:
|
||||
return
|
||||
|
||||
# REMEMBER THAT ATTRIBUTES ASSIGNED IN THIS METHOD ARE NOT AVAILABLE IN
|
||||
# THE do_work() METHOD (because this is only called in the main process
|
||||
# not the workers)
|
||||
@@ -415,15 +432,16 @@ class TileSet(object):
|
||||
return 1
|
||||
|
||||
def get_phase_length(self, phase):
|
||||
"""Returns the number of work items in a given phase, or None if there
|
||||
is no good estimate.
|
||||
"""Returns the number of work items in a given phase.
|
||||
"""
|
||||
# Yeah functional programming!
|
||||
# and by functional we mean a bastardized python switch statement
|
||||
return {
|
||||
0: lambda: self.dirtytree.count_all(),
|
||||
#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(),
|
||||
3: lambda: 0,
|
||||
}[self.options['renderchecks']]()
|
||||
|
||||
def iterate_work_items(self, phase):
|
||||
@@ -433,6 +451,10 @@ class TileSet(object):
|
||||
This method returns an iterator over (obj, [dependencies, ...])
|
||||
"""
|
||||
|
||||
# skip if asked to
|
||||
if self.options['renderchecks'] == 3:
|
||||
return
|
||||
|
||||
# The following block of code implementes the changelist functionality.
|
||||
fd = self.options.get("changelist", None)
|
||||
if fd:
|
||||
@@ -536,6 +558,11 @@ class TileSet(object):
|
||||
return "#%02x%02x%02x" % color[0:3]
|
||||
isOverlay = self.options.get("overlay") or (not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode")))
|
||||
|
||||
# don't update last render time if we're leaving this alone
|
||||
last_rendertime = self.last_rendertime
|
||||
if self.options['renderchecks'] != 3:
|
||||
last_rendertime = self.max_chunk_mtime
|
||||
|
||||
d = dict(name = self.options.get('title'),
|
||||
zoomLevels = self.treedepth,
|
||||
defaultZoom = self.options.get('defaultzoom'),
|
||||
@@ -545,13 +572,15 @@ class TileSet(object):
|
||||
bgcolor = bgcolorformat(self.options.get('bgcolor')),
|
||||
world = self.options.get('worldname_orig') +
|
||||
(" - " + self.options.get('dimension')[0] if self.options.get('dimension')[1] != 0 else ''),
|
||||
last_rendertime = self.max_chunk_mtime,
|
||||
last_rendertime = last_rendertime,
|
||||
imgextension = self.imgextension,
|
||||
isOverlay = isOverlay,
|
||||
poititle = self.options.get("poititle"),
|
||||
showlocationmarker = self.options.get("showlocationmarker")
|
||||
)
|
||||
d['maxZoom'] = min(self.treedepth, d['maxZoom'])
|
||||
d['minZoom'] = min(max(0, self.options.get("minzoom", 0)), d['maxZoom'])
|
||||
d['defaultZoom'] = max(d['minZoom'], min(d['defaultZoom'], d['maxZoom']))
|
||||
|
||||
if isOverlay:
|
||||
d.update({"tilesets": self.options.get("overlay")})
|
||||
@@ -760,8 +789,8 @@ class TileSet(object):
|
||||
# Compare the last modified time of the chunk and tile. If the
|
||||
# tile is older, mark it in a RendertileSet object as dirty.
|
||||
|
||||
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks():
|
||||
|
||||
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks() if (markall or platform.system() == 'Windows') else self.regionset.iterate_newer_chunks(last_rendertime):
|
||||
chunkcount += 1
|
||||
|
||||
if chunkmtime > max_chunk_mtime:
|
||||
@@ -892,7 +921,11 @@ class TileSet(object):
|
||||
try:
|
||||
#quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
|
||||
src = Image.open(path[1])
|
||||
# optimizeimg may have converted them to a palette image in the meantime
|
||||
if src.mode != "RGB" and src.mode != "RGBA":
|
||||
src = src.convert("RGBA")
|
||||
src.load()
|
||||
|
||||
quad = Image.new("RGBA", (192, 192), self.options['bgcolor'])
|
||||
resize_half(quad, src)
|
||||
img.paste(quad, path[0])
|
||||
|
||||
@@ -273,7 +273,7 @@ class RegionSet(object):
|
||||
|
||||
for x, y, regionfile in self._iterate_regionfiles():
|
||||
# regionfile is a pathname
|
||||
self.regionfiles[(x,y)] = regionfile
|
||||
self.regionfiles[(x,y)] = (regionfile, os.path.getmtime(regionfile))
|
||||
|
||||
self.empty_chunk = [None,None]
|
||||
logging.debug("Done scanning regions")
|
||||
@@ -459,7 +459,7 @@ class RegionSet(object):
|
||||
|
||||
"""
|
||||
|
||||
for (regionx, regiony), regionfile in self.regionfiles.iteritems():
|
||||
for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
|
||||
try:
|
||||
mcr = self._get_regionobj(regionfile)
|
||||
except nbt.CorruptRegionError:
|
||||
@@ -468,6 +468,27 @@ class RegionSet(object):
|
||||
for chunkx, chunky in mcr.get_chunks():
|
||||
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
|
||||
|
||||
def iterate_newer_chunks(self, mtime):
|
||||
"""Returns an iterator over all chunk metadata in this world. Iterates
|
||||
over tuples of integers (x,z,mtime) for each chunk. Other chunk data
|
||||
is not returned here.
|
||||
|
||||
"""
|
||||
|
||||
for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
|
||||
""" SKIP LOADING A REGION WHICH HAS NOT BEEN MODIFIED! """
|
||||
if (filemtime < mtime):
|
||||
continue
|
||||
|
||||
try:
|
||||
mcr = self._get_regionobj(regionfile)
|
||||
except nbt.CorruptRegionError:
|
||||
logging.warning("Found a corrupt region file at %s,%s. Skipping it.", regionx, regiony)
|
||||
continue
|
||||
|
||||
for chunkx, chunky in mcr.get_chunks():
|
||||
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
|
||||
|
||||
def get_chunk_mtime(self, x, z):
|
||||
"""Returns a chunk's mtime, or False if the chunk does not exist. This
|
||||
is therefore a dual purpose method. It corrects for the given north
|
||||
@@ -493,7 +514,7 @@ class RegionSet(object):
|
||||
Coords can be either be global chunk coords, or local to a region
|
||||
|
||||
"""
|
||||
regionfile = self.regionfiles.get((chunkX//32, chunkY//32),None)
|
||||
(regionfile,filemtime) = self.regionfiles.get((chunkX//32, chunkY//32),(None, None))
|
||||
return regionfile
|
||||
|
||||
def _iterate_regionfiles(self):
|
||||
@@ -537,6 +558,8 @@ class RegionSetWrapper(object):
|
||||
return self._r.get_chunk(x,z)
|
||||
def iterate_chunks(self):
|
||||
return self._r.iterate_chunks()
|
||||
def iterate_newer_chunks(self,filemtime):
|
||||
return self._r.iterate_newer_chunks(filemtime)
|
||||
def get_chunk_mtime(self, x, z):
|
||||
return self._r.get_chunk_mtime(x,z)
|
||||
|
||||
@@ -623,6 +646,11 @@ class RotatedRegionSet(RegionSetWrapper):
|
||||
x,z = self.rotate(x,z)
|
||||
yield x,z,mtime
|
||||
|
||||
def iterate_newer_chunks(self, filemtime):
|
||||
for x,z,mtime in super(RotatedRegionSet, self).iterate_newer_chunks(filemtime):
|
||||
x,z = self.rotate(x,z)
|
||||
yield x,z,mtime
|
||||
|
||||
class CroppedRegionSet(RegionSetWrapper):
|
||||
def __init__(self, rsetobj, xmin, zmin, xmax, zmax):
|
||||
super(CroppedRegionSet, self).__init__(rsetobj)
|
||||
@@ -646,6 +674,14 @@ class CroppedRegionSet(RegionSetWrapper):
|
||||
self.xmin <= x <= self.xmax and
|
||||
self.zmin <= z <= self.zmax
|
||||
)
|
||||
|
||||
def iterate_newer_chunks(self, filemtime):
|
||||
return ((x,z,mtime) for (x,z,mtime) in super(CroppedRegionSet,self).iterate_newer_chunks(filemtime)
|
||||
if
|
||||
self.xmin <= x <= self.xmax and
|
||||
self.zmin <= z <= self.zmax
|
||||
)
|
||||
|
||||
def get_chunk_mtime(self,x,z):
|
||||
if (
|
||||
self.xmin <= x <= self.xmax and
|
||||
|
||||
2
setup.py
2
setup.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# quick version check
|
||||
if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
|
||||
@@ -272,6 +273,7 @@ class CustomBuild(build):
|
||||
build.run(self)
|
||||
print("\nBuild Complete")
|
||||
except Exception:
|
||||
traceback.print_exc(limit=1)
|
||||
print("\nFailed to build Overviewer!")
|
||||
print("Please review the errors printed above and the build instructions")
|
||||
print("at <http://docs.overviewer.org/en/latest/building/>. If you are")
|
||||
|
||||
@@ -53,6 +53,10 @@ class FakeRegionset(object):
|
||||
for (x,z),mtime in self.chunks.iteritems():
|
||||
yield x,z,mtime
|
||||
|
||||
def iterate_newer_chunks(self, filemtime):
|
||||
for (x,z),mtime in self.chunks.iteritems():
|
||||
yield x,z,mtime
|
||||
|
||||
def get_chunk_mtime(self, x, z):
|
||||
try:
|
||||
return self.chunks[x,z]
|
||||
|
||||
Reference in New Issue
Block a user