From a5cc2e3ae25a2baa2978dd5697cb3bd002dd85e3 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Mon, 27 Sep 2010 14:54:10 -0400 Subject: [PATCH 01/27] more accurate marker positioning on map, and a reasonable custom projection --- template.html | 162 ++++++++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 70 deletions(-) diff --git a/template.html b/template.html index ef2868f..63ebdb6 100644 --- a/template.html +++ b/template.html @@ -22,6 +22,65 @@ debug: false }; + // our custom projection maps Latitude to Y, and Longitude to X as normal, + // but it maps the range [0.0, 1.0] to [0, tileSize] in both directions + // so it is easier to position markers, etc. based on their position + // (find their position in the lowest-zoom image, and divide by tileSize) + function MCMapProjection() { + this.inverseTileSize = 1.0 / config.tileSize; + } + + MCMapProjection.prototype.fromLatLngToPoint = function(latLng) { + var x = latLng.lng() * config.tileSize; + var y = latLng.lat() * config.tileSize; + return new google.maps.Point(x, y); + }; + + MCMapProjection.prototype.fromPointToLatLng = function(point) { + var lng = point.x * this.inverseTileSize; + var lat = point.y * this.inverseTileSize; + return new google.maps.LatLng(lat, lng); + }; + + // helper to get map LatLng from world coordinates + // takes arguments in X, Y, Z order + // (arguments are *out of order*, because within the function we use + // the axes like the rest of Minecraft Overviewer -- with the Z and Y + // flipped from normal minecraft usage.) + function fromWorldToLatLng(x, z, y) + { + // the width and height of all the highest-zoom tiles combined, inverted + var perPixel = 1.0 / (config.tileSize * Math.pow(2, config.maxZoom)); + + // This information about where the center column is may change with a different + // drawing implementation -- check it again after any drawing overhauls! + + // point (0, 0, 127) is at (0.5, 0.0) of tile (tiles/2 - 1, tiles/2) + // so the Y coordinate is at 0.5, and the X is at 0.5 - ((tileSize / 2) / (tileSize * 2^maxZoom)) + // or equivalently, 0.5 - (1 / 2^(maxZoom + 1)) + var lng = 0.5 - (1.0 / Math.pow(2, config.maxZoom + 1)); + var lat = 0.5; + + // the following metrics mimic those in ChunkRenderer.chunk_render in "chunk.py" + + // each block on X axis adds 12px to x and subtracts 6px from y + lng += 12 * x * perPixel; + lat -= 6 * x * perPixel; + + // each block on Y axis adds 12px to x and adds 6px to y + lng += 12 * y * perPixel; + lat += 6 * y * perPixel; + + // each block down along Z adds 12px to y + lat += 12 * (128 - z) * perPixel; + + // add on 12 px to the X coordinate and 18px to the Y to center our point + lng += 12 * perPixel; + lat += 18 * perPixel; + + return new google.maps.LatLng(lat, lng); + } + var MCMapOptions = { getTileUrl: function(tile, zoom) { var url = config.path; @@ -52,6 +111,7 @@ var MCMapType = new google.maps.ImageMapType(MCMapOptions); MCMapType.name = "MC Map"; MCMapType.alt = "Minecraft Map"; + MCMapType.projection = new MCMapProjection(); function CoordMapType() { } @@ -75,61 +135,30 @@ }; var map; - var prot; - + var markersInit = false; - - function convertCoords (x,y,z) { - - var imgx = 0; - var imgy = 0; - - imgx = imgx + (12*x); - imgy = imgy - (6*x); - - imgx = imgx + (12 * y); - imgy = imgy + (6* y); - - imgy = imgy - (12*z); - - // this math is mysterious. i don't fully understand it - // but the idea is to assume that block 0,0,0 in chunk 0,0 - // is drawn in the very middle of the gmap at (192,192) - return [192*Math.pow(2,config.maxZoom)+imgx, 192*Math.pow(2,config.maxZoom)+imgy+768+768]; - } - + function initMarkers() { - if (markersInit) { return; } - - markersInit = true; - - prot = map.getProjection(); - - for (i in markerData) { - var item = markerData[i]; - - var converted = convertCoords(item.x-16, item.z, item.y); - - - var x = converted[0] / Math.pow(2, config.maxZoom); - var y = converted[1] / Math.pow(2, config.maxZoom); - var p = new google.maps.Point(x,y); - var marker = new google.maps.Marker({ -position: prot.fromPointToLatLng(p), -map: map, -title:item.msg -}); - -} - - - -} + if (markersInit) { return; } + + markersInit = true; + for (i in markerData) { + var item = markerData[i]; + + var converted = fromWorldToLatLng(item.x, item.y, item.z); + var marker = new google.maps.Marker({ + position: converted, + map: map, + title: item.msg + }); + } + } + function initialize() { var mapOptions = { zoom: config.defaultZoom, - center: new google.maps.LatLng(-45, 90), + center: new google.maps.LatLng(0.5, 0.5), navigationControl: true, scaleControl: false, mapTypeControl: false, @@ -139,34 +168,27 @@ title:item.msg if(config.debug) { map.overlayMapTypes.insertAt(0, new CoordMapType(new google.maps.Size(config.tileSize, config.tileSize))); + + google.maps.event.addListener(map, 'click', function(event) { + console.log("latLng; " + event.latLng.lat() + ", " + event.latLng.lng()); + + var pnt = map.getProjection().fromLatLngToPoint(event.latLng); + console.log("point: " + pnt); + + var pxx = pnt.x * config.tileSize * Math.pow(2, config.maxZoom); + var pxy = pnt.y * config.tileSize * Math.pow(2, config.maxZoom); + console.log("pixel: " + pxx + ", " + pxy); + }); } - - // Now attach the coordinate map type to the map's registry map.mapTypes.set('mcmap', MCMapType); // We can now set the map to use the 'coordinate' map type map.setMapTypeId('mcmap'); - - prot = map.getProjection(); - - if (config.debug) - google.maps.event.addListener(map, 'click', function(event) { - console.log("latLng: " + event.latLng.lat() + ", " + event.latLng.lng()); - var pnt = prot.fromLatLngToPoint(event.latLng); - - console.log("point: " + pnt);// - var pxx = pnt.x * Math.pow(2,config.maxZoom); - var pxy = pnt.y * Math.pow(2,config.maxZoom); - console.log("pixel: " + pxx + ", " + pxy); - }); - - google.maps.event.addListener(map, 'projection_changed', function(event) { - initMarkers(); - }); - - + + // initialize the markers + initMarkers(); } From 5a14f1b486892b7ba0882356faa3963151ea3a34 Mon Sep 17 00:00:00 2001 From: Stephen Fluin Date: Wed, 29 Sep 2010 21:17:22 -0500 Subject: [PATCH 02/27] Fixed scoping for imageformat --- quadtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadtree.py b/quadtree.py index 8733881..03f6672 100644 --- a/quadtree.py +++ b/quadtree.py @@ -172,8 +172,8 @@ class QuadtreeGen(object): newdir = "new" + str(dirnum) newdirpath = getpath(newdir) - files = [str(dirnum)+"."+imgformat, str(dirnum)+".hash", str(dirnum)] - newfiles = [str(newnum)+"."+imgformat, str(newnum)+".hash", str(newnum)] + files = [str(dirnum)+"."+self.imgformat, str(dirnum)+".hash", str(dirnum)] + newfiles = [str(newnum)+"."+self.imgformat, str(newnum)+".hash", str(newnum)] os.mkdir(newdirpath) for f, newf in zip(files, newfiles): From fd43331350b285e7c3f0a60e6cbd5c6e5b2631e8 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 29 Sep 2010 23:31:19 -0400 Subject: [PATCH 03/27] removed unused imports --- chunk.py | 1 - world.py | 1 - 2 files changed, 2 deletions(-) diff --git a/chunk.py b/chunk.py index 47d1dc8..fef3a84 100644 --- a/chunk.py +++ b/chunk.py @@ -15,7 +15,6 @@ import numpy from PIL import Image, ImageDraw -from itertools import izip, count import os.path import hashlib diff --git a/world.py b/world.py index 99728c1..3ee4b5b 100644 --- a/world.py +++ b/world.py @@ -20,7 +20,6 @@ import multiprocessing import sys import numpy -from PIL import Image import chunk import nbt From 9f49bf3d774b97e594b552ebbb01e1a87f7e1136 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Thu, 30 Sep 2010 14:35:37 +1000 Subject: [PATCH 04/27] Change logging to use the 'logging' module. --- gmap.py | 17 +++++++++++++++-- quadtree.py | 36 ++++++++++++++++++------------------ world.py | 18 +++++++++++------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/gmap.py b/gmap.py index 7d3f165..aad5959 100755 --- a/gmap.py +++ b/gmap.py @@ -26,6 +26,9 @@ from optparse import OptionParser import re import multiprocessing import time +import logging + +logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s") import world import quadtree @@ -46,6 +49,8 @@ def main(): parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.") + parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") + parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") options, args = parser.parse_args() @@ -93,6 +98,14 @@ def main(): else: imgformat = 'png' + logging.getLogger().setLevel( + logging.getLogger().level + 10*options.quiet) + logging.getLogger().setLevel( + logging.getLogger().level - 10*options.verbose) + + logging.info("Welcome to Minecraft Overviewer!") + logging.debug("Current log level: {0}".format(logging.getLogger().level)) + # First generate the world's chunk images w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist) w.go(options.procs) @@ -110,7 +123,7 @@ def delete_all(worlddir, tiledir): for f in filenames: if matcher.match(f): filepath = os.path.join(dirpath, f) - print "Deleting {0}".format(filepath) + logging.info("Deleting {0}".format(filepath)) os.unlink(filepath) # Now delete all /hash/ files in the tile dir. @@ -119,7 +132,7 @@ def delete_all(worlddir, tiledir): for f in filenames: if f.endswith(".hash"): filepath = os.path.join(dirpath, f) - print "Deleting {0}".format(filepath) + logging.info("Deleting {0}".format(filepath)) os.unlink(filepath) def list_worlds(): diff --git a/quadtree.py b/quadtree.py index 8733881..9fb8128 100644 --- a/quadtree.py +++ b/quadtree.py @@ -23,6 +23,7 @@ import re import shutil import collections import json +import logging from PIL import Image @@ -45,7 +46,7 @@ def catch_keyboardinterrupt(func): try: return func(*args, **kwargs) except KeyboardInterrupt: - print "Ctrl-C caught!" + logging.error("Ctrl-C caught!") raise Exception("Exiting") except: import traceback @@ -111,8 +112,8 @@ class QuadtreeGen(object): else: if not complete % 1000 == 0: return - print "{0}/{1} tiles complete on level {2}/{3}".format( - complete, total, level, self.p) + logging.info("{0}/{1} tiles complete on level {2}/{3}".format( + complete, total, level, self.p)) def write_html(self, zoomlevel, imgformat): """Writes out index.html""" @@ -258,12 +259,12 @@ class QuadtreeGen(object): curdepth = self._get_cur_depth() if curdepth != -1: if self.p > curdepth: - print "Your map seemes to have expanded beyond its previous bounds." - print "Doing some tile re-arrangements... just a sec..." + logging.warning("Your map seemes to have expanded beyond its previous bounds.") + logging.warning( "Doing some tile re-arrangements... just a sec...") for _ in xrange(self.p-curdepth): self._increase_depth() elif self.p < curdepth: - print "Your map seems to have shrunk. Re-arranging tiles, just a sec..." + logging.warning("Your map seems to have shrunk. Re-arranging tiles, just a sec...") for _ in xrange(curdepth - self.p): self._decrease_depth() @@ -279,11 +280,11 @@ class QuadtreeGen(object): results = collections.deque() complete = 0 total = 4**self.p - print "Rendering highest zoom level of tiles now." - print "There are {0} tiles to render".format(total) - print "There are {0} total levels to render".format(self.p) - print "Don't worry, each level has only 25% as many tiles as the last." - print "The others will go faster" + logging.info("Rendering highest zoom level of tiles now.") + logging.info("There are {0} tiles to render".format(total)) + logging.info("There are {0} total levels to render".format(self.p)) + logging.info("Don't worry, each level has only 25% as many tiles as the last.") + logging.info("The others will go faster") for result in self._apply_render_worldtiles(pool): results.append(result) if len(results) > 10000: @@ -308,7 +309,7 @@ class QuadtreeGen(object): assert len(results) == 0 complete = 0 total = 4**zoom - print "Starting level", level + logging.info("Starting level {0}".format(level)) for result in self._apply_render_inntertile(pool, zoom): results.append(result) if len(results) > 10000: @@ -324,7 +325,7 @@ class QuadtreeGen(object): self.print_statusline(complete, total, level, True) - print "Done" + logging.info("Done") pool.close() pool.join() @@ -561,8 +562,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # corrupting it), then this could error. # Since we have no easy way of determining how this chunk was # generated, we need to just ignore it. - print "Error opening file", chunkfile - print "(Error was {0})".format(e) + logging.warning("Could not open chunk '{0}' ({1})".format(chunkfile,e)) try: # Remove the file so that the next run will re-generate it. os.unlink(chunkfile) @@ -571,12 +571,12 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # Ignore if file doesn't exist, another task could have already # removed it. if e.errno != errno.ENOENT: - print "Could not remove the corrupt chunk!" + logging.warning("Could not remove chunk '{0}'!".format(chunkfile)) raise else: - print "Removed the corrupt file" + logging.warning("Removed the corrupt file") - print "You will need to re-run the Overviewer to fix this chunk" + logging.warning("You will need to re-run the Overviewer to fix this chunk") continue xpos = -192 + (col-colstart)*192 diff --git a/world.py b/world.py index 99728c1..cc3f545 100644 --- a/world.py +++ b/world.py @@ -18,6 +18,7 @@ import os import os.path import multiprocessing import sys +import logging import numpy from PIL import Image @@ -171,8 +172,9 @@ class WorldRenderer(object): def go(self, procs): """Starts the render. This returns when it is finished""" - print "Scanning chunks" + logging.info("Scanning chunks") raw_chunks = self._find_chunkfiles() + logging.debug("Done scanning chunks") # Translate chunks to our diagonal coordinate system mincol, maxcol, minrow, maxrow, chunks = _convert_coords(raw_chunks) @@ -206,9 +208,11 @@ class WorldRenderer(object): p = f.split(".") all_chunks.append((base36decode(p[1]), base36decode(p[2]), os.path.join(dirpath, f))) + logging.debug((base36decode(p[1]), base36decode(p[2]), + os.path.join(dirpath, f))) if not all_chunks: - print "Error: No chunks found!" + logging.error("Error: No chunks found!") sys.exit(1) return all_chunks @@ -229,7 +233,7 @@ class WorldRenderer(object): results = {} if processes == 1: # Skip the multiprocessing stuff - print "Rendering chunks synchronously since you requested 1 process" + logging.debug("Rendering chunks synchronously since you requested 1 process") for i, (col, row, chunkfile) in enumerate(chunks): if inclusion_set and (col, row) not in inclusion_set: # Skip rendering, just find where the existing image is @@ -243,9 +247,9 @@ class WorldRenderer(object): results[(col, row)] = result if i > 0: if 1000 % i == 0 or i % 1000 == 0: - print "{0}/{1} chunks rendered".format(i, len(chunks)) + logging.info("{0}/{1} chunks rendered".format(i, len(chunks))) else: - print "Rendering chunks in {0} processes".format(processes) + logging.debug("Rendering chunks in {0} processes".format(processes)) pool = multiprocessing.Pool(processes=processes) asyncresults = [] for col, row, chunkfile in chunks: @@ -268,10 +272,10 @@ class WorldRenderer(object): results[(col, row)] = result.get() if i > 0: if 1000 % i == 0 or i % 1000 == 0: - print "{0}/{1} chunks rendered".format(i, len(asyncresults)) + logging.info("{0}/{1} chunks rendered".format(i, len(asyncresults))) pool.join() - print "Done!" + logging.info("Done!") return results From 110240b53a4480097f1e9d75e0eb372b888012b6 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 18:35:46 -0700 Subject: [PATCH 05/27] Ignore .pyc files --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From cf971c17c6b7735d496501f43f63c953241353db Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 18:36:10 -0700 Subject: [PATCH 06/27] Render snow as half-blocks --- chunk.py | 9 +++++++-- textures.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/chunk.py b/chunk.py index fef3a84..aa2aed1 100644 --- a/chunk.py +++ b/chunk.py @@ -312,12 +312,17 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # Draw edge lines + if blockid in (44,78,): # step block, snow + half = 6 + else: + half = 0 + if blockid not in transparent_blocks: draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy), (imgx+22,imgy+5)), fill=(0,0,0), width=1) + draw.line(((imgx+12,imgy+half), (imgx+22,imgy+5+half)), fill=(0,0,0), width=1) if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6), (imgx+12,imgy)), fill=(0,0,0), width=1) + draw.line(((imgx,imgy+6+half), (imgx+12,imgy+half)), fill=(0,0,0), width=1) finally: diff --git a/textures.py b/textures.py index 8d34b03..320d980 100644 --- a/textures.py +++ b/textures.py @@ -151,7 +151,7 @@ def _transform_image_side(img, blockID): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" - if blockID in (44,): # step block + if blockID in (44,78,): # step block, snow # make the top half transpartent # (don't just crop img, since we want the size of # img to be unchanged @@ -213,7 +213,7 @@ def _build_block(top, side, blockID=None): img.paste(side, (2,6), side) img.paste(otherside, (10,6), otherside) img.paste(top, (0,2), top) - elif blockID in (44,): # half step + elif blockID in (44,78,): # half step, snow # shift each texture down 6 pixels img.paste(side, (0,12), side) img.paste(otherside, (12,12), otherside) From 822dd75431e857a312a0be3d198e785130f306b6 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Thu, 30 Sep 2010 21:38:14 -0400 Subject: [PATCH 07/27] Render minetracks (with correct orientation) Other things with direction (torches, ladders, stairs, etc) will be handled in a similar fashion. Note: minetracks on slopes are still not rendered correctly --- chunk.py | 24 +++++++++++++++++++++++- textures.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index fef3a84..aaee2e4 100644 --- a/chunk.py +++ b/chunk.py @@ -59,6 +59,11 @@ def get_skylight_array(level): """ return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) +def get_blockdata_array(level): + """Returns the ancillary data from the 'Data' byte array. Data is packed + in a similar manner to skylight data""" + return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) + # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) @@ -237,6 +242,13 @@ class ChunkRenderer(object): blocks = blocks.copy() blocks[skylight_expanded != 0] = 21 + blockData = get_blockdata_array(self.level) + blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) + # Even elements get the lower 4 bits + blockData_expanded[:,:,::2] = blockData & 0x0F + # Odd elements get the upper 4 bits + blockData_expanded[:,:,1::2] = blockData >> 4 + # Each block is 24x24 # The next block on the X axis adds 12px to x and subtracts 6px from y in the image @@ -255,7 +267,17 @@ class ChunkRenderer(object): for z in xrange(128): try: blockid = blocks[x,y,z] - t = textures.blockmap[blockid] + + # the following blocks don't have textures that can be pre-computed from the blockid + # alone. additional data is required. + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in textures.py) + if blockid in (66,): ## minecart track + ancilData = blockData_expanded[x,y,z] + t = textures.generate_special_texture(blockid, ancilData) + + else: + t = textures.blockmap[blockid] if not t: continue diff --git a/textures.py b/textures.py index 8d34b03..a73517f 100644 --- a/textures.py +++ b/textures.py @@ -114,7 +114,7 @@ def _split_terrain(terrain): # This maps terainids to 16x16 images terrain_images = _split_terrain(_get_terrain_image()) -def _transform_image(img, blockID): +def _transform_image(img, blockID=None): """Takes a PIL image and rotates it left 45 degrees and shrinks the y axis by a factor of 2. Returns the resulting image, which will be 24x12 pixels @@ -201,6 +201,8 @@ def _build_block(top, side, blockID=None): otherside.putalpha(othersidealpha) ## special case for non-block things + # TODO once torches are handled by generate_special_texture, remove + # them from this list if blockID in (37,38,6,39,40,50,83): ## flowers, sapling, mushrooms, regular torch, reeds # instead of pasting these blocks at the cube edges, place them in the middle: # and omit the top @@ -321,3 +323,41 @@ def load_water(): blockmap[10] = lavablock.convert("RGB"), lavablock blockmap[11] = blockmap[10] load_water() + + +def generate_special_texture(blockID, data): + """Generates a special texture, such as a correctly facing minecraft track""" + #print "%s has ancillary data: %X" %(blockID, data) + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in chunkpy) + if blockID == 66: # minetrack: + raw_straight = terrain_images[128] + raw_corner = terrain_images[112] + + ## use _transform_image to scale and shear + if data == 0: + track = _transform_image(raw_straight, blockID) + elif data == 6: + track = _transform_image(raw_corner, blockID) + elif data == 7: + track = _transform_image(raw_corner.rotate(270), blockID) + elif data == 8: + # flip + track = _transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM).rotate(90), + blockID) + elif data == 9: + track = _transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM), + blockID) + elif data == 1: + track = _transform_image(raw_straight.rotate(90), blockID) + else: + # TODO render carts that slop up or down + track = _transform_image(raw_straight, blockID) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(track, (0,12), track) + + return (img.convert("RGB"), img.split()[3]) + + + return None From 5d22e1cd6f667bbba7242d1f7a0ba2551b598778 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 22:47:10 -0700 Subject: [PATCH 08/27] Fix whitespace formatting, make snow 1/4th of block --- chunk.py | 14 ++++++++------ textures.py | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/chunk.py b/chunk.py index 474bc76..91f8ac5 100644 --- a/chunk.py +++ b/chunk.py @@ -334,17 +334,19 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # Draw edge lines - if blockid in (44,78,): # step block, snow - half = 6 - else: - half = 0 + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 if blockid not in transparent_blocks: draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy+half), (imgx+22,imgy+5+half)), fill=(0,0,0), width=1) + draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6+half), (imgx+12,imgy+half)), fill=(0,0,0), width=1) + draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) finally: diff --git a/textures.py b/textures.py index b9285d4..5bb7382 100644 --- a/textures.py +++ b/textures.py @@ -151,14 +151,20 @@ def _transform_image_side(img, blockID): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" - if blockID in (44,78,): # step block, snow - # make the top half transpartent + if blockID in (44,): # step block + # make the top half transparent # (don't just crop img, since we want the size of # img to be unchanged mask = img.crop((0,8,16,16)) n = Image.new(img.mode, img.size, (38,92,255,0)) n.paste(mask,(0,0,16,8), mask) img = n + if blockID in (78,): # snow + # make the top three quarters transparent + mask = img.crop((0,12,16,16)) + n = Image.new(img.mode, img.size, (38,92,255,0)) + n.paste(mask,(0,12,16,16), mask) + img = n # Size of the cube side before shear img = img.resize((12,12)) @@ -215,11 +221,16 @@ def _build_block(top, side, blockID=None): img.paste(side, (2,6), side) img.paste(otherside, (10,6), otherside) img.paste(top, (0,2), top) - elif blockID in (44,78,): # half step, snow + elif blockID in (44,): # half step # shift each texture down 6 pixels img.paste(side, (0,12), side) img.paste(otherside, (12,12), otherside) img.paste(top, (0,6), top) + elif blockID in (78,): # snow + # shift each texture down 9 pixels + img.paste(side, (0,6), side) + img.paste(otherside, (12,6), otherside) + img.paste(top, (0,9), top) else: img.paste(side, (0,6), side) img.paste(otherside, (12,6), otherside) From 93af1ef15817a9667a34eb0a608e358cc08acdc6 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 23:52:50 -0700 Subject: [PATCH 09/27] Add snow to the transparent_blocks array --- chunk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index 91f8ac5..322f6c2 100644 --- a/chunk.py +++ b/chunk.py @@ -66,7 +66,7 @@ def get_blockdata_array(level): # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, - 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) + 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85]) def render_and_save(chunkfile, cachedir, cave=False): """Used as the entry point for the multiprocessing workers (since processes @@ -341,7 +341,7 @@ class ChunkRenderer(object): else: increment = 0 - if blockid not in transparent_blocks: + if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) From 86d5476234cd061713bd207785d198a2b570da9c Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 19:45:41 -0400 Subject: [PATCH 10/27] use os.path.join to build the spawn chunk path --- world.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/world.py b/world.py index 231287b..25157d1 100644 --- a/world.py +++ b/world.py @@ -147,10 +147,8 @@ class WorldRenderer(object): chunkY = spawnZ/16 ## The filename of this chunk - chunkFile = "%s/%s/c.%s.%s.dat" % (base36encode(chunkX % 64), - base36encode(chunkY % 64), - base36encode(chunkX), - base36encode(chunkY)) + chunkFile = os.path.join(base36encode(chunkX % 64), base36encode(chunkY % 64), + "c.%s.%s.dat" % (base36encode(chunkX), base36encode(chunkY))) data=nbt.load(os.path.join(self.worlddir, chunkFile))[1] From 78962cfbe57243949856051504d71ca4f20c77fc Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 22:33:25 -0400 Subject: [PATCH 11/27] Passable crop rendering --- chunk.py | 2 +- textures.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index aaee2e4..d3b3fc4 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,7 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,): ## minecart track + if blockid in (66,59): ## minecart track, crops ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index a73517f..0d4cfb1 100644 --- a/textures.py +++ b/textures.py @@ -253,7 +253,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 6, 6, 7, 8, 35, # Gold/iron blocks? Doublestep? TNT from above? # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops left out. sign post + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops handled elsewhere. sign post # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation # 80 81 82 83 84 @@ -358,6 +358,20 @@ def generate_special_texture(blockID, data): img.paste(track, (0,12), track) return (img.convert("RGB"), img.split()[3]) + if blockID == 59: # crops + raw_crop = terrain_images[88+data] + crop1 = _transform_image(raw_crop, blockID) + crop2 = _transform_image_side(raw_crop, blockID) + crop3 = crop2.transpose(Image.FLIP_LEFT_RIGHT) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(crop1, (0,12), crop1) + img.paste(crop2, (6,3), crop2) + img.paste(crop3, (6,3), crop3) + return (img.convert("RGB"), img.split()[3]) + + + return None From 66a90bb4113cf7590a0ba0b15d884d04b3c45c45 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 22:51:02 -0400 Subject: [PATCH 12/27] Render furnaces with the correct texture (instead of smooth stone) These are handled specially, since one side texture is different than the other --- chunk.py | 3 ++- textures.py | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/chunk.py b/chunk.py index d3b3fc4..2c345c1 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,8 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59): ## minecart track, crops + if blockid in (66,59,61,62): ## minecart track, crops + # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index 0d4cfb1..1b7d27c 100644 --- a/textures.py +++ b/textures.py @@ -147,7 +147,7 @@ def _transform_image(img, blockID=None): newimg = img.transform((24,12), Image.AFFINE, transform) return newimg -def _transform_image_side(img, blockID): +def _transform_image_side(img, blockID=None): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" @@ -253,7 +253,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 6, 6, 7, 8, 35, # Gold/iron blocks? Doublestep? TNT from above? # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops handled elsewhere. sign post + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops/furnaces handled elsewhere. sign post # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation # 80 81 82 83 84 @@ -270,7 +270,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 5, 5, 7, 8, 35, # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 44, 61, -1, # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # 80 81 82 83 84 @@ -370,8 +370,29 @@ def generate_special_texture(blockID, data): img.paste(crop3, (6,3), crop3) return (img.convert("RGB"), img.split()[3]) + if blockID == 61: #furnace + top = _transform_image(terrain_images[1]) + side1 = _transform_image_side(terrain_images[45]) + side2 = _transform_image_side(terrain_images[44]).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(side1, (0,6), side1) + img.paste(side2, (12,6), side2) + img.paste(top, (0,0), top) + return (img.convert("RGB"), img.split()[3]) + + if blockID == 62: # lit furnace + top = _transform_image(terrain_images[1]) + side1 = _transform_image_side(terrain_images[45]) + side2 = _transform_image_side(terrain_images[45+16]).transpose(Image.FLIP_LEFT_RIGHT) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + + img.paste(side1, (0,6), side1) + img.paste(side2, (12,6), side2) + img.paste(top, (0,0), top) + return (img.convert("RGB"), img.split()[3]) return None From cd97222a9b9681b7d856bb91f0511843a7bc0485 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 23:19:53 -0400 Subject: [PATCH 13/27] Render ladders (with correct orientation) --- chunk.py | 2 +- textures.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/chunk.py b/chunk.py index 2c345c1..2817979 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,7 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59,61,62): ## minecart track, crops + if blockid in (66,59,61,62, 65): ## minecart track, crops, ladder # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index 1b7d27c..dbcac8a 100644 --- a/textures.py +++ b/textures.py @@ -394,5 +394,33 @@ def generate_special_texture(blockID, data): img.paste(top, (0,0), top) return (img.convert("RGB"), img.split()[3]) + if blockID == 65: # ladder + raw_texture = terrain_images[83] + #print "ladder is facing: %d" % data + if data == 5: + # normally this ladder would be obsured by the block it's attached to + # but since ladders can apparently be placed on transparent blocks, we + # have to render this thing anyway. same for data == 2 + tex = _transform_image_side(raw_texture) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (0,6), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 2: + tex = _transform_image_side(raw_texture).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (12,6), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 3: + tex = _transform_image_side(raw_texture).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (0,0), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 4: + tex = _transform_image_side(raw_texture) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (12,0), tex) + return (img.convert("RGB"), img.split()[3]) + + return None From 6993f2159db6e11778166f7a38b7b9a3afa1bfc9 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Sat, 2 Oct 2010 15:33:33 -0400 Subject: [PATCH 14/27] Render iron and wood doors with correct orientation. Note: iron doors need testing (they are currently broken in my test SMP world) --- chunk.py | 6 ++++-- textures.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index 2817979..a1deab1 100644 --- a/chunk.py +++ b/chunk.py @@ -272,8 +272,10 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59,61,62, 65): ## minecart track, crops, ladder - # also handle furnaces here, since one side has a different texture than the other + + ## minecart track, crops, ladder, doors + if blockid in (66,59,61,62, 65,64,71): + # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index dbcac8a..755dae7 100644 --- a/textures.py +++ b/textures.py @@ -421,6 +421,56 @@ def generate_special_texture(blockID, data): img.paste(tex, (12,0), tex) return (img.convert("RGB"), img.split()[3]) + if blockID in (64,71): #wooden door, or iron door + if data & 0x8 == 0x8: # top of the door + raw_door = terrain_images[81 if blockID == 64 else 82] + else: # bottom of the door + raw_door = terrain_images[97 if blockID == 64 else 98] + + # if you want to render all doors as closed, then force + # force swung to be False + if data & 0x4 == 0x4: + swung=True + else: + swung=False + # mask out the high bits to figure out the orientation + img = Image.new("RGBA", (24,24), (38,92,255,0)) + if (data & 0x03) == 0: + if not swung: + tex = _transform_image_side(raw_door) + img.paste(tex, (0,6), tex) + else: + # flip first to set the doornob on the correct side + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + tex = tex.transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (0,0), tex) + + if (data & 0x03) == 1: + if not swung: + tex = _transform_image_side(raw_door).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (0,0), tex) + else: + tex = _transform_image_side(raw_door) + img.paste(tex, (12,0), tex) + + if (data & 0x03) == 2: + if not swung: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + img.paste(tex, (12,0), tex) + else: + tex = _transform_image_side(raw_door).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (12,6), tex) + + if (data & 0x03) == 3: + if not swung: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (12,6), tex) + else: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + img.paste(tex, (0,6), tex) + + return (img.convert("RGB"), img.split()[3]) + return None From 60966ffa73afdcb7ce068cd6ea20b08b214a915d Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Sat, 2 Oct 2010 20:17:13 -0400 Subject: [PATCH 15/27] Improve efficiency of special textures by pre-computing them --- chunk.py | 9 ++++++--- textures.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/chunk.py b/chunk.py index a1deab1..d109ee2 100644 --- a/chunk.py +++ b/chunk.py @@ -273,11 +273,14 @@ class ChunkRenderer(object): # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - ## minecart track, crops, ladder, doors - if blockid in (66,59,61,62, 65,64,71): + ## minecart track, crops, ladder, doors, etc. + if blockid in textures.special_blocks: # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] - t = textures.generate_special_texture(blockid, ancilData) + try: + t = textures.specialblockmap[(blockid, ancilData)] + except KeyError: + t = None else: t = textures.blockmap[blockid] diff --git a/textures.py b/textures.py index 755dae7..940c70c 100644 --- a/textures.py +++ b/textures.py @@ -474,3 +474,25 @@ def generate_special_texture(blockID, data): return None + + +# This set holds block ids that require special pre-computing. These are typically +# things that require ancillary data to render properly (i.e. ladder plus orientation) +special_blocks = set([66,59,61,62, 65,64,71]) + +# this is a map of special blockIDs to a list of all +# possible values for ancillary data that it might have. +special_map = {} +special_map[66] = range(10) # minecrart tracks +special_map[59] = range(8) # crops +special_map[61] = (0,) # furnace +special_map[62] = (0,) # burning furnace +special_map[65] = (2,3,4,5) # ladder +special_map[64] = range(16) # wooden door +special_map[71] = range(16) # iron door + +specialblockmap = {} + +for blockID in special_blocks: + for data in special_map[blockID]: + specialblockmap[(blockID, data)] = generate_special_texture(blockID, data) From 05770468fb9f7698d41e2069e2e30bc9bbf1bcc9 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Tue, 5 Oct 2010 15:29:22 +1100 Subject: [PATCH 16/27] More useful error if invalid --chunklist specified. --- world.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/world.py b/world.py index 25157d1..2e6d177 100644 --- a/world.py +++ b/world.py @@ -121,6 +121,11 @@ class WorldRenderer(object): chunklist.append((base36decode(p[1]), base36decode(p[2]), path)) + if not chunklist: + logging.error("No valid chunks specified in your chunklist!") + logging.error("HINT: chunks are in your world directory and have names of the form 'c.*.*.dat'") + sys.exit(1) + # Translate to col, row coordinates _, _, _, _, chunklist = _convert_coords(chunklist) @@ -205,8 +210,6 @@ class WorldRenderer(object): p = f.split(".") all_chunks.append((base36decode(p[1]), base36decode(p[2]), os.path.join(dirpath, f))) - logging.debug((base36decode(p[1]), base36decode(p[2]), - os.path.join(dirpath, f))) if not all_chunks: logging.error("Error: No chunks found!") From a5ae7032584889362fc7ea1e342cd26190c799bb Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Sat, 9 Oct 2010 15:44:48 -0400 Subject: [PATCH 17/27] added support for ploylines and polygons on the google map Polygons and polylines are read from the new file "regions.js". Polylines (entries with "closed" set to false) are just lines drawn on the map. Polygons (entries with "closed" set to true) are closed loops that are filled in with a transparent color. --- quadtree.py | 13 +++++++++++-- template.html | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/quadtree.py b/quadtree.py index 553c67f..27e3472 100644 --- a/quadtree.py +++ b/quadtree.py @@ -128,11 +128,20 @@ class QuadtreeGen(object): with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) - - + # write out the default marker table with open(os.path.join(self.destdir, "markers.js"), 'w') as output: output.write("var markerData=%s" % json.dumps(self.world.POI)) + # write out the default (empty, but documented) region table + with open(os.path.join(self.destdir, "regions.js"), 'w') as output: + output.write('var regionData=[\n'); + output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') + output.write(' // {"x": 0, "y": 0, "z": 0},\n') + output.write(' // {"x": 0, "y": 10, "z": 0},\n') + output.write(' // {"x": 0, "y": 0, "z": 10}\n') + output.write(' // ]},\n') + output.write('];'); + # Write a blank image blank = Image.new("RGBA", (1,1)) tileDir = os.path.join(self.destdir, "tiles") diff --git a/template.html b/template.html index 4387e8a..b304077 100644 --- a/template.html +++ b/template.html @@ -8,6 +8,7 @@ #mcmap { height: 100% } + @@ -155,6 +156,48 @@ } } + var regionsInit = false; + function initRegions() { + if (regionsInit) { return; } + + regionsInit = true; + + for (i in regionData) { + var region = regionData[i]; + var converted = new google.maps.MVCArray(); + for (j in region.path) { + var point = region.path[j]; + converted.push(fromWorldToLatLng(point.x, point.y, point.z)); + } + + if (region.closed) { + new google.maps.Polygon({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + fillColor: region.color, + fillOpacity: region.opacity * 0.25, + zIndex: i, + paths: converted + }); + } else { + new google.maps.Polyline({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + zIndex: i, + path: converted + }); + } + } + } + function initialize() { var mapOptions = { zoom: config.defaultZoom, @@ -187,8 +230,9 @@ // We can now set the map to use the 'coordinate' map type map.setMapTypeId('mcmap'); - // initialize the markers + // initialize the markers and regions initMarkers(); + initRegions(); } From bcb423ace48612b00f659af901f4f0cd6e3107d7 Mon Sep 17 00:00:00 2001 From: Kyle Brantley Date: Sat, 9 Oct 2010 22:57:20 -0600 Subject: [PATCH 18/27] Add --optimize-img={1,2} -- performs postprocessing on images This adds basic post-processing to images. At the moment, it only performs actions for the png output type, but changes to work for jpeg will be exceptionall minimal. --- gmap.py | 8 +++++++- optimizeimages.py | 21 +++++++++++++++++++++ quadtree.py | 24 +++++++++++++++++------- 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 optimizeimages.py diff --git a/gmap.py b/gmap.py index aad5959..cf5a517 100755 --- a/gmap.py +++ b/gmap.py @@ -49,6 +49,7 @@ def main(): parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.") + parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%") parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") @@ -98,6 +99,11 @@ def main(): else: imgformat = 'png' + if options.optimizeimg: + optimizeimg = options.optimizeimg + else: + optimizeimg = None + logging.getLogger().setLevel( logging.getLogger().level + 10*options.quiet) logging.getLogger().setLevel( @@ -111,7 +117,7 @@ def main(): w.go(options.procs) # Now generate the tiles - q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat) + q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg) q.go(options.procs) def delete_all(worlddir, tiledir): diff --git a/optimizeimages.py b/optimizeimages.py new file mode 100644 index 0000000..f5e87d2 --- /dev/null +++ b/optimizeimages.py @@ -0,0 +1,21 @@ +import os +import subprocess +import shlex + +def optimize_image(imgpath, imgformat, optimizeimg): + if imgformat == 'png': + if optimizeimg == "1" or optimizeimg == "2": + # 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(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), + stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] + os.remove(imgpath) + os.rename(imgpath+".tmp", imgpath) + + if optimizeimg == "2": + subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + diff --git a/quadtree.py b/quadtree.py index 553c67f..53d34b8 100644 --- a/quadtree.py +++ b/quadtree.py @@ -24,10 +24,11 @@ import shutil import collections import json import logging +import util from PIL import Image +from optimizeimages import optimize_image -import util """ This module has routines related to generating a quadtree of tiles @@ -55,7 +56,7 @@ def catch_keyboardinterrupt(func): return newfunc class QuadtreeGen(object): - def __init__(self, worldobj, destdir, depth=None, imgformat=None): + def __init__(self, worldobj, destdir, depth=None, imgformat=None, optimizeimg=None): """Generates a quadtree from the world given into the given dest directory @@ -67,6 +68,7 @@ class QuadtreeGen(object): """ assert(imgformat) self.imgformat = imgformat + self.optimizeimg = optimizeimg if depth is None: # Determine quadtree depth (midpoint is always 0,0) @@ -235,7 +237,8 @@ class QuadtreeGen(object): # (even if tilechunks is empty, render_worldtile will delete # existing images if appropriate) yield pool.apply_async(func=render_worldtile, args= (tilechunks, - colstart, colend, rowstart, rowend, dest, self.imgformat)) + colstart, colend, rowstart, rowend, dest, self.imgformat, + self.optimizeimg)) def _apply_render_inntertile(self, pool, zoom): """Same as _apply_render_worltiles but for the inntertile routine. @@ -247,7 +250,7 @@ class QuadtreeGen(object): dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path[:-1])) name = str(path[-1]) - yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat)) + yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat, self.optimizeimg)) def go(self, procs): """Renders all tiles""" @@ -331,7 +334,7 @@ class QuadtreeGen(object): pool.join() # Do the final one right here: - render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat) + render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat, self.optimizeimg) def _get_range_by_path(self, path): """Returns the x, y chunk coordinates of this tile""" @@ -362,7 +365,7 @@ class QuadtreeGen(object): return chunklist @catch_keyboardinterrupt -def render_innertile(dest, name, imgformat): +def render_innertile(dest, name, imgformat, optimizeimg): """ Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from os.path.join(dest, name, "{0,1,2,3}.png") @@ -452,12 +455,15 @@ def render_innertile(dest, name, imgformat): img.save(imgpath, quality=95, subsampling=0) else: # png img.save(imgpath) + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(newhash) @catch_keyboardinterrupt -def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat): +def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat, optimizeimg): """Renders just the specified chunks into a tile and save it. Unlike usual python conventions, rowend and colend are inclusive. Additionally, the chunks around the edges are half-way cut off (so that neighboring tiles @@ -586,6 +592,10 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # Save them tileimg.save(imgpath) + + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(digest) From 20e124b778650f2cf92852c9102a8b9bc8db321e Mon Sep 17 00:00:00 2001 From: Kyle Brantley Date: Sat, 9 Oct 2010 23:52:54 -0600 Subject: [PATCH 19/27] Added license to optimizeimages.py --- optimizeimages.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/optimizeimages.py b/optimizeimages.py index f5e87d2..8e34a91 100644 --- a/optimizeimages.py +++ b/optimizeimages.py @@ -1,3 +1,18 @@ +# 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 . + import os import subprocess import shlex From ea94bcc9164314a0fcf1646c0dc9171d0b2a5389 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 11 Oct 2010 20:56:43 -0400 Subject: [PATCH 20/27] removed your crazy semicolons. This ain't java =) --- quadtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadtree.py b/quadtree.py index 27e3472..746f381 100644 --- a/quadtree.py +++ b/quadtree.py @@ -134,13 +134,13 @@ class QuadtreeGen(object): # write out the default (empty, but documented) region table with open(os.path.join(self.destdir, "regions.js"), 'w') as output: - output.write('var regionData=[\n'); + output.write('var regionData=[\n') output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') output.write(' // {"x": 0, "y": 0, "z": 0},\n') output.write(' // {"x": 0, "y": 10, "z": 0},\n') output.write(' // {"x": 0, "y": 0, "z": 10}\n') output.write(' // ]},\n') - output.write('];'); + output.write('];') # Write a blank image blank = Image.new("RGBA", (1,1)) From a4c0e786a1b9389471b05824cc31369491b2e47c Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 11 Oct 2010 21:03:50 -0400 Subject: [PATCH 21/27] tabs -> 4 spaces --- optimizeimages.py | 18 +++++++++--------- quadtree.py | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/optimizeimages.py b/optimizeimages.py index 8e34a91..120b60b 100644 --- a/optimizeimages.py +++ b/optimizeimages.py @@ -19,18 +19,18 @@ import shlex def optimize_image(imgpath, imgformat, optimizeimg): if imgformat == 'png': - if optimizeimg == "1" or optimizeimg == "2": + if optimizeimg == "1" or optimizeimg == "2": # 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(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), - stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] - os.remove(imgpath) + subprocess.Popen(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), + stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] + os.remove(imgpath) os.rename(imgpath+".tmp", imgpath) - if optimizeimg == "2": - subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, - stdout=subprocess.PIPE).communicate()[0] - subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, - stdout=subprocess.PIPE).communicate()[0] + if optimizeimg == "2": + subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] diff --git a/quadtree.py b/quadtree.py index d97f3cb..8bd84ee 100644 --- a/quadtree.py +++ b/quadtree.py @@ -27,6 +27,7 @@ import logging import util from PIL import Image + from optimizeimages import optimize_image @@ -603,7 +604,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat tileimg.save(imgpath) if optimizeimg: - optimize_image(imgpath, imgformat, optimizeimg) + optimize_image(imgpath, imgformat, optimizeimg) with open(hashpath, "wb") as hashout: hashout.write(digest) From 33b6230babb21025a780fdf35535b3d35f08ede9 Mon Sep 17 00:00:00 2001 From: Gregory Short Date: Mon, 11 Oct 2010 23:41:59 -0500 Subject: [PATCH 22/27] Google released an update to v3 of the gmap api which enables streetview by default. This change explicitly disables it. --- template.html | 1 + 1 file changed, 1 insertion(+) diff --git a/template.html b/template.html index b304077..96ccae2 100644 --- a/template.html +++ b/template.html @@ -205,6 +205,7 @@ navigationControl: true, scaleControl: false, mapTypeControl: false, + streetViewControl: false, mapTypeId: 'mcmap' }; map = new google.maps.Map(document.getElementById("mcmap"), mapOptions); From 8b7eddf2bfa01e7520f633b169336e829b2388fc Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 12 Oct 2010 00:45:40 -0400 Subject: [PATCH 23/27] moved iteration code to its own function --- chunk.py | 187 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/chunk.py b/chunk.py index 35c14c8..b1dea08 100644 --- a/chunk.py +++ b/chunk.py @@ -64,6 +64,21 @@ def get_blockdata_array(level): in a similar manner to skylight data""" return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) +def iterate_chunkblocks(xoff,yoff): + """Iterates over the 16x16x128 blocks of a chunk in rendering order. + Yields (x,y,z,imgx,imgy) + x,y,z is the block coordinate in the chunk + imgx,imgy is the image offset in the chunk image where that block should go + """ + for x in xrange(15,-1,-1): + for y in xrange(16): + imgx = xoff + x*12 + y*12 + imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 + for z in xrange(128): + yield x,y,z,imgx,imgy + imgy -= 12 + + # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85]) @@ -260,104 +275,94 @@ class ChunkRenderer(object): if not img: img = Image.new("RGBA", (384, 1728), (38,92,255,0)) - for x in xrange(15,-1,-1): - for y in xrange(16): - imgx = xoff + x*12 + y*12 - imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 - for z in xrange(128): - try: - blockid = blocks[x,y,z] + for x,y,z,imgx,imgy in iterate_chunkblocks(xoff,yoff): + blockid = blocks[x,y,z] - # the following blocks don't have textures that can be pre-computed from the blockid - # alone. additional data is required. - # TODO torches, redstone torches, crops, ladders, stairs, - # levers, doors, buttons, and signs all need to be handled here (and in textures.py) + # the following blocks don't have textures that can be pre-computed from the blockid + # alone. additional data is required. + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - ## minecart track, crops, ladder, doors, etc. - if blockid in textures.special_blocks: - # also handle furnaces here, since one side has a different texture than the other - ancilData = blockData_expanded[x,y,z] - try: - t = textures.specialblockmap[(blockid, ancilData)] - except KeyError: - t = None + ## minecart track, crops, ladder, doors, etc. + if blockid in textures.special_blocks: + # also handle furnaces here, since one side has a different texture than the other + ancilData = blockData_expanded[x,y,z] + try: + t = textures.specialblockmap[(blockid, ancilData)] + except KeyError: + t = None - else: - t = textures.blockmap[blockid] - if not t: - continue + else: + t = textures.blockmap[blockid] + if not t: + continue - # Check if this block is occluded - if cave and ( - x == 0 and y != 15 and z != 127 - ): - # If it's on the x face, only render if there's a - # transparent block in the y+1 direction OR the z-1 - # direction - if ( - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x != 0 and z != 127 - ): - # If it's on the facing y face, only render if there's - # a transparent block in the x-1 direction OR the z-1 - # direction - if ( - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x == 0 - ): - # If it's on the facing edge, only render if what's - # above it is transparent - if ( - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif ( - # Normal block or not cave mode, check sides for - # transparentcy or render unconditionally if it's - # on a shown face - x != 0 and y != 15 and z != 127 and - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - # Don't render if all sides aren't transparent and - # we're not on the edge - continue + # Check if this block is occluded + if cave and ( + x == 0 and y != 15 and z != 127 + ): + # If it's on the x face, only render if there's a + # transparent block in the y+1 direction OR the z-1 + # direction + if ( + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x != 0 and z != 127 + ): + # If it's on the facing y face, only render if there's + # a transparent block in the x-1 direction OR the z-1 + # direction + if ( + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x == 0 + ): + # If it's on the facing edge, only render if what's + # above it is transparent + if ( + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif ( + # Normal block or not cave mode, check sides for + # transparentcy or render unconditionally if it's + # on a shown face + x != 0 and y != 15 and z != 127 and + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + # Don't render if all sides aren't transparent and + # we're not on the edge + continue - # Draw the actual block on the image. For cave images, - # tint the block with a color proportional to its depth - if cave: - img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) - else: - img.paste(t[0], (imgx, imgy), t[1]) + # Draw the actual block on the image. For cave images, + # tint the block with a color proportional to its depth + if cave: + img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) + else: + img.paste(t[0], (imgx, imgy), t[1]) - # Draw edge lines - if blockid in (44,): # step block - increment = 6 - elif blockid in (78,): # snow - increment = 9 - else: - increment = 0 + # Draw edge lines + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 - if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn - draw = ImageDraw.Draw(img) - if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) - if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) - - - finally: - # Do this no mater how the above block exits - imgy -= 12 + if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn + draw = ImageDraw.Draw(img) + if x != 15 and blocks[x+1,y,z] == 0: + draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) + if y != 0 and blocks[x,y-1,z] == 0: + draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) return img From a2f37e0649c3eed926f16a946ef6ab2eda9d7297 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 15 Oct 2010 21:55:00 -0400 Subject: [PATCH 24/27] made exceptions during innter tile rendering when opening tile images non-fatal --- quadtree.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/quadtree.py b/quadtree.py index 8bd84ee..3c00419 100644 --- a/quadtree.py +++ b/quadtree.py @@ -448,17 +448,29 @@ def render_innertile(dest, name, imgformat, optimizeimg): img = Image.new("RGBA", (384, 384), (38,92,255,0)) if q0path: - quad0 = Image.open(q0path).resize((192,192), Image.ANTIALIAS) - img.paste(quad0, (0,0)) + try: + quad0 = Image.open(q0path).resize((192,192), Image.ANTIALIAS) + img.paste(quad0, (0,0)) + except Exception, e: + logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q0path, e) if q1path: - quad1 = Image.open(q1path).resize((192,192), Image.ANTIALIAS) - img.paste(quad1, (192,0)) + try: + quad1 = Image.open(q1path).resize((192,192), Image.ANTIALIAS) + img.paste(quad1, (192,0)) + except Exception, e: + logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q1path, e) if q2path: - quad2 = Image.open(q2path).resize((192,192), Image.ANTIALIAS) - img.paste(quad2, (0, 192)) + try: + quad2 = Image.open(q2path).resize((192,192), Image.ANTIALIAS) + img.paste(quad2, (0, 192)) + except Exception, e: + logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q2path, e) if q3path: - quad3 = Image.open(q3path).resize((192,192), Image.ANTIALIAS) - img.paste(quad3, (192, 192)) + try: + quad3 = Image.open(q3path).resize((192,192), Image.ANTIALIAS) + img.paste(quad3, (192, 192)) + except Exception, e: + logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q3path, e) # Save it if imgformat == 'jpg': From 324ac5d28d3f0732e49f0c44db6bbc65ab335c63 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 15 Oct 2010 22:07:46 -0400 Subject: [PATCH 25/27] updates mtime on chunk images that match the hash --- chunk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chunk.py b/chunk.py index b1dea08..763d433 100644 --- a/chunk.py +++ b/chunk.py @@ -187,7 +187,7 @@ class ChunkRenderer(object): # An image exists? Instead of checking the hash which is kinda # expensive (for tens of thousands of chunks, yes it is) check if # the mtime of the chunk file is newer than the mtime of oldimg - if os.path.getmtime(self.chunkfile) < os.path.getmtime(oldimg_path): + if os.path.getmtime(self.chunkfile) <= os.path.getmtime(oldimg_path): # chunkfile is older than the image, don't even bother checking # the hash return oldimg_path @@ -212,6 +212,9 @@ class ChunkRenderer(object): if dest_filename == oldimg: # There is an existing file, the chunk has a newer mtime, but the # hashes match. + # Before we return it, update its mtime so the next round + # doesn't have to check the hash + os.utime(dest_path, None) return dest_path else: # Remove old image for this chunk. Anything already existing is From ea179118bdb039db5d292210efb2036eab710527 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 15 Oct 2010 22:23:15 -0400 Subject: [PATCH 26/27] added an option to skip generating region.js and marker.js --- gmap.py | 2 ++ quadtree.py | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/gmap.py b/gmap.py index cf5a517..59ce6f3 100755 --- a/gmap.py +++ b/gmap.py @@ -52,6 +52,7 @@ def main(): parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%") parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") + parser.add_option("--skip-js", dest="skipjs", action="store_true", help="Don't output marker.js or regions.js") options, args = parser.parse_args() @@ -118,6 +119,7 @@ def main(): # Now generate the tiles q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg) + q.write_html(options.skipjs) q.go(options.procs) def delete_all(worlddir, tiledir): diff --git a/quadtree.py b/quadtree.py index 3c00419..0f2ea2f 100644 --- a/quadtree.py +++ b/quadtree.py @@ -118,8 +118,10 @@ class QuadtreeGen(object): logging.info("{0}/{1} tiles complete on level {2}/{3}".format( complete, total, level, self.p)) - def write_html(self, zoomlevel, imgformat): - """Writes out index.html""" + def write_html(self, skipjs=False): + """Writes out index.html, marker.js, and region.js""" + zoomlevel = self.p + imgformat = self.imgformat templatepath = os.path.join(util.get_program_path(), "template.html") html = open(templatepath, 'r').read() @@ -131,6 +133,15 @@ class QuadtreeGen(object): with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) + # Write a blank image + blank = Image.new("RGBA", (1,1)) + tileDir = os.path.join(self.destdir, "tiles") + if not os.path.exists(tileDir): os.mkdir(tileDir) + blank.save(os.path.join(tileDir, "blank."+self.imgformat)) + + if skipjs: + return + # write out the default marker table with open(os.path.join(self.destdir, "markers.js"), 'w') as output: output.write("var markerData=%s" % json.dumps(self.world.POI)) @@ -145,12 +156,6 @@ class QuadtreeGen(object): output.write(' // ]},\n') output.write('];') - # Write a blank image - blank = Image.new("RGBA", (1,1)) - tileDir = os.path.join(self.destdir, "tiles") - if not os.path.exists(tileDir): os.mkdir(tileDir) - blank.save(os.path.join(tileDir, "blank."+self.imgformat)) - def _get_cur_depth(self): """How deep is the quadtree currently in the destdir? This glances in index.html to see what maxZoom is set to. @@ -287,8 +292,6 @@ class QuadtreeGen(object): else: pool = multiprocessing.Pool(processes=procs) - self.write_html(self.p, self.imgformat) - # Render the highest level of tiles from the chunks results = collections.deque() complete = 0 From 4c65271dd023535dd81dc0cd49aeb75fd545d64d Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 15 Oct 2010 22:43:40 -0400 Subject: [PATCH 27/27] doesn't crash on a corrupt world chunk --- chunk.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/chunk.py b/chunk.py index 763d433..1980517 100644 --- a/chunk.py +++ b/chunk.py @@ -17,6 +17,7 @@ import numpy from PIL import Image, ImageDraw import os.path import hashlib +import logging import nbt import textures @@ -91,6 +92,9 @@ def render_and_save(chunkfile, cachedir, cave=False): a = ChunkRenderer(chunkfile, cachedir) try: return a.render_and_save(cave) + except ChunkCorrupt: + # This should be non-fatal, but should print a warning + pass except Exception, e: import traceback traceback.print_exc() @@ -104,6 +108,9 @@ def render_and_save(chunkfile, cachedir, cave=False): # forever for it to finish. raise Exception() +class ChunkCorrupt(Exception): + pass + class ChunkRenderer(object): def __init__(self, chunkfile, cachedir): """Make a new chunk renderer for the given chunkfile. @@ -134,7 +141,11 @@ class ChunkRenderer(object): def _load_level(self): """Loads and returns the level structure""" if not hasattr(self, "_level"): - self._level = get_lvldata(self.chunkfile) + try: + self._level = get_lvldata(self.chunkfile) + except Exception, e: + logging.warning("Error opening chunk file %s. It may be corrupt. %s", self.chunkfile, e) + raise ChunkCorrupt(str(e)) return self._level level = property(_load_level)