copied DirtyTiles and Tile objects to tileset.py
This commit is contained in:
@@ -18,8 +18,9 @@ import os.path
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
import itertools
|
||||||
|
|
||||||
from . import util
|
from .util import iterate_base4, convert_coords
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ class TileSet(object):
|
|||||||
|
|
||||||
for c_x, c_z, _ in self.regionset.iterate_chunks():
|
for c_x, c_z, _ in self.regionset.iterate_chunks():
|
||||||
# Convert these coordinates to row/col
|
# Convert these coordinates to row/col
|
||||||
col, row = util.convert_coords(c_x, c_z)
|
col, row = convert_coords(c_x, c_z)
|
||||||
|
|
||||||
minrow = min(minrow, row)
|
minrow = min(minrow, row)
|
||||||
maxrow = max(maxrow, row)
|
maxrow = max(maxrow, row)
|
||||||
@@ -360,3 +361,309 @@ def get_dirdepth(outputdir):
|
|||||||
|
|
||||||
return depth
|
return depth
|
||||||
|
|
||||||
|
class DirtyTiles(object):
|
||||||
|
"""This tree holds which tiles need rendering.
|
||||||
|
Each instance is a node, and the root of a subtree.
|
||||||
|
|
||||||
|
Each node knows its "level", which corresponds to the zoom level where 0 is
|
||||||
|
the inner-most (most zoomed in) tiles.
|
||||||
|
|
||||||
|
Instances hold the clean/dirty state of their children. Leaf nodes are
|
||||||
|
images and do not physically exist in the tree, level 1 nodes keep track of
|
||||||
|
leaf image state. Level 2 nodes keep track of level 1 state, and so fourth.
|
||||||
|
|
||||||
|
In attempt to keep things memory efficient, subtrees that are completely
|
||||||
|
dirty are collapsed
|
||||||
|
|
||||||
|
"""
|
||||||
|
__slots__ = ("depth", "children")
|
||||||
|
def __init__(self, depth):
|
||||||
|
"""Initialize a new tree with the specified depth. This actually
|
||||||
|
initializes a node, which is the root of a subtree, with `depth` levels
|
||||||
|
beneath it.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Stores the depth of the tree according to this node. This is not the
|
||||||
|
# depth of this node, but rather the number of levels below this node
|
||||||
|
# (including this node).
|
||||||
|
self.depth = depth
|
||||||
|
|
||||||
|
# the self.children array holds the 4 children of this node. This
|
||||||
|
# follows the same quadtree convention as elsewhere: children 0, 1, 2,
|
||||||
|
# 3 are the upper-left, upper-right, lower-left, and lower-right
|
||||||
|
# respectively
|
||||||
|
# Values are:
|
||||||
|
# False
|
||||||
|
# All children down this subtree are clean
|
||||||
|
# True
|
||||||
|
# All children down this subtree are dirty
|
||||||
|
# A DirtyTiles instance
|
||||||
|
# the instance defines which children down that subtree are
|
||||||
|
# clean/dirty.
|
||||||
|
# A node with depth=1 cannot have a DirtyTiles instance in its
|
||||||
|
# children since its leaves are images, not more tree
|
||||||
|
self.children = [False] * 4
|
||||||
|
|
||||||
|
def set_dirty(self, path):
|
||||||
|
"""Marks the requested leaf node as "dirty".
|
||||||
|
|
||||||
|
Path is an iterable of integers representing the path to the leaf node
|
||||||
|
that is requested to be marked as dirty.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = list(path)
|
||||||
|
assert len(path) == self.depth
|
||||||
|
path.reverse()
|
||||||
|
self._set_dirty_helper(path)
|
||||||
|
|
||||||
|
def _set_dirty_helper(self, path):
|
||||||
|
"""Recursive call for set_dirty()
|
||||||
|
|
||||||
|
Expects path to be a list in reversed order
|
||||||
|
|
||||||
|
If *all* the nodes below this one are dirty, this function returns
|
||||||
|
true. Otherwise, returns None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.depth == 1:
|
||||||
|
# Base case
|
||||||
|
self.children[path[0]] = True
|
||||||
|
|
||||||
|
# Check to see if all children are dirty
|
||||||
|
if all(self.children):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Recursive case
|
||||||
|
|
||||||
|
childnum = path.pop()
|
||||||
|
child = self.children[childnum]
|
||||||
|
|
||||||
|
if child == False:
|
||||||
|
# Create a new node
|
||||||
|
child = self.__class__(self.depth-1)
|
||||||
|
child._set_dirty_helper(path)
|
||||||
|
self.children[childnum] = child
|
||||||
|
elif child == True:
|
||||||
|
# Every child is already dirty. Nothing to do.
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# subtree is mixed clean/dirty. Recurse
|
||||||
|
ret = child._set_dirty_helper(path)
|
||||||
|
if ret:
|
||||||
|
# Child says it's completely dirty, so we can purge the
|
||||||
|
# subtree and mark it as dirty. The subtree will be garbage
|
||||||
|
# collected when this method exits.
|
||||||
|
self.children[childnum] = True
|
||||||
|
|
||||||
|
# Since we've marked an entire sub-tree as dirty, we may be
|
||||||
|
# able to signal to our parent
|
||||||
|
if all(x is True for x in self.children):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def iterate_dirty(self, level=None):
|
||||||
|
"""Returns an iterator over every dirty tile in this subtree. Each item
|
||||||
|
yielded is a sequence of integers representing the quadtree path to the
|
||||||
|
dirty tile. Yielded sequences are of length self.depth.
|
||||||
|
|
||||||
|
If level is None, iterates over tiles of the highest level, i.e.
|
||||||
|
worldtiles. If level is a value between 0 and the depth of this tree,
|
||||||
|
this method iterates over tiles at that level. Zoom level 0 is zoomed
|
||||||
|
all the way out, zoom level `depth` is all the way in.
|
||||||
|
|
||||||
|
In other words, specifying level causes the tree to be iterated as if
|
||||||
|
it was only that depth.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if level is None:
|
||||||
|
todepth = 1
|
||||||
|
else:
|
||||||
|
if not (level > 0 and level <= self.depth):
|
||||||
|
raise ValueError("Level parameter must be between 1 and %s" % self.depth)
|
||||||
|
todepth = self.depth - level + 1
|
||||||
|
|
||||||
|
return (tuple(reversed(rpath)) for rpath in self._iterate_dirty_helper(todepth))
|
||||||
|
|
||||||
|
def _iterate_dirty_helper(self, todepth):
|
||||||
|
if self.depth == todepth:
|
||||||
|
# Base case
|
||||||
|
if self.children[0]: yield [0]
|
||||||
|
if self.children[1]: yield [1]
|
||||||
|
if self.children[2]: yield [2]
|
||||||
|
if self.children[3]: yield [3]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Higher levels:
|
||||||
|
for c, child in enumerate(self.children):
|
||||||
|
if child == True:
|
||||||
|
# All dirty down this subtree, iterate over every leaf
|
||||||
|
for x in iterate_base4(self.depth-todepth):
|
||||||
|
x = list(x)
|
||||||
|
x.append(c)
|
||||||
|
yield x
|
||||||
|
elif child != False:
|
||||||
|
# Mixed dirty/clean down this subtree, recurse
|
||||||
|
for path in child._iterate_dirty_helper(todepth):
|
||||||
|
path.append(c)
|
||||||
|
yield path
|
||||||
|
|
||||||
|
def query_path(self, path):
|
||||||
|
"""Queries for the state of the given tile in the tree.
|
||||||
|
|
||||||
|
Returns False for "clean", True for "dirty"
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Traverse the tree down the given path. If the tree has been
|
||||||
|
# collapsed, then just return what the subtree is. Otherwise, if we
|
||||||
|
# find the specific DirtyTree requested, return its state using the
|
||||||
|
# __nonzero__ call.
|
||||||
|
treenode = self
|
||||||
|
for pathelement in path:
|
||||||
|
treenode = treenode.children[pathelement]
|
||||||
|
if not isinstance(treenode, DirtyTiles):
|
||||||
|
return treenode
|
||||||
|
|
||||||
|
# If the method has not returned at this point, treenode is the
|
||||||
|
# requested node, but it is an inner node with possibly mixed state
|
||||||
|
# subtrees. If any of the children are True return True. This call
|
||||||
|
# relies on the __nonzero__ method
|
||||||
|
return bool(treenode)
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
"""Returns the boolean context of this particular node. If any
|
||||||
|
descendent of this node is True return True. Otherwise, False.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Any chilren that are True or are DirtyTiles that evaluate to True
|
||||||
|
# IDEA: look at all children for True before recursing
|
||||||
|
# Better idea: every node except the root /must/ have a dirty
|
||||||
|
# descendent or it wouldn't exist. This assumption is only valid as
|
||||||
|
# long as an unset_dirty() method or similar does not exist.
|
||||||
|
return any(self.children)
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
"""Returns the total number of dirty leaf nodes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: Make this more efficient (although for even the largest trees,
|
||||||
|
# this takes only seconds)
|
||||||
|
c = 0
|
||||||
|
for _ in self.iterate_dirty():
|
||||||
|
c += 1
|
||||||
|
return c
|
||||||
|
|
||||||
|
class Tile(object):
|
||||||
|
"""A simple container class that represents a single render-tile.
|
||||||
|
|
||||||
|
A render-tile is a tile that is rendered, not a tile composed of other
|
||||||
|
tiles (composite-tile).
|
||||||
|
|
||||||
|
"""
|
||||||
|
__slots__ = ("col", "row", "path")
|
||||||
|
def __init__(self, col, row, path):
|
||||||
|
"""Initialize the tile obj with the given parameters. It's probably
|
||||||
|
better to use one of the other constructors though
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.col = col
|
||||||
|
self.row = row
|
||||||
|
self.path = tuple(path)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(%r,%r,%r)" % (self.__class__.__name__, self.col, self.row, self.path)
|
||||||
|
|
||||||
|
def __eq__(self,other):
|
||||||
|
return self.col == other.col and self.row == other.row and tuple(self.path) == tuple(other.path)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
def get_filepath(self, tiledir, imgformat):
|
||||||
|
"""Returns the path to this file given the directory to the tiles
|
||||||
|
|
||||||
|
"""
|
||||||
|
# os.path.join would be the proper way to do this path concatenation,
|
||||||
|
# but it is surprisingly slow, probably because it checks each path
|
||||||
|
# element if it begins with a slash. Since we know these components are
|
||||||
|
# all relative, just concatinate with os.path.sep
|
||||||
|
pathcomponents = [tiledir]
|
||||||
|
pathcomponents.extend(str(x) for x in self.path)
|
||||||
|
path = os.path.sep.join(pathcomponents)
|
||||||
|
imgpath = ".".join((path, imgformat))
|
||||||
|
return imgpath
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_path(cls, path):
|
||||||
|
"""Constructor that takes a path and computes the col,row address of
|
||||||
|
the tile and constructs a new tile object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = tuple(path)
|
||||||
|
|
||||||
|
depth = len(path)
|
||||||
|
|
||||||
|
# Radius of the world in chunk cols/rows
|
||||||
|
# (Diameter in X is 2**depth, divided by 2 for a radius, multiplied by
|
||||||
|
# 2 for 2 chunks per tile. Similarly for Y)
|
||||||
|
xradius = 2**depth
|
||||||
|
yradius = 2*2**depth
|
||||||
|
|
||||||
|
col = -xradius
|
||||||
|
row = -yradius
|
||||||
|
xsize = xradius
|
||||||
|
ysize = yradius
|
||||||
|
|
||||||
|
for p in path:
|
||||||
|
if p in (1,3):
|
||||||
|
col += xsize
|
||||||
|
if p in (2,3):
|
||||||
|
row += ysize
|
||||||
|
xsize //= 2
|
||||||
|
ysize //= 2
|
||||||
|
|
||||||
|
return cls(col, row, path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_path(cls, col, row, depth):
|
||||||
|
"""Constructor that takes a col,row of a tile and computes the path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert col % 2 == 0
|
||||||
|
assert row % 4 == 0
|
||||||
|
|
||||||
|
xradius = 2**depth
|
||||||
|
yradius = 2*2**depth
|
||||||
|
|
||||||
|
colbounds = [-xradius, xradius]
|
||||||
|
rowbounds = [-yradius, yradius]
|
||||||
|
|
||||||
|
path = []
|
||||||
|
|
||||||
|
for level in xrange(depth):
|
||||||
|
# Strategy: Find the midpoint of this level, and determine which
|
||||||
|
# quadrant this row/col is in. Then set the bounds to that level
|
||||||
|
# and repeat
|
||||||
|
|
||||||
|
xmid = (colbounds[1] + colbounds[0]) // 2
|
||||||
|
ymid = (rowbounds[1] + rowbounds[0]) // 2
|
||||||
|
|
||||||
|
if col < xmid:
|
||||||
|
if row < ymid:
|
||||||
|
path.append(0)
|
||||||
|
colbounds[1] = xmid
|
||||||
|
rowbounds[1] = ymid
|
||||||
|
else:
|
||||||
|
path.append(2)
|
||||||
|
colbounds[1] = xmid
|
||||||
|
rowbounds[0] = ymid
|
||||||
|
else:
|
||||||
|
if row < ymid:
|
||||||
|
path.append(1)
|
||||||
|
colbounds[0] = xmid
|
||||||
|
rowbounds[1] = ymid
|
||||||
|
else:
|
||||||
|
path.append(3)
|
||||||
|
colbounds[0] = xmid
|
||||||
|
rowbounds[0] = ymid
|
||||||
|
|
||||||
|
return cls(col, row, path)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import logging
|
|||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
import ctypes
|
import ctypes
|
||||||
import platform
|
import platform
|
||||||
from itertools import cycle, islice
|
from itertools import cycle, islice, product
|
||||||
|
|
||||||
def get_program_path():
|
def get_program_path():
|
||||||
if hasattr(sys, "frozen") or imp.is_frozen("__main__"):
|
if hasattr(sys, "frozen") or imp.is_frozen("__main__"):
|
||||||
@@ -101,6 +101,11 @@ def roundrobin(iterables):
|
|||||||
pending -= 1
|
pending -= 1
|
||||||
nexts = cycle(islice(nexts, pending))
|
nexts = cycle(islice(nexts, pending))
|
||||||
|
|
||||||
|
def iterate_base4(d):
|
||||||
|
"""Iterates over a base 4 number with d digits"""
|
||||||
|
return product(xrange(4), repeat=d)
|
||||||
|
|
||||||
|
|
||||||
def convert_coords(chunkx, chunkz):
|
def convert_coords(chunkx, chunkz):
|
||||||
"""Takes a coordinate (chunkx, chunkz) where chunkx and chunkz are
|
"""Takes a coordinate (chunkx, chunkz) where chunkx and chunkz are
|
||||||
in the chunk coordinate system, and figures out the row and column
|
in the chunk coordinate system, and figures out the row and column
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from overviewer_core.quadtree import DirtyTiles, iterate_base4
|
from overviewer_core.tileset import DirtyTiles
|
||||||
|
from overviewer_core.util import iterate_base4
|
||||||
|
|
||||||
class DirtyTilesTest(unittest.TestCase):
|
class DirtyTilesTest(unittest.TestCase):
|
||||||
# If you change this definition, you must also change the hard-coded
|
# If you change this definition, you must also change the hard-coded
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from overviewer_core import quadtree
|
from overviewer_core.util import iterate_base4
|
||||||
from overviewer_core.quadtree import Tile
|
from overviewer_core.tileset import Tile
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
((-4,-8), (0,0)),
|
((-4,-8), (0,0)),
|
||||||
@@ -28,7 +28,7 @@ class TileTest(unittest.TestCase):
|
|||||||
given to compute_path
|
given to compute_path
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for path in quadtree.iterate_base4(7):
|
for path in iterate_base4(7):
|
||||||
t1 = Tile.from_path(path)
|
t1 = Tile.from_path(path)
|
||||||
col = t1.col
|
col = t1.col
|
||||||
row = t1.row
|
row = t1.row
|
||||||
|
|||||||
Reference in New Issue
Block a user