big commits to a bunch of stuff. See expanded message
Added an option to enter your own zoom level. Use -z to set the map at a particular zoom level. Zoom levels define the width and height in tiles of the highest zoom level, each new zoom level is twice as wide and tall. (z=6 -> 2^6 tiles wide and tall) Implemented tile re-arrangement on map expansion. Now most tiles will get re-used if your map needs another zoom level! No longer does it need to re-generate everything. No longer creates empty directories for tiles, only creates directories if needed. Fixed some minor off-by-one logic (and the code that canceled it out to make it work)
This commit is contained in:
3
gmap.py
3
gmap.py
@@ -21,6 +21,7 @@ def main():
|
|||||||
cpus = 1
|
cpus = 1
|
||||||
parser = OptionParser(usage=helptext)
|
parser = OptionParser(usage=helptext)
|
||||||
parser.add_option("-p", "--processes", dest="procs", help="How many chunks to render in parallel. A good number for this is the number of cores in your computer. Default %s" % cpus, default=cpus, action="store", type="int")
|
parser.add_option("-p", "--processes", dest="procs", help="How many chunks to render in parallel. A good number for this is the number of cores in your computer. Default %s" % cpus, default=cpus, action="store", type="int")
|
||||||
|
parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int")
|
||||||
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true")
|
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true")
|
||||||
|
|
||||||
options, args = parser.parse_args()
|
options, args = parser.parse_args()
|
||||||
@@ -43,7 +44,7 @@ def main():
|
|||||||
w.go(options.procs)
|
w.go(options.procs)
|
||||||
|
|
||||||
# Now generate the tiles
|
# Now generate the tiles
|
||||||
q = quadtree.QuadtreeGen(w, destdir)
|
q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom)
|
||||||
q.go(options.procs)
|
q.go(options.procs)
|
||||||
|
|
||||||
def delete_all(worlddir, tiledir):
|
def delete_all(worlddir, tiledir):
|
||||||
|
|||||||
144
quadtree.py
144
quadtree.py
@@ -4,6 +4,8 @@ import os
|
|||||||
import os.path
|
import os.path
|
||||||
import hashlib
|
import hashlib
|
||||||
import functools
|
import functools
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -33,30 +35,44 @@ def catch_keyboardinterrupt(func):
|
|||||||
return newfunc
|
return newfunc
|
||||||
|
|
||||||
class QuadtreeGen(object):
|
class QuadtreeGen(object):
|
||||||
def __init__(self, worldobj, destdir):
|
def __init__(self, worldobj, destdir, depth=None):
|
||||||
"""Generates a quadtree from the world given into the
|
"""Generates a quadtree from the world given into the
|
||||||
given dest directory
|
given dest directory
|
||||||
|
|
||||||
worldobj is a world.WorldRenderer object that has already been processed
|
worldobj is a world.WorldRenderer object that has already been processed
|
||||||
|
|
||||||
"""
|
If depth is given, it overrides the calculated value. Otherwise, the
|
||||||
# Determine quadtree depth (midpoint is always 0,0)
|
minimum depth that contains all chunks is calculated and used.
|
||||||
for p in xrange(15):
|
|
||||||
xdiameter = 2*2**p
|
|
||||||
ydiameter = 4*2**p
|
|
||||||
if xdiameter >= worldobj.maxcol and -xdiameter <= worldobj.mincol and \
|
|
||||||
ydiameter >= worldobj.maxrow and -ydiameter <= worldobj.minrow:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError("Your map is waaaay to big!")
|
|
||||||
|
|
||||||
self.p = p
|
"""
|
||||||
|
if depth is None:
|
||||||
|
# Determine quadtree depth (midpoint is always 0,0)
|
||||||
|
for p in xrange(15):
|
||||||
|
# Will 2^p tiles wide and high suffice?
|
||||||
|
|
||||||
|
# X has twice as many chunks as tiles, then halved since this is a
|
||||||
|
# radius
|
||||||
|
xradius = 2**p
|
||||||
|
# Y has 4 times as many chunks as tiles, then halved since this is
|
||||||
|
# a radius
|
||||||
|
yradius = 2*2**p
|
||||||
|
if xradius >= worldobj.maxcol and -xradius <= worldobj.mincol and \
|
||||||
|
yradius >= worldobj.maxrow and -yradius <= worldobj.minrow:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("Your map is waaaay too big!")
|
||||||
|
|
||||||
|
self.p = p
|
||||||
|
else:
|
||||||
|
self.p = depth
|
||||||
|
xradius = 2**depth
|
||||||
|
yradius = 2*2**depth
|
||||||
|
|
||||||
# Make new row and column ranges
|
# Make new row and column ranges
|
||||||
self.mincol = -xdiameter
|
self.mincol = -xradius
|
||||||
self.maxcol = xdiameter
|
self.maxcol = xradius
|
||||||
self.minrow = -ydiameter
|
self.minrow = -yradius
|
||||||
self.maxrow = ydiameter
|
self.maxrow = yradius
|
||||||
|
|
||||||
self.world = worldobj
|
self.world = worldobj
|
||||||
self.destdir = destdir
|
self.destdir = destdir
|
||||||
@@ -72,14 +88,22 @@ class QuadtreeGen(object):
|
|||||||
output.write(html)
|
output.write(html)
|
||||||
|
|
||||||
def _get_cur_depth(self):
|
def _get_cur_depth(self):
|
||||||
"""How deep is the quadtree currently in the destdir? This assumes all
|
"""How deep is the quadtree currently in the destdir? This glances in
|
||||||
the directories are created, even if they don't have any images
|
index.html to see what maxZoom is set to.
|
||||||
|
returns -1 if it couldn't be detected, file not found, or nothing in
|
||||||
|
index.html matched
|
||||||
"""
|
"""
|
||||||
p = 0
|
indexfile = os.path.join(self.destdir, "index.html")
|
||||||
curdir = os.path.join(self.destdir, "tiles")
|
if not os.path.exists(indexfile):
|
||||||
while "0" in os.listdir(curdir):
|
return -1
|
||||||
curdir = os.path.join(curdir, "0")
|
matcher = re.compile(r"maxZoom:\s*(\d+)")
|
||||||
p += 1
|
p = -1
|
||||||
|
for line in open(indexfile, "r"):
|
||||||
|
res = matcher.search(line)
|
||||||
|
if res:
|
||||||
|
p = int(res.group(1))
|
||||||
|
break
|
||||||
|
print "detected previous zoom level:", p
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def _increase_depth(self):
|
def _increase_depth(self):
|
||||||
@@ -108,16 +132,46 @@ class QuadtreeGen(object):
|
|||||||
os.rename(p, getpath(newdir, newf))
|
os.rename(p, getpath(newdir, newf))
|
||||||
os.rename(newdirpath, getpath(str(dirnum)))
|
os.rename(newdirpath, getpath(str(dirnum)))
|
||||||
|
|
||||||
|
def _decrease_depth(self):
|
||||||
|
"""If the map size decreases, or perhaps the user has a depth override
|
||||||
|
in effect, re-arrange existing tiles for a smaller tree"""
|
||||||
|
getpath = functools.partial(os.path.join, self.destdir, "tiles")
|
||||||
|
|
||||||
|
# quadrant 0/3 goes to 0
|
||||||
|
# 1/2 goes to 1
|
||||||
|
# 2/1 goes to 2
|
||||||
|
# 3/0 goes to 3
|
||||||
|
# Just worry about the directories here, the files at the top two
|
||||||
|
# levels are cheap enough to replace
|
||||||
|
os.rename(getpath("0", "3"), getpath("new0"))
|
||||||
|
os.rename(getpath("1", "2"), getpath("new1"))
|
||||||
|
os.rename(getpath("2", "1"), getpath("new2"))
|
||||||
|
os.rename(getpath("3", "0"), getpath("new3"))
|
||||||
|
|
||||||
|
shutil.rmtree(getpath("0"))
|
||||||
|
shutil.rmtree(getpath("1"))
|
||||||
|
shutil.rmtree(getpath("2"))
|
||||||
|
shutil.rmtree(getpath("3"))
|
||||||
|
|
||||||
|
os.rename(getpath("new0"), getpath("0"))
|
||||||
|
os.rename(getpath("new1"), getpath("1"))
|
||||||
|
os.rename(getpath("new2"), getpath("2"))
|
||||||
|
os.rename(getpath("new3"), getpath("3"))
|
||||||
|
|
||||||
def go(self, procs):
|
def go(self, procs):
|
||||||
"""Renders all tiles"""
|
"""Renders all tiles"""
|
||||||
|
|
||||||
curdepth = self._get_cur_depth()
|
curdepth = self._get_cur_depth()
|
||||||
if self.p > curdepth:
|
if curdepth != -1:
|
||||||
print "Your map seemes to have expanded beyond its previous bounds."
|
if self.p > curdepth:
|
||||||
print "Doing some tile re-arrangements... just a sec..."
|
print "Your map seemes to have expanded beyond its previous bounds."
|
||||||
for _ in xrange(self.p-curdepth):
|
print "Doing some tile re-arrangements... just a sec..."
|
||||||
print "Increasing depth..."
|
for _ in xrange(self.p-curdepth):
|
||||||
self._increase_depth()
|
self._increase_depth()
|
||||||
|
elif self.p < curdepth:
|
||||||
|
print "Your map seems to have shrunk. Re-arranging tiles, just a sec..."
|
||||||
|
for _ in xrange(curdepth - self.p):
|
||||||
|
self._decrease_depth()
|
||||||
|
|
||||||
# Create a pool
|
# Create a pool
|
||||||
pool = multiprocessing.Pool(processes=procs)
|
pool = multiprocessing.Pool(processes=procs)
|
||||||
@@ -126,7 +180,7 @@ class QuadtreeGen(object):
|
|||||||
print "Computing the tile ranges and starting tile processers for inner-most tiles..."
|
print "Computing the tile ranges and starting tile processers for inner-most tiles..."
|
||||||
print "This takes the longest. The other levels will go quicker"
|
print "This takes the longest. The other levels will go quicker"
|
||||||
results = []
|
results = []
|
||||||
for path in iterate_base4(self.p+1):
|
for path in iterate_base4(self.p):
|
||||||
# Get the range for this tile
|
# Get the range for this tile
|
||||||
colstart, rowstart = self._get_range_by_path(path)
|
colstart, rowstart = self._get_range_by_path(path)
|
||||||
colend = colstart + 2
|
colend = colstart + 2
|
||||||
@@ -135,11 +189,6 @@ class QuadtreeGen(object):
|
|||||||
# This image is rendered at:
|
# This image is rendered at:
|
||||||
dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path))
|
dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path))
|
||||||
|
|
||||||
# The directory, create it if not exists
|
|
||||||
dirdest = os.path.dirname(dest)
|
|
||||||
if not os.path.exists(dirdest):
|
|
||||||
os.makedirs(dirdest)
|
|
||||||
|
|
||||||
# And uses these chunks
|
# And uses these chunks
|
||||||
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart,
|
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart,
|
||||||
rowend)
|
rowend)
|
||||||
@@ -153,7 +202,7 @@ class QuadtreeGen(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.write_html(self.p+1)
|
self.write_html(self.p)
|
||||||
|
|
||||||
# Wait for all results to finish
|
# Wait for all results to finish
|
||||||
print "Rendering inner most zoom level tiles now!"
|
print "Rendering inner most zoom level tiles now!"
|
||||||
@@ -161,15 +210,14 @@ class QuadtreeGen(object):
|
|||||||
# get() instead of wait() so we can see errors
|
# get() instead of wait() so we can see errors
|
||||||
result.get()
|
result.get()
|
||||||
if i > 0 and (i % 100 == 0 or 100 % i == 0):
|
if i > 0 and (i % 100 == 0 or 100 % i == 0):
|
||||||
print "{0}/{1} tiles complete on level {2}/{3}".format(
|
print "{0}/{1} tiles complete on level 1/{2}".format(
|
||||||
i, len(results), 1, self.p+1)
|
i, len(results), self.p)
|
||||||
print "Done"
|
print "Done"
|
||||||
|
|
||||||
# Now do the other layers
|
# Now do the other layers
|
||||||
for zoom in xrange(self.p, 0, -1):
|
for zoom in xrange(self.p-1, 0, -1):
|
||||||
level = self.p+2-zoom
|
level = self.p - zoom + 1
|
||||||
print "Preparing level", level
|
print "Starting level", level
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for path in iterate_base4(zoom):
|
for path in iterate_base4(zoom):
|
||||||
# This image is rendered at:
|
# This image is rendered at:
|
||||||
@@ -182,13 +230,12 @@ class QuadtreeGen(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
print "Rendering level {0}/{1} now!".format(level, self.p+1)
|
|
||||||
for i, result in enumerate(results):
|
for i, result in enumerate(results):
|
||||||
# get() instead of wait() so we can see errors
|
# get() instead of wait() so we can see errors
|
||||||
result.get()
|
result.get()
|
||||||
if i > 0 and (i % 100 == 0 or 100 % i == 0):
|
if i > 0 and (i % 100 == 0 or 100 % i == 0):
|
||||||
print "{0}/{1} tiles complete on level {2}/{3}".format(
|
print "{0}/{1} tiles complete for level {2}/{3}".format(
|
||||||
i, len(results), level, self.p+1)
|
i, len(results), level, self.p)
|
||||||
print "Done"
|
print "Done"
|
||||||
|
|
||||||
# Do the final one right here:
|
# Do the final one right here:
|
||||||
@@ -274,7 +321,7 @@ def render_innertile(dest, name):
|
|||||||
if os.path.exists(hashpath):
|
if os.path.exists(hashpath):
|
||||||
os.unlink(hashpath)
|
os.unlink(hashpath)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Now check the hashes
|
# Now check the hashes
|
||||||
hasher = hashlib.md5()
|
hasher = hashlib.md5()
|
||||||
if q0hash:
|
if q0hash:
|
||||||
@@ -378,6 +425,11 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path):
|
|||||||
os.unlink(hashpath)
|
os.unlink(hashpath)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Create the directory if not exists
|
||||||
|
dirdest = os.path.dirname(path)
|
||||||
|
if not os.path.exists(dirdest):
|
||||||
|
os.makedirs(dirdest)
|
||||||
|
|
||||||
imghash = hashlib.md5()
|
imghash = hashlib.md5()
|
||||||
for col, row, chunkfile in chunks:
|
for col, row, chunkfile in chunks:
|
||||||
# Get the hash of this image and add it to our hash for this tile
|
# Get the hash of this image and add it to our hash for this tile
|
||||||
|
|||||||
Reference in New Issue
Block a user