This is an automated email from the git hooks/post-receive script. sebastic pushed a commit to branch master in repository tilestache.
commit 1d7ed3dc221bfc59a1ee94b73043794dac615842 Author: Bas Couwenberg <sebas...@xs4all.nl> Date: Mon Oct 17 08:30:34 2016 +0200 Imported Upstream version 1.51+ds --- PKG-INFO | 2 +- TileStache/Core.py | 11 +- TileStache/Goodies/Providers/Composite.py | 346 +++++++++++++++--------------- TileStache/Goodies/Providers/GDAL.py | 50 ++--- TileStache/Mapnik.py | 134 ++++++------ TileStache/Pixels.py | 35 +-- TileStache/S3.py | 3 + TileStache/VERSION | 2 +- TileStache/Vector/__init__.py | 23 +- scripts/tilestache-seed.py | 10 + setup.py | 8 +- 11 files changed, 317 insertions(+), 307 deletions(-) diff --git a/PKG-INFO b/PKG-INFO index a75117a..06d4c36 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: TileStache -Version: 1.50.1 +Version: 1.51 Summary: A stylish alternative for caching your map tiles. Home-page: http://tilestache.org Author: Michal Migurski diff --git a/TileStache/Core.py b/TileStache/Core.py index 416d787..d325283 100644 --- a/TileStache/Core.py +++ b/TileStache/Core.py @@ -172,22 +172,20 @@ def _addRecentTile(layer, coord, format, body, age=300): logging.debug('TileStache.Core._addRecentTile() added tile to recent tiles: %s', key) # now look at the oldest keys and remove them if needed - for (key, due_by) in _recent_tiles['list']: + cutoff = 0 + for i, (key, due_by) in enumerate(_recent_tiles['list']): # new enough? if time() < due_by: + cutoff = i break logging.debug('TileStache.Core._addRecentTile() removed tile from recent tiles: %s', key) try: - _recent_tiles['list'].remove((key, due_by)) - except ValueError: - pass - - try: del _recent_tiles['hash'][key] except KeyError: pass + del _recent_tiles['list'][:cutoff] def _getRecentTile(layer, coord, format): """ Return the body of a recent tile, or None if it's not there. @@ -427,6 +425,7 @@ class Layer: except NoTileLeftBehind, e: tile = e.tile save = False + status_code = 404 if not self.write_cache: save = False diff --git a/TileStache/Goodies/Providers/Composite.py b/TileStache/Goodies/Providers/Composite.py index 659329a..5209af5 100644 --- a/TileStache/Goodies/Providers/Composite.py +++ b/TileStache/Goodies/Providers/Composite.py @@ -83,7 +83,7 @@ A complete example configuration might look like this: { "name": "Test" }, - "layers": + "layers": { "base": { @@ -134,7 +134,7 @@ Corresponding example stackfile XML: <?xml version="1.0"?> <stack> <layer src="base" /> - + <stack> <layer src="outlines"> <mask src="halos" /> @@ -185,24 +185,24 @@ class Provider: """ def __init__(self, layer, stack=None, stackfile=None): """ Make a new Composite.Provider. - + Arguments: - + layer: The current TileStache.Core.Layer - + stack: A list or dictionary with configuration for the image stack, parsed by build_stack(). Also acceptable is a URL to a JSON file. - + stackfile: *Deprecated* filename for an XML representation of the image stack. """ self.layer = layer - + if type(stack) in (str, unicode): stack = jsonload(urlopen(urljoin(layer.config.dirpath, stack)).read()) - + if type(stack) in (list, dict): self.stack = build_stack(stack) @@ -212,21 +212,21 @@ class Provider: # stackfile = pathjoin(self.layer.config.dirpath, stackfile) stack = parseXML(stackfile).firstChild - + assert stack.tagName == 'stack', \ 'Expecting root element "stack" but got "%s"' % stack.tagName - + self.stack = makeStack(stack) - + else: raise Exception('Note sure what to do with this stack argument: %s' % repr(stack)) - + def renderTile(self, width, height, srs, coord): - + rgba = [numpy.zeros((width, height), float) for chan in range(4)] - + rgba = self.stack.render(self.layer.config, rgba, coord) - + return _rgba2img(rgba) class Composite(Provider): @@ -236,13 +236,13 @@ class Composite(Provider): def build_stack(obj): """ Build up a data structure of Stack and Layer objects from lists of dictionaries. - + Normally, this is applied to the "stack" parameter to Composite.Provider. """ if type(obj) is list: layers = map(build_stack, obj) return Stack(layers) - + elif type(obj) is dict: keys = (('src', 'layername'), ('color', 'colorname'), ('mask', 'maskname'), ('opacity', 'opacity'), @@ -250,7 +250,7 @@ def build_stack(obj): ('zoom', 'zoom')) args = [(arg, obj[key]) for (key, arg) in keys if key in obj] - + return Layer(**dict(args)) else: @@ -258,7 +258,7 @@ def build_stack(obj): class Layer: """ A single image layer in a stack. - + Can include a reference to another layer for the source image, a second reference to another layer for the mask, and a color name for the fill. """ @@ -267,13 +267,13 @@ class Layer: """ A new image layer. Arguments: - + layername: Name of the primary source image layer. - + colorname: Fill color, passed to make_color(). - + maskname: Name of the mask image layer. """ @@ -283,17 +283,17 @@ class Layer: self.opacity = opacity self.blendmode = blendmode self.adjustments = adjustments - + zooms = re.search("^(\d+)-(\d+)$|^(\d+)$", zoom) if zoom else None - + if zooms: min_zoom, max_zoom, at_zoom = zooms.groups() - + if min_zoom is not None and max_zoom is not None: self.min_zoom, self.max_zoom = int(min_zoom), int(max_zoom) elif at_zoom is not None: self.min_zoom, self.max_zoom = int(at_zoom), int(at_zoom) - + else: self.min_zoom, self.max_zoom = 0, float('inf') @@ -301,7 +301,7 @@ class Layer: """ Return true if the requested zoom level is valid for this layer. """ return self.min_zoom <= zoom and zoom <= self.max_zoom - + def render(self, config, input_rgba, coord): """ Render this image layer. @@ -309,9 +309,9 @@ class Layer: return an output image with the contents of this image layer. """ has_layer, has_color, has_mask = False, False, False - + output_rgba = [chan.copy() for chan in input_rgba] - + if self.layername: layer = config.layers[self.layername] mime, body = TileStache.getTile(layer, coord, 'png') @@ -319,7 +319,7 @@ class Layer: layer_rgba = _img2rgba(layer_img) has_layer = True - + if self.maskname: layer = config.layers[self.maskname] mime, body = TileStache.getTile(layer, coord, 'png') @@ -336,10 +336,10 @@ class Layer: if has_layer: layer_rgba = apply_adjustments(layer_rgba, self.adjustments) - + if has_layer and has_color and has_mask: raise KnownUnknown("You can't specify src, color and mask together in a Composite Layer: %s, %s, %s" % (repr(self.layername), repr(self.colorname), repr(self.maskname))) - + elif has_layer and has_color: # color first, then layer output_rgba = blend_images(output_rgba, color_rgba[:3], color_rgba[3], self.opacity, self.blendmode) @@ -352,10 +352,10 @@ class Layer: elif has_color and has_mask: output_rgba = blend_images(output_rgba, color_rgba[:3], mask_chan, self.opacity, self.blendmode) - + elif has_layer: output_rgba = blend_images(output_rgba, layer_rgba[:3], layer_rgba[3], self.opacity, self.blendmode) - + elif has_color: output_rgba = blend_images(output_rgba, color_rgba[:3], color_rgba[3], self.opacity, self.blendmode) @@ -375,9 +375,9 @@ class Stack: """ def __init__(self, layers): """ A new image stack. - + Argument: - + layers: List of Layer instances. """ @@ -387,7 +387,7 @@ class Stack: """ """ return True - + def render(self, config, input_rgba, coord): """ Render this image stack. @@ -396,7 +396,7 @@ class Stack: this stack pasted on in turn. """ stack_rgba = [numpy.zeros(chan.shape, chan.dtype) for chan in input_rgba] - + for layer in self.layers: try: if layer.in_zoom(coord.zoom): @@ -412,9 +412,9 @@ class Stack: def make_color(color): """ Convert colors expressed as HTML-style RGB(A) strings to tuples. - + Returns four-element RGBA tuple, e.g. (0xFF, 0x99, 0x00, 0xFF). - + Examples: white: "#ffffff", "#fff", "#ffff", "#ffffffff" black: "#000000", "#000", "#000f", "#000000ff" @@ -436,7 +436,7 @@ def make_color(color): elif len(color) == 5: color = ''.join([color[i] for i in (0, 1, 1, 2, 2, 3, 3, 4, 4)]) - + try: r = int(color[1:3], 16) g = int(color[3:5], 16) @@ -451,13 +451,13 @@ def make_color(color): def _arr2img(ar): """ Convert Numeric array to PIL Image. """ - return Image.fromstring('L', (ar.shape[1], ar.shape[0]), ar.astype(numpy.ubyte).tostring()) + return Image.frombytes('L', (ar.shape[1], ar.shape[0]), ar.astype(numpy.ubyte).tostring()) def _img2arr(im): """ Convert PIL Image to Numeric array. """ assert im.mode == 'L' - return numpy.reshape(numpy.fromstring(im.tostring(), numpy.ubyte), (im.size[1], im.size[0])) + return numpy.reshape(numpy.fromstring(im.tobytes(), numpy.ubyte), (im.size[1], im.size[0])) def _rgba2img(rgba): """ Convert four Numeric array objects to PIL Image. @@ -473,15 +473,15 @@ def _img2rgba(im): def apply_adjustments(rgba, adjustments): """ Apply image adjustments one by one and return a modified image. - + Working adjustments: - + threshold: Calls apply_threshold_adjustment() - + curves: Calls apply_curves_adjustment() - + curves2: Calls apply_curves2_adjustment() """ @@ -493,16 +493,16 @@ def apply_adjustments(rgba, adjustments): if name == 'threshold': rgba = apply_threshold_adjustment(rgba, *args) - + elif name == 'curves': rgba = apply_curves_adjustment(rgba, *args) - + elif name == 'curves2': rgba = apply_curves2_adjustment(rgba, *args) - + else: raise KnownUnknown('Unrecognized composite adjustment: "%s" with args %s' % (name, repr(args))) - + return rgba def apply_threshold_adjustment(rgba, red_value, green_value=None, blue_value=None): @@ -514,30 +514,30 @@ def apply_threshold_adjustment(rgba, red_value, green_value=None, blue_value=Non # channels red, green, blue, alpha = rgba - + # knowns are given in 0-255 range, need to be converted to floats red_value, green_value, blue_value = red_value / 255.0, green_value / 255.0, blue_value / 255.0 - + red[red > red_value] = 1 red[red <= red_value] = 0 - + green[green > green_value] = 1 green[green <= green_value] = 0 - + blue[blue > blue_value] = 1 blue[blue <= blue_value] = 0 - + return red, green, blue, alpha def apply_curves_adjustment(rgba, black_grey_white): """ Adjustment inspired by Photoshop "Curves" feature. - + Arguments are three integers that are intended to be mapped to black, grey, and white outputs. Curves2 offers more flexibility, see apply_curves2_adjustment(). - + Darken a light image by pushing light grey to 50% grey, 0xCC to 0x80: - + [ "curves", [0, 204, 255] @@ -546,54 +546,54 @@ def apply_curves_adjustment(rgba, black_grey_white): # channels red, green, blue, alpha = rgba black, grey, white = black_grey_white - + # coefficients a, b, c = [sympy.Symbol(n) for n in 'abc'] - + # knowns are given in 0-255 range, need to be converted to floats black, grey, white = black / 255.0, grey / 255.0, white / 255.0 - + # black, gray, white eqs = [a * black**2 + b * black + c - 0.0, a * grey**2 + b * grey + c - 0.5, a * white**2 + b * white + c - 1.0] - + co = sympy.solve(eqs, a, b, c) - + # arrays for each coefficient a, b, c = [float(co[n]) * numpy.ones(red.shape, numpy.float32) for n in (a, b, c)] - + # arithmetic red = numpy.clip(a * red**2 + b * red + c, 0, 1) green = numpy.clip(a * green**2 + b * green + c, 0, 1) blue = numpy.clip(a * blue**2 + b * blue + c, 0, 1) - + return red, green, blue, alpha def apply_curves2_adjustment(rgba, map_red, map_green=None, map_blue=None): """ Adjustment inspired by Photoshop "Curves" feature. - + Arguments are given in the form of three value mappings, typically mapping black, grey and white input and output values. One argument indicates an effect applicable to all channels, three arguments apply effects to each channel separately. - + Simple monochrome inversion: - + [ "curves2", [[0, 255], [128, 128], [255, 0]] ] - + Darken a light image by pushing light grey down by 50%, 0x99 to 0x66: - + [ "curves2", [[0, 255], [153, 102], [255, 0]] ] - - Shaded hills, with Imhof-style purple-blue shadows and warm highlights: - + + Shaded hills, with Imhof-style purple-blue shadows and warm highlights: + [ "curves2", [[0, 22], [128, 128], [255, 255]], @@ -608,43 +608,43 @@ def apply_curves2_adjustment(rgba, map_red, map_green=None, map_blue=None): # channels red, green, blue, alpha = rgba out = [] - + for (chan, input) in ((red, map_red), (green, map_green), (blue, map_blue)): # coefficients a, b, c = [sympy.Symbol(n) for n in 'abc'] - + # parameters given in 0-255 range, need to be converted to floats (in_1, out_1), (in_2, out_2), (in_3, out_3) \ = [(in_ / 255.0, out_ / 255.0) for (in_, out_) in input] - + # quadratic function eqs = [a * in_1**2 + b * in_1 + c - out_1, a * in_2**2 + b * in_2 + c - out_2, a * in_3**2 + b * in_3 + c - out_3] - + co = sympy.solve(eqs, a, b, c) - + # arrays for each coefficient a, b, c = [float(co[n]) * numpy.ones(chan.shape, numpy.float32) for n in (a, b, c)] - + # arithmetic out.append(numpy.clip(a * chan**2 + b * chan + c, 0, 1)) - + return out + [alpha] def blend_images(bottom_rgba, top_rgb, mask_chan, opacity, blendmode): """ Blend images using a given mask, opacity, and blend mode. - + Working blend modes: None for plain pass-through, "screen", "multiply", "linear light", and "hard light". """ if opacity == 0 or not mask_chan.any(): # no-op for zero opacity or empty mask return [numpy.copy(chan) for chan in bottom_rgba] - + # prepare unitialized output arrays output_rgba = [numpy.empty_like(chan) for chan in bottom_rgba] - + if not blendmode: # plain old paste output_rgba[:3] = [numpy.copy(chan) for chan in top_rgb] @@ -659,17 +659,17 @@ def blend_images(bottom_rgba, top_rgb, mask_chan, opacity, blendmode): for c in (0, 1, 2): blend_function = blend_functions[blendmode] output_rgba[c] = blend_function(bottom_rgba[c], top_rgb[c]) - + else: raise KnownUnknown('Unrecognized blend mode: "%s"' % blendmode) - + # comined effective mask channel if opacity < 1: mask_chan = mask_chan * opacity # pixels from mask that aren't full-white gr = mask_chan < 1 - + if gr.any(): # we have some shades of gray to take care of for c in (0, 1, 2): @@ -677,18 +677,18 @@ def blend_images(bottom_rgba, top_rgb, mask_chan, opacity, blendmode): # Math borrowed from Wikipedia; C0 is the variable alpha_denom: # http://en.wikipedia.org/wiki/Alpha_compositing#Analytical_derivation_of_the_over_operator # - + alpha_denom = 1 - (1 - mask_chan) * (1 - bottom_rgba[3]) nz = alpha_denom > 0 # non-zero alpha denominator - + alpha_ratio = mask_chan[nz] / alpha_denom[nz] - + output_rgba[c][nz] = output_rgba[c][nz] * alpha_ratio \ + bottom_rgba[c][nz] * (1 - alpha_ratio) - + # let the zeros perish output_rgba[c][~nz] = 0 - + # output mask is the screen of the existing and overlaid alphas output_rgba[3] = blend_channels_screen(bottom_rgba[3], mask_chan) @@ -696,75 +696,75 @@ def blend_images(bottom_rgba, top_rgb, mask_chan, opacity, blendmode): def blend_channels_screen(bottom_chan, top_chan): """ Return combination of bottom and top channels. - + Math from http://illusions.hu/effectwiki/doku.php?id=screen_blending """ return 1 - (1 - bottom_chan[:,:]) * (1 - top_chan[:,:]) def blend_channels_multiply(bottom_chan, top_chan): """ Return combination of bottom and top channels. - + Math from http://illusions.hu/effectwiki/doku.php?id=multiply_blending """ return bottom_chan[:,:] * top_chan[:,:] def blend_channels_linear_light(bottom_chan, top_chan): """ Return combination of bottom and top channels. - + Math from http://illusions.hu/effectwiki/doku.php?id=linear_light_blending """ return numpy.clip(bottom_chan[:,:] + 2 * top_chan[:,:] - 1, 0, 1) def blend_channels_hard_light(bottom_chan, top_chan): """ Return combination of bottom and top channels. - + Math from http://illusions.hu/effectwiki/doku.php?id=hard_light_blending """ # different pixel subsets for dark and light parts of overlay dk, lt = top_chan < .5, top_chan >= .5 - + output_chan = numpy.empty(bottom_chan.shape, bottom_chan.dtype) output_chan[dk] = 2 * bottom_chan[dk] * top_chan[dk] output_chan[lt] = 1 - 2 * (1 - bottom_chan[lt]) * (1 - top_chan[lt]) - + return output_chan def makeColor(color): """ An old name for the make_color function, deprecated for the next version. """ return make_color(color) - + def makeLayer(element): """ Build a Layer object from an XML element, deprecated for the next version. """ kwargs = {} - + if element.hasAttribute('src'): kwargs['layername'] = element.getAttribute('src') if element.hasAttribute('color'): kwargs['colorname'] = element.getAttribute('color') - + for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.tagName == 'mask' and child.hasAttribute('src'): kwargs['maskname'] = child.getAttribute('src') print >> sys.stderr, 'Making a layer from', kwargs - + return Layer(**kwargs) def makeStack(element): """ Build a Stack object from an XML element, deprecated for the next version. """ layers = [] - + for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.tagName == 'stack': stack = makeStack(child) layers.append(stack) - + elif child.tagName == 'layer': layer = makeLayer(child) layers.append(layer) @@ -779,27 +779,27 @@ def makeStack(element): if __name__ == '__main__': import unittest - + import TileStache.Core import TileStache.Caches import TileStache.Geography import TileStache.Config import ModestMaps.Core - + class SizelessImage: """ Wrap an image without wrapping the size() method, for Layer.render(). """ def __init__(self, img): self.img = img - + def save(self, out, format): self.img.save(out, format) - + class TinyBitmap: """ A minimal provider that only returns 3x3 bitmaps from strings. """ def __init__(self, string): - self.img = Image.fromstring('RGBA', (3, 3), string) + self.img = Image.frombytes('RGBA', (3, 3), string) def renderTile(self, *args, **kwargs): return SizelessImage(self.img) @@ -823,7 +823,7 @@ if __name__ == '__main__': layer.provider = Provider(layer, stack=stack) return layer - + class ColorTests(unittest.TestCase): """ """ @@ -847,7 +847,7 @@ if __name__ == '__main__': assert make_color('#f908') == (0xFF, 0x99, 0x00, 0x88), 'transparent orange' assert make_color('#ff990088') == (0xFF, 0x99, 0x00, 0x88), 'transparent orange again' - + def testErrors(self): # it has to be a string @@ -855,32 +855,32 @@ if __name__ == '__main__': self.assertRaises(KnownUnknown, make_color, None) self.assertRaises(KnownUnknown, make_color, 1337) self.assertRaises(KnownUnknown, make_color, [93]) - + # it has to start with a hash self.assertRaises(KnownUnknown, make_color, 'hello') - + # it has to have 3, 4, 6 or 7 hex chars self.assertRaises(KnownUnknown, make_color, '#00') self.assertRaises(KnownUnknown, make_color, '#00000') self.assertRaises(KnownUnknown, make_color, '#0000000') self.assertRaises(KnownUnknown, make_color, '#000000000') - + # they have to actually hex chars self.assertRaises(KnownUnknown, make_color, '#foo') self.assertRaises(KnownUnknown, make_color, '#bear') self.assertRaises(KnownUnknown, make_color, '#monkey') self.assertRaises(KnownUnknown, make_color, '#dedboeuf') - + class CompositeTests(unittest.TestCase): """ """ def setUp(self): - + cache = TileStache.Caches.Test() self.config = TileStache.Config.Configuration(cache, '.') - + # Sort of a sw/ne diagonal street, with a top-left corner halo: - # + # # +------+ +------+ +------+ +------+ +------+ # |\\\\\\| |++++--| | ////| | ''| |\\//''| # |\\\\\\| + |++++--| + |//////| + | '' | > |//''\\| @@ -891,7 +891,7 @@ if __name__ == '__main__': # Just trust the tests. # _fff, _ccc, _999, _000, _nil = '\xFF\xFF\xFF\xFF', '\xCC\xCC\xCC\xFF', '\x99\x99\x99\xFF', '\x00\x00\x00\xFF', '\x00\x00\x00\x00' - + self.config.layers = \ { 'base': tinybitmap_layer(self.config, _ccc * 9), @@ -899,11 +899,11 @@ if __name__ == '__main__': 'outlines': tinybitmap_layer(self.config, _nil + (_999 * 7) + _nil), 'streets': tinybitmap_layer(self.config, _nil + _nil + _fff + _nil + _fff + _nil + _fff + _nil + _nil) } - + self.start_img = Image.new('RGBA', (3, 3), (0x00, 0x00, 0x00, 0x00)) - + def test0(self): - + stack = \ [ {"src": "base"}, @@ -912,10 +912,10 @@ if __name__ == '__main__': {"src": "streets"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0xCC, 0xCC, 0xCC, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0xFF, 0xFF, 0xFF, 0xFF), 'top right pixel' @@ -925,9 +925,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0x99, 0x99, 0x99, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom right pixel' - + def test1(self): - + stack = \ [ {"src": "base"}, @@ -936,10 +936,10 @@ if __name__ == '__main__': {"src": "streets"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0xCC, 0xCC, 0xCC, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0xFF, 0xFF, 0xFF, 0xFF), 'top right pixel' @@ -949,9 +949,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom right pixel' - + def test2(self): - + stack = \ [ {"color": "#ccc"}, @@ -960,10 +960,10 @@ if __name__ == '__main__': {"src": "streets"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0xCC, 0xCC, 0xCC, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0xFF, 0xFF, 0xFF, 0xFF), 'top right pixel' @@ -973,9 +973,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom right pixel' - + def test3(self): - + stack = \ [ {"color": "#ccc"}, @@ -984,10 +984,10 @@ if __name__ == '__main__': {"src": "streets"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0xFF, 0xFF, 0xFF, 0xFF), 'top right pixel' @@ -997,9 +997,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xCC, 0xCC, 0xCC, 0xFF), 'bottom right pixel' - + def test4(self): - + stack = \ [ [ @@ -1007,10 +1007,10 @@ if __name__ == '__main__': {"src": "streets"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x99, 0x99, 0x99, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0xFF, 0xFF, 0xFF, 0xFF), 'top right pixel' @@ -1020,24 +1020,24 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0x00, 0x00, 0x00, 0x00), 'bottom center pixel' assert img.getpixel((2, 2)) == (0x00, 0x00, 0x00, 0x00), 'bottom right pixel' - + def test5(self): stack = {"src": "streets", "color": "#999", "mask": "halos"} layer = minimal_stack_layer(self.config, stack) - + # it's an error to specify scr, color, and mask all together self.assertRaises(KnownUnknown, layer.provider.renderTile, 3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) stack = {"mask": "halos"} layer = minimal_stack_layer(self.config, stack) - + # it's also an error to specify just a mask self.assertRaises(KnownUnknown, layer.provider.renderTile, 3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) stack = {} layer = minimal_stack_layer(self.config, stack) - + # an empty stack is not so great self.assertRaises(KnownUnknown, layer.provider.renderTile, 3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) @@ -1045,36 +1045,36 @@ if __name__ == '__main__': """ """ def setUp(self): - + cache = TileStache.Caches.Test() self.config = TileStache.Config.Configuration(cache, '.') - + _808f = '\x80\x80\x80\xFF' _fff0, _fff8, _ffff = '\xFF\xFF\xFF\x00', '\xFF\xFF\xFF\x80', '\xFF\xFF\xFF\xFF' _0000, _0008, _000f = '\x00\x00\x00\x00', '\x00\x00\x00\x80', '\x00\x00\x00\xFF' - + self.config.layers = \ { # 50% gray all over 'gray': tinybitmap_layer(self.config, _808f * 9), - + # nothing anywhere 'nothing': tinybitmap_layer(self.config, _0000 * 9), - + # opaque horizontal gradient, black to white 'h gradient': tinybitmap_layer(self.config, (_000f + _808f + _ffff) * 3), - + # transparent white at top to opaque white at bottom 'white wipe': tinybitmap_layer(self.config, _fff0 * 3 + _fff8 * 3 + _ffff * 3), - + # transparent black at top to opaque black at bottom 'black wipe': tinybitmap_layer(self.config, _0000 * 3 + _0008 * 3 + _000f * 3) } - + self.start_img = Image.new('RGBA', (3, 3), (0x00, 0x00, 0x00, 0x00)) - + def test0(self): - + stack = \ [ [ @@ -1082,10 +1082,10 @@ if __name__ == '__main__': {"src": "white wipe"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top right pixel' @@ -1095,9 +1095,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom right pixel' - + def test1(self): - + stack = \ [ [ @@ -1105,10 +1105,10 @@ if __name__ == '__main__': {"src": "black wipe"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top right pixel' @@ -1118,9 +1118,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0x00, 0x00, 0x00, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0x00, 0x00, 0x00, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0x00, 0x00, 0x00, 0xFF), 'bottom right pixel' - + def test2(self): - + stack = \ [ [ @@ -1128,10 +1128,10 @@ if __name__ == '__main__': {"src": "white wipe", "mask": "h gradient"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top right pixel' @@ -1141,9 +1141,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0x80, 0x80, 0x80, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xC0, 0xC0, 0xC0, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom right pixel' - + def test3(self): - + stack = \ [ [ @@ -1151,10 +1151,10 @@ if __name__ == '__main__': {"src": "black wipe", "mask": "h gradient"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top left pixel' assert img.getpixel((1, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top center pixel' assert img.getpixel((2, 0)) == (0x80, 0x80, 0x80, 0xFF), 'top right pixel' @@ -1164,9 +1164,9 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0x80, 0x80, 0x80, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0x40, 0x40, 0x40, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0x00, 0x00, 0x00, 0xFF), 'bottom right pixel' - + def test4(self): - + stack = \ [ [ @@ -1174,10 +1174,10 @@ if __name__ == '__main__': {"src": "white wipe"} ] ] - + layer = minimal_stack_layer(self.config, stack) img = layer.provider.renderTile(3, 3, None, ModestMaps.Core.Coordinate(0, 0, 0)) - + assert img.getpixel((0, 0)) == (0x00, 0x00, 0x00, 0x00), 'top left pixel' assert img.getpixel((1, 0)) == (0x00, 0x00, 0x00, 0x00), 'top center pixel' assert img.getpixel((2, 0)) == (0x00, 0x00, 0x00, 0x00), 'top right pixel' @@ -1187,5 +1187,5 @@ if __name__ == '__main__': assert img.getpixel((0, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom left pixel' assert img.getpixel((1, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom center pixel' assert img.getpixel((2, 2)) == (0xFF, 0xFF, 0xFF, 0xFF), 'bottom right pixel' - + unittest.main() diff --git a/TileStache/Goodies/Providers/GDAL.py b/TileStache/Goodies/Providers/GDAL.py index b09afcd..f5d89ec 100644 --- a/TileStache/Goodies/Providers/GDAL.py +++ b/TileStache/Goodies/Providers/GDAL.py @@ -42,39 +42,39 @@ class Provider: """ """ self.layer = layer - + fileurl = urljoin(layer.config.dirpath, filename) scheme, h, file_path, p, q, f = urlparse(fileurl) - + if scheme not in ('', 'file'): raise Exception('GDAL file must be on the local filesystem, not: '+fileurl) - + if resample not in resamplings: raise Exception('Resample must be "cubic", "linear", or "nearest", not: '+resample) - + self.filename = file_path self.resample = resamplings[resample] self.maskband = maskband - + def renderArea(self, width, height, srs, xmin, ymin, xmax, ymax, zoom): """ """ src_ds = gdal.Open(str(self.filename)) driver = gdal.GetDriverByName('GTiff') - + if src_ds.GetGCPs(): src_ds.SetProjection(src_ds.GetGCPProjection()) - + grayscale_src = (src_ds.RasterCount == 1) try: # Prepare output gdal datasource ----------------------------------- - + area_ds = driver.Create('/vsimem/output', width, height, 3) - + if area_ds is None: raise Exception('uh oh.') - + # If we are using a mask band, create a data set which possesses a 'NoData' value enabling us to create a # mask for validity. mask_ds = None @@ -83,31 +83,31 @@ class Provider: # efficient way to extract a single band from a dataset which doesn't risk attempting to copy the entire # dataset. mask_ds = driver.Create('/vsimem/alpha', width, height, src_ds.RasterCount, gdal.GDT_Float32) - + if mask_ds is None: raise Exception('Failed to create dataset mask.') [mask_ds.GetRasterBand(i).SetNoDataValue(float('nan')) for i in xrange(1, src_ds.RasterCount+1)] - + merc = osr.SpatialReference() merc.ImportFromProj4(srs) area_ds.SetProjection(merc.ExportToWkt()) if mask_ds is not None: mask_ds.SetProjection(merc.ExportToWkt()) - + # note that 900913 points north and east x, y = xmin, ymax w, h = xmax - xmin, ymin - ymax - + gtx = [x, w/width, 0, y, 0, h/height] area_ds.SetGeoTransform(gtx) if mask_ds is not None: mask_ds.SetGeoTransform(gtx) - + # Adjust resampling method ----------------------------------------- - + resample = self.resample - + if resample == gdal.GRA_CubicSpline: # # I've found through testing that when ReprojectImage is used @@ -118,35 +118,35 @@ class Provider: xscale = area_ds.GetGeoTransform()[1] / src_ds.GetGeoTransform()[1] yscale = area_ds.GetGeoTransform()[5] / src_ds.GetGeoTransform()[5] diff = max(abs(xscale - 1), abs(yscale - 1)) - + if diff < .001: resample = gdal.GRA_Cubic - + # Create rendered area --------------------------------------------- - + src_sref = osr.SpatialReference() src_sref.ImportFromWkt(src_ds.GetProjection()) - + gdal.ReprojectImage(src_ds, area_ds, src_ds.GetProjection(), area_ds.GetProjection(), resample) if mask_ds is not None: # Interpolating validity makes no sense and so we can use nearest neighbour resampling here no matter # what is requested. gdal.ReprojectImage(src_ds, mask_ds, src_ds.GetProjection(), mask_ds.GetProjection(), gdal.GRA_NearestNeighbour) - + channel = grayscale_src and (1, 1, 1) or (1, 2, 3) r, g, b = [area_ds.GetRasterBand(i).ReadRaster(0, 0, width, height) for i in channel] if mask_ds is None: data = ''.join([''.join(pixel) for pixel in zip(r, g, b)]) - area = Image.fromstring('RGB', (width, height), data) + area = Image.frombytes('RGB', (width, height), data) else: a = mask_ds.GetRasterBand(self.maskband).GetMaskBand().ReadRaster(0, 0, width, height) data = ''.join([''.join(pixel) for pixel in zip(r, g, b, a)]) - area = Image.fromstring('RGBA', (width, height), data) + area = Image.frombytes('RGBA', (width, height), data) finally: driver.Delete('/vsimem/output') if self.maskband > 0: driver.Delete('/vsimem/alpha') - + return area diff --git a/TileStache/Mapnik.py b/TileStache/Mapnik.py index 76ad779..2a6530c 100644 --- a/TileStache/Mapnik.py +++ b/TileStache/Mapnik.py @@ -40,7 +40,7 @@ except ImportError: if 'mapnik' in locals(): _version = hasattr(mapnik, 'mapnik_version') and mapnik.mapnik_version() or 701 - + if _version >= 20000: Box2d = mapnik.Box2d else: @@ -50,14 +50,14 @@ global_mapnik_lock = allocate_lock() class ImageProvider: """ Built-in Mapnik provider. Renders map images from Mapnik XML files. - + This provider is identified by the name "mapnik" in the TileStache config. - + Arguments: - + - mapfile (required) Local file path to Mapnik XML file. - + - fonts (optional) Local directory path to *.ttf font files. @@ -65,41 +65,41 @@ class ImageProvider: Scale multiplier used for Mapnik rendering pipeline. Used for supporting retina resolution. - For more information about the scale factor, see: + For more information about the scale factor, see: https://github.com/mapnik/mapnik/wiki/Scale-factor - + More information on Mapnik and Mapnik XML: - http://mapnik.org - http://trac.mapnik.org/wiki/XMLGettingStarted - http://trac.mapnik.org/wiki/XMLConfigReference """ - + def __init__(self, layer, mapfile, fonts=None, scale_factor=None): """ Initialize Mapnik provider with layer and mapfile. - + XML mapfile keyword arg comes from TileStache config, and is an absolute path by the time it gets here. """ maphref = urljoin(layer.config.dirpath, mapfile) scheme, h, path, q, p, f = urlparse(maphref) - + if scheme in ('file', ''): self.mapfile = path else: self.mapfile = maphref - + self.layer = layer self.mapnik = None - + engine = mapnik.FontEngine.instance() - + if fonts: fontshref = urljoin(layer.config.dirpath, fonts) scheme, h, path, q, p, f = urlparse(fontshref) - + if scheme not in ('file', ''): raise Exception('Fonts from "%s" can\'t be used by Mapnik' % fontshref) - + for font in glob(path.rstrip('/') + '/*.ttf'): engine.register_font(str(font)) @@ -116,14 +116,14 @@ class ImageProvider: if 'scale factor' in config_dict: kwargs['scale_factor'] = int(config_dict['scale factor']) - + return kwargs - + def renderArea(self, width, height, srs, xmin, ymin, xmax, ymax, zoom): """ """ start_time = time() - + # # Mapnik can behave strangely when run in threads, so place a lock on the instance. # @@ -136,14 +136,14 @@ class ImageProvider: self.mapnik.width = width self.mapnik.height = height self.mapnik.zoom_to_box(Box2d(xmin, ymin, xmax, ymax)) - + img = mapnik.Image(width, height) # Don't even call render with scale factor if it's not # defined. Plays safe with older versions. if self.scale_factor is None: - mapnik.render(self.mapnik, img) + mapnik.render(self.mapnik, img) else: - mapnik.render(self.mapnik, img, self.scale_factor) + mapnik.render(self.mapnik, img, self.scale_factor) except: self.mapnik = None raise @@ -151,28 +151,28 @@ class ImageProvider: # always release the lock global_mapnik_lock.release() - img = Image.fromstring('RGBA', (width, height), img.tostring()) - + img = Image.frombytes('RGBA', (width, height), img.tobytes()) + logging.debug('TileStache.Mapnik.ImageProvider.renderArea() %dx%d in %.3f from %s', width, height, time() - start_time, self.mapfile) - + return img class GridProvider: """ Built-in UTF Grid provider. Renders JSON raster objects from Mapnik. - + This provider is identified by the name "mapnik grid" in the Tilestache config, and uses Mapnik 2.0 (and above) to generate JSON UTF grid responses. - + Sample configuration for a single grid layer: "provider": { "name": "mapnik grid", - "mapfile": "world_merc.xml", + "mapfile": "world_merc.xml", "fields": ["NAME", "POP2005"] } - + Sample configuration for multiple overlaid grid layers: "provider": @@ -187,41 +187,41 @@ class GridProvider: [0, []] ] } - + Arguments: - + - mapfile (required) Local file path to Mapnik XML file. - + - fields (optional) Array of field names to return in the response, defaults to all. An empty list will return no field names, while a value of null is equivalent to all. - + - layer_index (optional) Which layer from the mapfile to render, defaults to 0 (first layer). - + - layers (optional) Ordered list of (layer_index, fields) to combine; if provided layers overrides both layer_index and fields arguments. - An empty fields list will return no field names, while a value of null + An empty fields list will return no field names, while a value of null is equivalent to all fields. - + - scale (optional) Scale factor of output raster, defaults to 4 (64x64). - + - layer_id_key (optional) If set, each item in the 'data' property will have its source mapnik layer name added, keyed by this value. Useful for distingushing between data items. - + Information and examples for UTF Grid: - https://github.com/mapbox/utfgrid-spec/blob/master/1.2/utfgrid.md - http://mapbox.github.com/wax/interaction-leaf.html """ def __init__(self, layer, mapfile, fields=None, layers=None, layer_index=0, scale=4, layer_id_key=None): """ Initialize Mapnik grid provider with layer and mapfile. - + XML mapfile keyword arg comes from TileStache config, and is an absolute path by the time it gets here. """ @@ -238,7 +238,7 @@ class GridProvider: self.scale = scale self.layer_id_key = layer_id_key - + if layers: self.layers = layers else: @@ -253,14 +253,14 @@ class GridProvider: for key in ('fields', 'layers', 'layer_index', 'scale', 'layer_id_key'): if key in config_dict: kwargs[key] = config_dict[key] - + return kwargs - + def renderArea(self, width, height, srs, xmin, ymin, xmax, ymax, zoom): """ """ start_time = time() - + # # Mapnik can behave strangely when run in threads, so place a lock on the instance. # @@ -273,33 +273,33 @@ class GridProvider: self.mapnik.width = width self.mapnik.height = height self.mapnik.zoom_to_box(Box2d(xmin, ymin, xmax, ymax)) - + if self.layer_id_key is not None: grids = [] - + for (index, fields) in self.layers: datasource = self.mapnik.layers[index].datasource fields = (type(fields) is list) and map(str, fields) or datasource.fields() - + grid = mapnik.render_grid(self.mapnik, index, resolution=self.scale, fields=fields) - + for key in grid['data']: grid['data'][key][self.layer_id_key] = self.mapnik.layers[index].name - + grids.append(grid) - + # global_mapnik_lock.release() outgrid = reduce(merge_grids, grids) - + else: grid = mapnik.Grid(width, height) - + for (index, fields) in self.layers: datasource = self.mapnik.layers[index].datasource fields = (type(fields) is list) and map(str, fields) or datasource.fields() - + mapnik.render_layer(self.mapnik, grid, layer=index, fields=fields) - + # global_mapnik_lock.release() outgrid = grid.encode('utf', resolution=self.scale, features=True) except: @@ -337,7 +337,7 @@ class SaveableResponse: bytes = json.dumps(self.content, ensure_ascii=False).encode('utf-8') out.write(bytes) - + def crop(self, bbox): """ Return a cropped grid response. """ @@ -345,7 +345,7 @@ class SaveableResponse: keys, data = self.content['keys'], self.content.get('data', None) grid = [row[minchar:maxchar] for row in self.content['grid'][minrow:maxrow]] - + cropped = dict(keys=keys, data=data, grid=grid) return SaveableResponse(cropped, self.scale) @@ -357,42 +357,42 @@ def merge_grids(grid1, grid2): # keygen, outkeys, outdata = count(1), [], dict() - + for ingrid in [grid1, grid2]: for (index, key) in enumerate(ingrid['keys']): if key not in ingrid['data']: outkeys.append('') continue - + outkey = '%d' % keygen.next() outkeys.append(outkey) - + datum = ingrid['data'][key] outdata[outkey] = datum - + # # Merge the two grids, one on top of the other. # - + offset, outgrid = len(grid1['keys']), [] - + def newchar(char1, char2): """ Return a new encoded character based on two inputs. """ id1, id2 = decode_char(char1), decode_char(char2) - + if grid2['keys'][id2] == '': # transparent pixel, use the bottom character return encode_id(id1) - + else: # opaque pixel, use the top character return encode_id(id2 + offset) - + for (row1, row2) in zip(grid1['grid'], grid2['grid']): outrow = [newchar(c1, c2) for (c1, c2) in zip(row1, row2)] outgrid.append(''.join(outrow)) - + return dict(keys=outkeys, data=outdata, grid=outgrid) def encode_id(id): @@ -417,10 +417,10 @@ def get_mapnikMap(mapfile): """ Get a new mapnik.Map instance for a mapfile """ mmap = mapnik.Map(0, 0) - + if exists(mapfile): mapnik.load_map(mmap, str(mapfile)) - + else: handle, filename = mkstemp() os.write(handle, urlopen(mapfile).read()) @@ -428,5 +428,5 @@ def get_mapnikMap(mapfile): mapnik.load_map(mmap, filename) os.unlink(filename) - + return mmap diff --git a/TileStache/Pixels.py b/TileStache/Pixels.py index de7f208..b3c74fa 100644 --- a/TileStache/Pixels.py +++ b/TileStache/Pixels.py @@ -34,7 +34,7 @@ except ImportError: def load_palette(file_href): """ Load colors from a Photoshop .act file, return palette info. - + Return tuple is an array of [ (r, g, b), (r, g, b), ... ], bit depth of the palette, and a numeric transparency index or None if not defined. @@ -42,19 +42,19 @@ def load_palette(file_href): bytes = urlopen(file_href).read() count, t_index = unpack('!HH', bytes[768:768+4]) t_index = (t_index <= 0xff) and t_index or None - + palette = [] - + for offset in range(0, count): if offset == t_index: rgb = 0xff, 0x99, 0x00 else: rgb = unpack('!BBB', bytes[offset*3:(offset + 1)*3]) - + palette.append(rgb) - + bits = int(ceil(log(len(palette)) / log(2))) - + return palette, bits, t_index def palette_color(r, g, b, palette, t_index): @@ -65,48 +65,49 @@ def palette_color(r, g, b, palette, t_index): """ distances = [(r - _r)**2 + (g - _g)**2 + (b - _b)**2 for (_r, _g, _b) in palette] distances = map(sqrt, distances) - + if t_index is not None: distances = distances[:t_index] + distances[t_index+1:] - + return distances.index(min(distances)) def apply_palette(image, palette, t_index): """ Apply a palette array to an image, return a new image. """ image = image.convert('RGBA') - pixels = image.tostring() + pixels = image.tobytes() + t_value = (t_index in range(256)) and pack('!B', t_index) or None mapping = {} indexes = [] - + for offset in range(0, len(pixels), 4): r, g, b, a = unpack('!BBBB', pixels[offset:offset+4]) - + if a < 0x80 and t_value is not None: # Sufficiently transparent indexes.append(t_value) continue - + try: indexes.append(mapping[(r, g, b)]) except KeyError: # Never seen this color mapping[(r, g, b)] = pack('!B', palette_color(r, g, b, palette, t_index)) - + else: continue - + indexes.append(mapping[(r, g, b)]) - output = Image.fromstring('P', image.size, ''.join(indexes)) + output = Image.frombytes('P', image.size, ''.join(indexes)) bits = int(ceil(log(len(palette)) / log(2))) - + palette += [(0, 0, 0)] * (256 - len(palette)) palette = reduce(add, palette) output.putpalette(palette) - + return output def apply_palette256(image): diff --git a/TileStache/S3.py b/TileStache/S3.py index 6c98ef3..601a494 100644 --- a/TileStache/S3.py +++ b/TileStache/S3.py @@ -102,6 +102,9 @@ class Cache: def unlock(self, layer, coord, format): """ Release a cache lock for this tile. """ + if not self.use_locks: + return + key_name = tile_key(layer, coord, format, self.path) self.bucket.delete_key(key_name+'-lock') diff --git a/TileStache/VERSION b/TileStache/VERSION index daf515c..08369aa 100644 --- a/TileStache/VERSION +++ b/TileStache/VERSION @@ -1 +1 @@ -1.50.1 \ No newline at end of file +1.51 diff --git a/TileStache/Vector/__init__.py b/TileStache/Vector/__init__.py index 2ff987c..72bda34 100644 --- a/TileStache/Vector/__init__.py +++ b/TileStache/Vector/__init__.py @@ -312,7 +312,7 @@ def _tile_perimeter_geom(coord, projection, padded): return geom -def _feature_properties(feature, layer_definition, whitelist=None): +def _feature_properties(feature, layer_definition, whitelist=None, skip_empty_fields=False): """ Returns a dictionary of feature properties for a feature in a layer. Third argument is an optional list or dictionary of properties to @@ -343,10 +343,11 @@ def _feature_properties(feature, layer_definition, whitelist=None): raise KnownUnknown("Found an OGR field type I've never even seen: %d" % field_type) else: raise KnownUnknown("Found an OGR field type I don't know what to do with: ogr.%s" % name) - - property = type(whitelist) is dict and whitelist[name] or name - properties[property] = feature.GetField(name) - + + if not skip_empty_fields or feature.IsFieldSet(name): + property = type(whitelist) is dict and whitelist[name] or name + properties[property] = feature.GetField(name) + return properties def _append_with_delim(s, delim, data, key): @@ -419,7 +420,7 @@ def _open_layer(driver_name, parameters, dirpath): elif driver_name in ('ESRI Shapefile', 'GeoJSON', 'SQLite'): if 'file' not in parameters: - raise KnownUnknown('Need at least a "file" parameter for a shapefile') + raise KnownUnknown('Need a "file" parameter') file_href = urljoin(dirpath, parameters['file']) scheme, h, file_path, q, p, f = urlparse(file_href) @@ -460,7 +461,7 @@ def _open_layer(driver_name, parameters, dirpath): # return layer, datasource -def _get_features(coord, properties, projection, layer, clipped, projected, spacing, id_property): +def _get_features(coord, properties, projection, layer, clipped, projected, spacing, id_property, skip_empty_fields=False): """ Return a list of features in an OGR layer with properties in GeoJSON form. Optionally clip features to coordinate bounding box, and optionally @@ -524,7 +525,7 @@ def _get_features(coord, properties, projection, layer, clipped, projected, spac geometry.TransformTo(output_sref) geom = json_loads(geometry.ExportToJson()) - prop = _feature_properties(feature, definition, properties) + prop = _feature_properties(feature, definition, properties, skip_empty_fields) geojson_feature = {'type': 'Feature', 'properties': prop, 'geometry': geom} if id_property != None and id_property in prop: @@ -539,7 +540,7 @@ class Provider: See module documentation for explanation of constructor arguments. """ - def __init__(self, layer, driver, parameters, clipped, verbose, projected, spacing, properties, precision, id_property): + def __init__(self, layer, driver, parameters, clipped, verbose, projected, spacing, properties, precision, id_property, skip_empty_fields=False): self.layer = layer self.driver = driver self.clipped = clipped @@ -550,6 +551,7 @@ class Provider: self.properties = properties self.precision = precision self.id_property = id_property + self.skip_empty_fields = skip_empty_fields @staticmethod def prepareKeywordArgs(config_dict): @@ -564,6 +566,7 @@ class Provider: kwargs['projected'] = bool(config_dict.get('projected', False)) kwargs['verbose'] = bool(config_dict.get('verbose', False)) kwargs['precision'] = int(config_dict.get('precision', 6)) + kwargs['skip_empty_fields'] = bool(config_dict.get('skip_empty_fields', False)) if 'spacing' in config_dict: kwargs['spacing'] = float(config_dict.get('spacing', 0.0)) @@ -581,7 +584,7 @@ class Provider: """ Render a single tile, return a VectorResponse instance. """ layer, ds = _open_layer(self.driver, self.parameters, self.layer.config.dirpath) - features = _get_features(coord, self.properties, self.layer.projection, layer, self.clipped, self.projected, self.spacing, self.id_property) + features = _get_features(coord, self.properties, self.layer.projection, layer, self.clipped, self.projected, self.spacing, self.id_property, self.skip_empty_fields) response = {'type': 'FeatureCollection', 'features': features} if self.projected: diff --git a/scripts/tilestache-seed.py b/scripts/tilestache-seed.py index d62dfaf..f19814f 100644 --- a/scripts/tilestache-seed.py +++ b/scripts/tilestache-seed.py @@ -280,6 +280,16 @@ if __name__ == '__main__': south, west = min(lat1, lat2), min(lon1, lon2) north, east = max(lat1, lat2), max(lon1, lon2) + if not (-90.0 < south < 90.0) or not (-90.0 < north < 90.0): + raise KnownUnknown( + 'Latitude must be a value between -90 and 90 ' + '(Hint: Maybe you did long/lat instead of lat/long?).' + ) + if not (-180.0 < west < 180.0) or not (-180.0 < east < 180.0): + raise KnownUnknown( + 'Longitude must be a value between -180 and 180.' + ) + northwest = Location(north, west) southeast = Location(south, east) diff --git a/setup.py b/setup.py index f4f74fe..385984f 100644 --- a/setup.py +++ b/setup.py @@ -20,13 +20,7 @@ def is_installed(name): return False -requires = ['ModestMaps >=1.3.0','simplejson', 'Werkzeug'] - -# Soft dependency on PIL or Pillow -if is_installed('PIL'): - requires.append('PIL') -else: - requires.append('Pillow') +requires = ['ModestMaps >=1.3.0','simplejson', 'Werkzeug', 'Pillow'] setup(name='TileStache', -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/tilestache.git _______________________________________________ Pkg-grass-devel mailing list Pkg-grass-devel@lists.alioth.debian.org http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel