Esempio n. 1
0
class Mapchete(object):
    """
    Class handling MapcheteProcesses and MapcheteConfigs. Main access point to
    get, retrieve MapcheteTiles or seed entire pyramids.
    """

    def __repr__(self):
        return "<objec 'Mapchete'>"

    def __str__(self):
        return "Mapchete: %s" % self.config.mapchete_file

    def __init__(self, config):
        """
        Initialize with a .mapchete file and optional zoom & bound parameters.
        """
        try:
            self.config = config
            self.output = self.config.output
            base_tile_pyramid = TilePyramid(self.output.type)
            self.tile_pyramid = MetaTilePyramid(base_tile_pyramid, self.config.metatiling)
        except:
            raise
        try:
            py_compile.compile(self.config.process_file, doraise=True)
        except:
            raise
        self.process_name = os.path.splitext(os.path.basename(self.config.process_file))[0]
        if self.output.is_db:
            self._init_db_tables()

        if self.output.format == "GeoPackage":
            self._init_gpkg()

    def tile(self, tile):
        """
        Takes a Tile object and adds process specific metadata.
        """
        return MapcheteTile(self, tile)

    def get_work_tiles(self, zoom=None):
        """
        Determines the tiles affected by zoom levels, bounding box and input
        data.
        Returns (yields) valid Tile objects.
        """
        try:
            if zoom or zoom == 0:
                bbox = self.config.process_area(zoom)
                for tile in self.tile_pyramid.tiles_from_geom(bbox, zoom):
                    yield self.tile(tile)
            else:
                for zoom in reversed(self.config.zoom_levels):
                    bbox = self.config.process_area(zoom)
                    for tile in self.tile_pyramid.tiles_from_geom(bbox, zoom):
                        yield self.tile(tile)
        except Exception as e:
            LOGGER.error("error getting work tiles: %s" % traceback.print_exc())
            raise StopIteration()

    def execute(self, tile, overwrite=True):
        """
        Processes and saves tile.
        Returns a tuple of three values:
        - Tile ID: zoom, row, col
        - status: "exists", "empty", "failed" or "processed"
        - error message: optional or None
        """
        # Do nothing if tile exists or overwrite is turned off.
        if not overwrite and tile.exists():
            return tile.id, "exists", None
        try:
            new_process = imp.load_source(self.process_name + "Process", self.config.process_file)
            tile_process = new_process.Process(config=self.config, tile=tile, params=self.config.at_zoom(tile.zoom))
        except:
            return tile.id, "failed", traceback.print_exc()

        # Generate tile using the user defined process.
        if not self.config.baselevel or (tile.zoom == self.config.baselevel["zoom"]):
            try:
                result = tile_process.execute()
            except:
                return tile.id, "failed", traceback.print_exc()
            finally:
                tile_process = None

            message = None
            if result:
                if result == "empty":
                    status = "empty"
                else:
                    status = "custom"
                    message = result
            else:
                status = "processed"
            return tile.id, status, message

        # If baselevel interpolation is activated, generate from neighbor zooms.
        else:
            if tile.zoom < self.config.baselevel["zoom"]:
                # determine tiles from next zoom level
                # TODO create option for on demand processing if subtile is not
                # available.

                # create temporary VRT and create new tile from resampled
                # subtiles.
                children_paths = [child.path for child in tile.get_children() if child.exists()]
                if len(children_paths) == 0:
                    return tile.id, "empty", None
                temp_vrt = NamedTemporaryFile()
                build_vrt = "gdalbuildvrt %s %s > /dev/null" % (temp_vrt.name, " ".join(children_paths))
                try:
                    os.system(build_vrt)
                    assert os.path.isfile(temp_vrt.name)
                except:
                    build_vrt = "gdalbuildvrt %s %s" % (temp_vrt.name, " ".join(children_paths))
                    os.system(build_vrt)
                    return tile.id, "failed", "GDAL VRT building"
                try:
                    assert os.path.isfile(temp_vrt.name)
                    bands = tuple(
                        read_raster_window(temp_vrt.name, tile, resampling=self.config.baselevel["resampling"])
                    )
                except:
                    return tile.id, "failed", traceback.print_exc()
                try:
                    write_raster(tile_process, self.output.profile, bands)
                    return tile.id, "processed", None
                except:
                    return tile.id, "failed", traceback.print_exc()

            elif tile.zoom > self.config.baselevel["zoom"]:
                # determine tiles from previous zoom level
                # check if tiles exist and if not, execute subtiles
                parent = tile.get_parent()
                if not overwrite and parent.exists():
                    # LOGGER.info((tile.id, "exists", None))
                    pass
                else:
                    pass
                if not parent.exists():
                    # TODO create option for on demand processing
                    return tile.id, "empty", "source tile does not exist"
                try:
                    bands = tuple(read_raster_window(parent.path, tile, resampling=self.config.baselevel["resampling"]))
                    write_raster(tile_process, self.output.profile, bands)
                    return tile.id, "processed", None
                except:
                    return tile.id, "failed", traceback.print_exc()

    def get(self, tile, overwrite=False):
        """
        Returns tile as file object (image, for mapchete_serve). Also processes
        tile if necessary.
        """
        # return empty image if nothing to do at zoom level
        if tile.zoom not in self.config.zoom_levels:
            return self._empty_image()

        # return/process tile or crop/process metatile
        if self.config.metatiling > 1:
            metatile = MapcheteTile(
                self,
                self.tile_pyramid.tile(
                    tile.zoom, int(tile.row / self.config.metatiling), int(tile.col / self.config.metatiling)
                ),
            )

            # if overwrite is on or metatile doesn't exist, generate
            if overwrite or not metatile.exists():
                try:
                    messages = self.execute(metatile)
                except:
                    LOGGER.error(messages)
                    raise
                LOGGER.info(messages)
                # return empty image if process messaged empty
                if messages[1] == "empty":
                    return self._empty_image()
                if messages[1] == "failed":
                    LOGGER.error(messages)
                    raise IOError(messages)
                else:
                    return send_file(self._cropped_metatile(metatile, tile), mimetype="image/png")
            else:
                # return cropped image
                try:
                    LOGGER.info((metatile.id, tile.id, "return cropped metatile"))
                    return send_file(self._cropped_metatile(metatile, tile), mimetype="image/png")
                except Exception as e:
                    LOGGER.error(tile.id, "failed", e)
                    raise

        # return/process tile with no metatiling
        else:
            tile = MapcheteTile(self, tile)
            # if overwrite is on or metatile doesn't exist, generate
            if overwrite or not tile.exists():
                try:
                    messages = self.execute(tile)
                except:
                    raise
                # return empty image if process messaged empty
                if messages[1] == "empty":
                    return self._empty_image()
            return send_file(tile.path, mimetype="image/png")

    def read(self, tile, indexes=None, pixelbuffer=0, resampling="nearest"):
        """
        Reads source tile to numpy array.
        """
        if indexes:
            if isinstance(indexes, list):
                band_indexes = indexes
            else:
                band_indexes = [indexes]
        else:
            band_indexes = range(1, self.output.profile["count"] + 1)

        return tile.read(indexes=band_indexes, pixelbuffer=pixelbuffer, resampling=resampling)

    def _empty_image(self):
        """
        Creates transparent PNG
        """
        size = self.tile_pyramid.tilepyramid.tile_size
        empty_image = Image.new("RGBA", (size, size))
        return empty_image.tobytes()

    def _cropped_metatile(self, metatile, tile):
        """
        Crops metatile to tile.
        """
        metatiling = self.tile_pyramid.metatiles
        # calculate pixel boundary
        left = (tile.col % metatiling) * tile.width
        right = left + tile.width
        top = (tile.row % metatiling) * tile.height
        bottom = top + tile.height
        # open buffer image and crop metatile
        img = Image.open(metatile.path)
        cropped = img.crop((left, top, right, bottom))
        out_img = io.BytesIO()
        cropped.save(out_img, "PNG")
        out_img.seek(0)
        return out_img

    def _init_db_tables(self):
        """
        Initializes target tables in database.
        """
        db_url = "postgresql://%s:%s@%s:%s/%s" % (
            self.output.db_params["user"],
            self.output.db_params["password"],
            self.output.db_params["host"],
            self.output.db_params["port"],
            self.output.db_params["db"],
        )
        engine = create_engine(db_url)
        for zoom in self.config.zoom_levels:
            config = self.config.at_zoom(zoom)
            geom_type = config["output"].schema["geometry"]
            table = config["output"].db_params["table"]
            schema = config["output"].schema["properties"]

            column_types = {column: SQL_DATATYPES[schema[column]] for column in schema}

            metadata = MetaData(bind=engine)
            Table(
                table,
                metadata,
                Column("id", Integer, primary_key=True),
                Column("zoom", Integer, index=True),
                Column("row", Integer, index=True),
                Column("col", Integer, index=True),
                Column("geom", Geometry(geom_type, srid=self.tile_pyramid.srid)),
                *(Column(column, dtype) for column, dtype in column_types.iteritems())
            )
            metadata.create_all()
        engine.dispose()

    def _init_gpkg(self):
        """
        Creates .gpkg file and initializes tables.
        """
        pass
Esempio n. 2
0
def main(args):

    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--debug", action="store_true")
    parsed = parser.parse_args(args)
    global debug
    debug = parsed.debug
    scriptdir = os.path.dirname(os.path.realpath(__file__))

    testdata_directory = os.path.join(scriptdir, "testdata")
    outdata_directory = os.path.join(testdata_directory, "out")
    wgs84 = TilePyramid("geodetic")
    wgs84_meta = MetaTilePyramid(wgs84, 16)

    # TilePyramid
    #===========

    # tiles per zoomlevel
    try:
        matrix_width = wgs84.matrix_width(5)
        matrix_height = wgs84.matrix_height(5)
        assert (matrix_width, matrix_height) == (64, 32)
        print "tiles per zoomlevel OK"
    except:
        print "tiles per zoomlevel FAILED"
        raise


    # top left coordinate
    try:
        tile = wgs84.tile(5, 3, 3)
        tl = (tile.left,tile.top)
        assert tl == (-163.125, 73.125)
        print "top left coordinate OK"
    except:
        print "top left coordinate FAILED"
        print tl
        raise


    # tile bounding box
    try:
        tile = wgs84.tile(5, 3, 3)
        bbox = tile.bbox()
        testpolygon = Polygon([[-163.125, 73.125], [-157.5, 73.125],
            [-157.5, 67.5], [-163.125, 67.5], [-163.125, 73.125]])
        assert bbox.equals(testpolygon)
        print "tile bounding box OK"
    except:
        print "tile bounding box FAILED"
        raise


    # tile bounding box with buffer
    try:
        tile = wgs84.tile(5, 3, 3)
        bbox = tile.bbox(1)
        testpolygon = Polygon([[-163.14697265625, 73.14697265625],
            [-157.47802734375, 73.14697265625],
            [-157.47802734375, 67.47802734375],
            [-163.14697265625, 67.47802734375],
            [-163.14697265625, 73.14697265625]])
        assert bbox.equals(testpolygon)
        print "tile bounding box with buffer OK"
    except:
        print "tile bounding box with buffer FAILED"
        print bbox
        raise


    # tile bounds
    try:
        tile = wgs84.tile(5, 3, 3)
        bounds = tile.bounds()
        testbounds = (-163.125, 67.5, -157.5, 73.125)
        assert bounds == testbounds
        print "tile bounds OK"
    except:
        print "tile bounds FAILED"
        raise


    # tile bounds buffer
    try:
        tile = wgs84.tile(5, 3, 3)
        bounds = tile.bounds(1)
        testbounds = (-163.14697265625, 67.47802734375, -157.47802734375,
            73.14697265625)
        assert bounds == testbounds
        print "tile bounds with buffer OK"
    except:
        print "tile bounds wigh buffer FAILED"
        raise


    # test bounding box
    bbox_location = os.path.join(testdata_directory, "bbox.geojson")
    tiled_out = os.path.join(outdata_directory, "bbox_tiles.geojson")
    zoom = 5
    testtiles = [(5, 5, 33), (5, 6, 33), (5, 7, 33), (5, 8, 33), (5, 9, 33), (5, 10, 33),
        (5, 5, 34), (5, 6, 34), (5, 7, 34), (5, 8, 34), (5, 9, 34), (5, 10, 34), (5, 5, 35),
        (5, 6, 35), (5, 7, 35), (5, 8, 35), (5, 9, 35), (5, 10, 35),(5, 5, 36), (5, 6, 36),
        (5, 7, 36), (5, 8, 36), (5, 9, 36), (5, 10, 36), (5, 5, 37), (5, 6, 37), (5, 7, 37),
        (5, 8, 37), (5, 9, 37), (5, 10, 37),(5, 5, 38), (5, 6, 38), (5, 7, 38), (5, 8, 38),
        (5, 9, 38), (5, 10, 38), (5, 5, 39), (5, 6, 39), (5, 7, 39), (5, 8, 39), (5, 9, 39),
        (5, 10, 39), (5, 5, 40), (5, 6, 40), (5, 7, 40), (5, 8, 40), (5, 9, 40), (5, 10, 40),
        (5, 5, 41), (5, 6, 41), (5, 7, 41), (5, 8, 41), (5, 9, 41), (5, 10, 41)]
    with fiona.open(bbox_location) as bbox_file:
        try:
            bbox_tiles = [
                (tile.zoom, tile.row, tile.col)
                for tile in wgs84.tiles_from_bbox(bbox_file, zoom)
            ]
            assert len(set(bbox_tiles).symmetric_difference(set(testtiles))) == 0
            print "bounding box OK"
        except:
            print "bounding box FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in bbox_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)


    # tiles from Point
    point_location = os.path.join(testdata_directory, "point.geojson")
    tiled_out = os.path.join(outdata_directory, "point_tiles.geojson")
    zoom = 6
    testtile = [(6, 14, 69)]
    with fiona.open(point_location) as point_file:
        point = shape(point_file[0]["geometry"])
        try:
            point_tile = [
                (tile.zoom, tile.row, tile.col)
                for tile in wgs84.tiles_from_geom(point, zoom)
            ]
            assert point_tile == testtile
            print "Point OK"
        except:
            print point_tile, testtile
            print "Point FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            zoom, row, col = point_tile[0]
            feature = {}
            feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
            feature['properties'] = {}
            feature['properties']['col'] = col
            feature['properties']['row'] = row
            sink.write(feature)

    # tiles from MultiPoint
    multipoint_location = os.path.join(testdata_directory,
        "multipoint.geojson")
    tiled_out = os.path.join(outdata_directory, "multipoint_tiles.geojson")
    zoom = 9
    testtiles = [(9, 113, 553), (9, 118, 558)]
    with fiona.open(multipoint_location) as multipoint_file:
        multipoint = shape(multipoint_file[0]["geometry"])
        try:
            multipoint_tiles = [
                (tile.zoom, tile.row, tile.col)
                for tile in wgs84.tiles_from_geom(multipoint, zoom)
            ]
            assert multipoint_tiles == testtiles
            print "MultiPoint OK"
        except:
            print "MultiPoint FAILED"
            print multipoint_tiles
            print testtiles
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in multipoint_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

    # tiles from LineString
    linestring_location = os.path.join(testdata_directory,
        "linestring.geojson")
    tiled_out = os.path.join(outdata_directory, "linestring_tiles.geojson")
    zoom = 6
    testtiles = [(6, 14, 66), (6, 14, 67), (6, 14, 68), (6, 14, 69), (6, 14, 70), (6, 15, 70),
        (6, 15, 71), (6, 16, 71), (6, 16, 72), (6, 15, 73), (6, 16, 73), (6, 15, 74)]
    with fiona.open(linestring_location) as linestring_file:
        linestring = shape(linestring_file[0]["geometry"])
        try:
            linestring_tiles = [
                (tile.zoom, tile.row, tile.col)
                for tile in wgs84.tiles_from_geom(linestring, zoom)
                ]
            assert len(set(linestring_tiles).symmetric_difference(set(testtiles))) == 0
            print "LineString OK"
        except:
            print "LineString FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in linestring_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

    # tiles from MultiLineString
    multilinestring_location = os.path.join(testdata_directory,
        "multilinestring.geojson")
    tiled_out = os.path.join(outdata_directory,
        "multilinestring_tiles.geojson")
    zoom = 6
    testtiles = [(6, 14, 66), (6, 14, 67), (6, 14, 68), (6, 14, 69), (6, 14, 70), (6, 15, 70),
       (6, 15, 71), (6, 16, 71), (6, 16, 72), (6, 15, 73), (6, 16, 73), (6, 15, 74), (6, 21, 74),
       (6, 22, 74), (6, 24, 74), (6, 25, 74), (6, 28, 74), (6, 29, 74), (6, 20, 75), (6, 21, 75),
       (6, 22, 75), (6, 23, 75), (6, 24, 75), (6, 25, 75), (6, 26, 75), (6, 27, 75), (6, 28, 75),
       (6, 29, 75), (6, 30, 75), (6, 31, 75), (6, 25, 76)]
    with fiona.open(multilinestring_location) as multilinestring_file:
        multilinestring = shape(multilinestring_file[0]["geometry"])
        try:
            multilinestring_tiles = [
                (tile.zoom, tile.row, tile.col)
                for tile in wgs84.tiles_from_geom(multilinestring, zoom)
                ]
            assert len(set(multilinestring_tiles).symmetric_difference(set(testtiles))) == 0
            print "MultiLineString OK"
        except:
            print "MultiLineString FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in multilinestring_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

    # tiles from Polygon
    polygon_location = os.path.join(testdata_directory,
        "polygon.geojson")
    tiled_out = os.path.join(outdata_directory, "polygon_tiles.geojson")
    zoom = 8
    testtiles = [(8, 60, 269), (8, 61, 269), (8, 60, 270), (8, 61, 270), (8, 60, 271),
        (8, 61, 271), (8, 60, 272), (8, 61, 272), (8, 60, 273), (8, 61, 273), (8, 59, 274),
        (8, 60, 274), (8, 61, 274), (8, 58, 275), (8, 59, 275), (8, 60, 275), (8, 61, 275),
        (8, 58, 276), (8, 59, 276), (8, 60, 276), (8, 61, 276), (8, 62, 276), (8, 58, 277),
        (8, 59, 277), (8, 60, 277), (8, 61, 277), (8, 58, 278), (8, 59, 278), (8, 60, 278),
        (8, 61, 278), (8, 58, 279), (8, 59, 279), (8, 60, 279), (8, 61, 279), (8, 58, 280),
        (8, 59, 280), (8, 60, 280)]
    with fiona.open(polygon_location) as polygon_file:
        polygon = shape(polygon_file[0]["geometry"])
        polygon_tiles = [
            (tile.zoom, tile.row, tile.col)
            for tile in wgs84.tiles_from_geom(polygon, zoom)
            ]
        try:
            assert len(set(polygon_tiles).symmetric_difference(set(testtiles))) == 0
            print "Polygon OK"
        except:
            print "Polygon FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in polygon_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

    # tiles from MultiPolygon
    multipolygon_location = os.path.join(testdata_directory,
        "multipolygon.geojson")
    tiled_out = os.path.join(outdata_directory, "multipolygon_tiles.geojson")
    zoom = 10
    testtiles = [(10, 243, 1081), (10, 244, 1081), (10, 245, 1081), (10, 242, 1082),
        (10, 243, 1082), (10, 244, 1082), (10, 245, 1082), (10, 241, 1083), (10, 242, 1083),
        (10, 243, 1083), (10, 244, 1083), (10, 245, 1083), (10, 241, 1084), (10, 242, 1084),
        (10, 243, 1084), (10, 244, 1084), (10, 245, 1084), (10, 241, 1085), (10, 242, 1085),
        (10, 243, 1085), (10, 244, 1085), (10, 245, 1085), (10, 241, 1086), (10, 242, 1086),
        (10, 243, 1086), (10, 244, 1086), (10, 245, 1086), (10, 242, 1087), (10, 243, 1087),
        (10, 244, 1087), (10, 245, 1087), (10, 241, 1088), (10, 242, 1088), (10, 243, 1088),
        (10, 244, 1088), (10, 241, 1089), (10, 242, 1089), (10, 243, 1089), (10, 244, 1089),
        (10, 241, 1090), (10, 242, 1090), (10, 243, 1090), (10, 244, 1090), (10, 241, 1091),
        (10, 242, 1091), (10, 243, 1091), (10, 244, 1091), (10, 241, 1092), (10, 242, 1092),
        (10, 243, 1092), (10, 244, 1092), (10, 240, 1093), (10, 241, 1093), (10, 242, 1093),
        (10, 244, 1093), (10, 245, 1093), (10, 240, 1094), (10, 241, 1094), (10, 242, 1094),
        (10, 243, 1094), (10, 244, 1094), (10, 245, 1094), (10, 246, 1094), (10, 240, 1095),
        (10, 241, 1095), (10, 242, 1095), (10, 243, 1095), (10, 244, 1095), (10, 245, 1095),
        (10, 246, 1095), (10, 241, 1096), (10, 244, 1096), (10, 245, 1096), (10, 246, 1096),
        (10, 245, 1097), (10, 246, 1097)]
    with fiona.open(multipolygon_location) as multipolygon_file:
        multipolygon = shape(multipolygon_file[0]["geometry"])
        multipolygon_tiles = [
            (tile.zoom, tile.row, tile.col)
            for tile in wgs84.tiles_from_geom(multipolygon, zoom)
            ]
        try:
            assert len(set(multipolygon_tiles).symmetric_difference(set(testtiles))) == 0
            print "MultiPolygon OK"
        except:
            print "MultiPolygon FAILED"
            raise
    if debug:
        ## write debug output
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in multipolygon_tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)


    if debug:
        # writing output test files
        col, row = 2, 2
        zoom = 5
        metatiling = 2
        wgs84_meta = MetaTilePyramid(wgs84, metatiling)
        antimeridian_location = os.path.join(testdata_directory,
            "antimeridian.geojson")
        with fiona.open(antimeridian_location) as antimeridian_file:
            geometries = []
            for feature in antimeridian_file:
                geometries.append(shape(feature["geometry"]))
        antimeridian = cascaded_union(geometries)
        print "top left tile coordinates:"
        print "metaTilePyramid: %s" %([wgs84_meta.top_left_tile_coords(zoom, row, col)])
        print "tile bounding box"
        print "metaTilePyramid: %s" %([mapping(wgs84.tile_bbox(zoom, row, col))])
        print "tile bounds"
        print "metaTilePyramid: %s" %([wgs84_meta.tile_bounds(zoom, row, col)])
        print "tiles from bbox"
        #print "metaTilePyramid: %s" %([wgs84_meta.tiles_from_bbox(antimeridian, zoom)])
        print "tiles from geometry"

        ## write debug output
        tiled_out = os.path.join(outdata_directory, "tile_antimeridian_tiles.geojson")
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(tiled_out)
        except:
            pass
        tiles = wgs84.tiles_from_geom(antimeridian, zoom)
        print "TilePyramid: %s" %(len(tiles))
        with fiona.open(tiled_out, 'w', 'GeoJSON', schema) as sink:
            for tile in tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)
        ## write debug output
        metatiled_out = os.path.join(outdata_directory, "metatile_antimeridian_tiles.geojson")
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(metatiled_out)
        except:
            pass
        metatiles = wgs84_meta.tiles_from_geom(antimeridian, zoom)
        print "metaTilePyramid: %s" %(len(metatiles))
        with fiona.open(metatiled_out, 'w', 'GeoJSON', schema) as sink:
            for metatile in metatiles:
                zoom, row, col = metatile
                feature = {}
                feature['geometry'] = mapping(wgs84_meta.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)


    for metatiling in (1, 2, 4, 8, 16):
        wgs84_meta = MetaTilePyramid(wgs84, metatiling)
        for zoom in range(22):
            tilepyramid_width = wgs84.matrix_width(zoom)
            tilepyramid_height = wgs84.matrix_height(zoom)
            metatilepyramid_width = wgs84_meta.matrix_width(zoom)
            metatilepyramid_height = wgs84_meta.matrix_height(zoom)
            control_width = int(math.ceil(
                float(tilepyramid_width)/float(metatiling)
            ))
            control_height = int(math.ceil(
                float(tilepyramid_height)/float(metatiling)
            ))
            try:
                assert metatilepyramid_width == control_width
                assert metatilepyramid_height == control_height
            except:
                print "ERROR: metatile number"
                print "metatiling, zoom:", metatiling, zoom
                print "width:", metatilepyramid_width, control_width
                print "height:", metatilepyramid_height, control_height
                raise


    for metatiling in (1, 2, 4, 8, 16):
        wgs84_meta = MetaTilePyramid(wgs84, metatiling)
        for zoom in range(21):
            # check tuple
            assert isinstance(wgs84_meta.matrix_width(zoom), int)
            assert isinstance(wgs84_meta.matrix_height(zoom), int)

            # check metatile size
            metatile_x_size = round(wgs84_meta.metatile_x_size(zoom), ROUND)
            metatile_y_size = round(wgs84_meta.metatile_y_size(zoom), ROUND)
            # assert metatile size equals TilePyramid width and height at zoom 0
            if zoom == 0:
                try:
                    if metatiling == 1:
                        assert (metatile_x_size * 2) == wgs84.x_size
                    else:
                        assert metatile_x_size == wgs84.x_size
                    assert metatile_y_size == wgs84.y_size
                except:
                    print "ERROR: zoom 0 metatile size not correct"
                    print "metatiling, zoom:", metatiling, zoom
                    print "metatile_x_size:", wgs84_meta.metatiling, metatile_x_size, wgs84.x_size
                    print "metatile_y_size:", metatile_y_size, wgs84.y_size
                    raise
            ## assert metatile size within TilePyramid bounds
            try:
                assert (metatile_x_size > 0.0) and (
                    metatile_x_size <= wgs84.x_size)
                assert (metatile_y_size > 0.0) and (
                    metatile_y_size <= wgs84.y_size)
            except:
                print "ERROR: metatile size"
                print zoom
                print "metatile_x_size:", metatile_x_size, wgs84_meta.x_size
                print "metatile_y_size:", metatile_y_size, wgs84_meta.x_size
                raise
            ## calculate control size from tiles
            tile_x_size = wgs84.tile_x_size(zoom)
            tile_y_size = wgs84.tile_y_size(zoom)
            we_control_size = round(tile_x_size * float(metatiling), ROUND)
            if we_control_size > wgs84.x_size:
                we_control_size = wgs84.x_size
            ns_control_size = round(tile_y_size * float(metatiling), ROUND)

            if ns_control_size > wgs84.y_size:
                ns_control_size = wgs84.y_size
            try:
                assert metatile_x_size == we_control_size
                assert metatile_y_size == ns_control_size
            except:
                print "ERROR: metatile size and control sizes"
                print "zoom, metatiling:", zoom, metatiling
                print metatile_x_size, we_control_size
                print metatile_y_size, ns_control_size
                raise

            # check metatile pixelsize (resolution)
            pixel_x_size = round(wgs84.pixel_x_size(zoom), ROUND)
            ctr_pixel_x_size = round(wgs84_meta.pixel_x_size(zoom), ROUND)
            pixel_y_size = round(wgs84.pixel_y_size(zoom), ROUND)
            ctr_pixel_y_size = round(wgs84_meta.pixel_y_size(zoom), ROUND)
            try:
                assert pixel_x_size == ctr_pixel_x_size
                assert pixel_y_size == ctr_pixel_y_size
            except:
                print "ERROR: metatile pixel size"
                print "zoom, metatiling:", zoom, metatiling
                print "pixel_x_size:", pixel_x_size, ctr_pixel_x_size
                print "pixel_y_size:", pixel_y_size, ctr_pixel_y_size
                raise

    if debug:
        fiji_borders = os.path.join(testdata_directory, "fiji.geojson")
        with fiona.open(fiji_borders, "r") as fiji:
            geometries = []
            for feature in fiji:
                geometry = shape(feature['geometry'])
                geometries.append(geometry)
        union = cascaded_union(geometries)
        # tiles
        fiji_tiles = os.path.join(outdata_directory, "fiji_tiles.geojson")
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(fiji_tiles)
        except:
            pass
        metatiling = 4
        zoom = 10
        tiles = wgs84.tiles_from_geom(union, zoom)
        with fiona.open(fiji_tiles, 'w', 'GeoJSON', schema) as sink:
            for tile in tiles:
                zoom, row, col = tile
                feature = {}
                feature['geometry'] = mapping(wgs84.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

        # metatiles
        fiji_metatiles = os.path.join(outdata_directory, "fiji_metatiles.geojson")
        schema = {
            'geometry': 'Polygon',
            'properties': {'col': 'int', 'row': 'int'}
        }
        try:
            os.remove(fiji_metatiles)
        except:
            pass
        wgs84_meta = MetaTilePyramid(wgs84, metatiling)
        metatiles = wgs84_meta.tiles_from_geom(union, zoom)
        with fiona.open(fiji_metatiles, 'w', 'GeoJSON', schema) as sink:
            for metatile in metatiles:
                zoom, row, col = metatile
                feature = {}
                feature['geometry'] = mapping(wgs84_meta.tile_bbox(zoom, row, col))
                feature['properties'] = {}
                feature['properties']['col'] = col
                feature['properties']['row'] = row
                sink.write(feature)

    ## test get neighbors
    metatiling = 1
    wgs84_meta = MetaTilePyramid(wgs84, metatiling)
    tile = wgs84_meta.tile(5, 4, 3)
    assert len(tile.get_neighbors()) == 8

    ## test tile <--> metatile conversion
    metatile = [(10, 44, 33)]
    metatiling = 4
    wgs84_meta = MetaTilePyramid(wgs84, metatiling)
    small_tile = wgs84_meta.tilepyramid.tile(10, 178, 133)
    test_metatile = [
        (tile.zoom, tile.row, tile.col)
        for tile in wgs84_meta.tiles_from_bbox(
            small_tile.bbox(),
            10
        )
    ]
    try:
        assert metatile == test_metatile
        print "OK: metatile <-> tile conversion"
    except:
        print metatile, test_metatile
        raise
        print "ERROR: metatile <-> tile conversion"

    # test mercator tile pyramid
    tile_pyramid = TilePyramid("mercator")
    assert tile_pyramid.srid == 3857
    try:
        for zoom in range(15):
            assert (
                (tile_pyramid.matrix_width(zoom), tile_pyramid.matrix_height(zoom)
                ) == (2**zoom, 2**zoom)
            )
        print "OK: mercator tile matrix widths"
    except:
        print "ERROR: mercator tile matrix widths"


    from shapely.ops import transform
    from functools import partial
    import pyproj
    import mercantile

    example_tiles = [
        (12, 1024, 512),
        (6, 32, 16),
        (0, 0, 0),
        ]

    for tile_idx in example_tiles:
        (zoom, row, col) = tile_idx
        mercantile_ul = Point(
            mercantile.ul(col, row, zoom).lng,
            mercantile.ul(col, row, zoom).lat,
            )
        m_bounds = mercantile.bounds(col, row, zoom)
        mercantile_bbox = box(*m_bounds)
        project = partial(
            pyproj.transform,
            pyproj.Proj({"init": "epsg:3857"}),
            pyproj.Proj({"init": "epsg:4326"})
        )
        tilematrix_ul = transform(
            project,
            Point(
                Tile(tile_pyramid, zoom, row, col).left,
                Tile(tile_pyramid, zoom, row, col).top,
                )
            )
        tilematrix_bbox = transform(
            project,
            Tile(tile_pyramid, zoom, row, col).bbox()
            )

        try:
            assert mercantile_ul.almost_equals(
                tilematrix_ul,
                decimal=GEOM_EQUALS_ROUND
                )
            assert mercantile_bbox.almost_equals(
                tilematrix_bbox,
                decimal=GEOM_EQUALS_ROUND
                )
            print "OK: mercator tile coordinates"
        except AssertionError:
            print "ERROR: mercator tile coordinates"
            print tile_idx, mercantile_ul, tilematrix_ul
            print tile_idx, mercantile_bbox, tilematrix_bbox


    tile_idx = (12, 1024, 512)
    tile = Tile(tile_pyramid, *tile_idx)
    for child in tile.get_children():
        try:
            assert child.get_parent().id == tile.id
            print "OK: tile children and parent"
        except AssertionError:
            print child.parent.id, tile.id
            print "ERROR: tile children and parent"

    # get tiles over antimeridian:
    tile_pyramid = TilePyramid("geodetic")
    tile = tile_pyramid.tile(5, 0, 63)
    target_tiles = set(
        target_tile.id
        for target_tile in tile_pyramid.tiles_from_bounds(tile.bounds(256), 5)
    )
    control_tiles = set(
        [
            # tiles west of antimeridian
            (5, 0, 62),
            (5, 0, 63),
            (5, 1, 62),
            (5, 1, 63),
            # tiles east of antimeridian
            (5, 0, 0),
            (5, 1, 0)
        ]
    )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: tiles over antimeridian"
    except AssertionError:
        print "ERROR: tiles over antimeridian"
        print diff
    tile = tile_pyramid.tile(5, 0, 0)
    target_tiles = set(
        target_tile.id
        for target_tile in tile_pyramid.tiles_from_bounds(tile.bounds(256), 5)
    )
    control_tiles = set(
        [
            # tiles west of antimeridian
            (5, 0, 63),
            (5, 1, 63),
            # tiles east of antimeridian
            (5, 0, 0),
            (5, 1, 0),
            (5, 0, 1),
            (5, 1, 1)
        ]
    )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: tiles over antimeridian"
    except AssertionError:
        print "ERROR: tiles over antimeridian"
        print diff

    # get tiles over antimeridian:
    tile_pyramid = TilePyramid("mercator")
    tile = tile_pyramid.tile(5, 0, 31)
    target_tiles = set(
        target_tile.id
        for target_tile in tile_pyramid.tiles_from_bounds(tile.bounds(256), 5)
    )
    control_tiles = set(
        [
            # tiles west of antimeridian
            (5, 0, 30),
            (5, 0, 31),
            (5, 1, 30),
            (5, 1, 31),
            # tiles east of antimeridian
            (5, 0, 0),
            (5, 1, 0)
        ]
    )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: tiles over antimeridian"
    except AssertionError:
        print "ERROR: tiles over antimeridian"
        print diff
    tile = tile_pyramid.tile(5, 0, 0)
    target_tiles = set(
        target_tile.id
        for target_tile in tile_pyramid.tiles_from_bounds(tile.bounds(256), 5)
    )
    control_tiles = set(
        [
            # tiles west of antimeridian
            (5, 0, 31),
            (5, 1, 31),
            # tiles east of antimeridian
            (5, 0, 0),
            (5, 1, 0),
            (5, 0, 1),
            (5, 1, 1)
        ]
    )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: tiles over antimeridian"
    except AssertionError:
        print "ERROR: tiles over antimeridian"
        print diff

    # geometry over antimeridian
    tile_pyramid = TilePyramid("geodetic")
    geometry = loads("POLYGON ((184.375 90, 190 90, 180 84.375, 174.375 84.375, 184.375 90))")
    target_tiles = set(
        tile.id
        for tile in tile_pyramid.tiles_from_geom(geometry, 6)
        )
    control_tiles = set(
        [
            (6, 0, 127),
            (6, 1, 126),
            (6, 1, 127),
            (6, 0, 0),
            (6, 0, 1),
            (6, 0, 2),
            (6, 0, 3),
            (6, 1, 0),
            (6, 1, 1)
        ]
        )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: geometry over antimeridian"
    except AssertionError:
        print "ERROR: geometry over antimeridian"
        print diff

    tile_pyramid = TilePyramid("mercator")
    geometry = loads("POLYGON ((-22037508.3427892 20037508.3427892, -20785164.07136488 20037508.3427892, -18785164.07136488 18785164.07136488, -20037508.3427892 18785164.07136488, -22037508.3427892 20037508.3427892))")
    target_tiles = set(
        tile.id
        for tile in tile_pyramid.tiles_from_geom(geometry, 6)
        )
    tile_pyramid_bbox = box(
        tile_pyramid.left,
        tile_pyramid.bottom,
        tile_pyramid.right,
        tile_pyramid.top
    )
    for tile in target_tiles:
        assert tile_pyramid.tile(*tile).bbox().within(tile_pyramid_bbox)
    control_tiles = set(
        [
            # west of antimeridian
            (6, 0, 60),
            (6, 0, 61),
            (6, 0, 62),
            (6, 0, 63),
            (6, 1, 62),
            (6, 1, 63),
            # east of antimeridian
            (6, 0, 0),
            (6, 1, 0),
            (6, 1, 1)
        ]
        )
    try:
        diff = control_tiles.difference(target_tiles)
        assert len(diff) == 0
        print "OK: geometry over antimeridian"
    except AssertionError:
        print "ERROR: geometry over antimeridian"
        print target_tiles
        print diff

    # tile shapes
    tile_pyramid = TilePyramid("mercator")
    col, row = (0, 0)
    for zoom in range(10):
        tile = Tile(tile_pyramid, zoom, col, row)
        assert tile.shape() == (256, 256)
    metatile_pyramid = MetaTilePyramid(tile_pyramid, metatiles=8)
    control_shapes = [
        (256, 256),
        (512, 512),
        (1024, 1024),
        (2048, 2048),
        (2048, 2048),
        (2048, 2048),
        (2048, 2048),
        (2048, 2048),
        (2048, 2048),
        (2048, 2048)
        ]
    for zoom, control_shape in zip(range(9), control_shapes):
        tile = Tile(metatile_pyramid, zoom, col, row)
        try:
            assert tile.shape() == control_shape
            print "OK: metatile shape at zoom ", zoom
        except AssertionError:
            print "ERROR: metatile shape at zoom", zoom
            print tile.id, tile.shape(), control_shape
            raise

    # tile shapes with pixelbuffer
    tile_pyramid = TilePyramid("mercator")
    test_tiles = [
        (0, 0, 0),
        (1, 0, 0), # top left
        (2, 0, 1), # top middle
        (2, 0, 3), # top right
        (2, 3, 3), # bottom right
        (2, 3, 0), # bottom left
        (2, 3, 2), # bottom middle
        (2, 2, 0), # left middle
        (2, 2, 3), # right middle
        ]
    pixelbuffer = 2
    tile_size = tile_pyramid.tile_size
    control_shapes = [
        (tile_size, tile_size+2*pixelbuffer),
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # top left
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # top middle
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # top right
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # bottom right
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # bottom left
        (tile_size+1*pixelbuffer, tile_size+2*pixelbuffer),  # bottom middle
        (tile_size+2*pixelbuffer, tile_size+2*pixelbuffer),  # left middle
        (tile_size+2*pixelbuffer, tile_size+2*pixelbuffer),  # right middle
        ]
    for test_tile, control_shape in zip(test_tiles, control_shapes):
        tile = Tile(tile_pyramid, *test_tile)
        try:
            assert tile.shape(pixelbuffer) == control_shape
            print "OK: tile pixelbuffer shape for tile ", test_tile
        except AssertionError:
            print "ERROR: tile pixelbuffer shape for tile", test_tile
            print tile.id, tile.shape(pixelbuffer), control_shape
    # metatile shapes with pixelbuffer
    pixelbuffer = 2
    metatiling = 8
    metatile_pyramid = MetaTilePyramid(tile_pyramid, metatiles=metatiling)
    tile_size = tile_pyramid.tile_size
    test_tiles = [
        (0, 0, 0),
        (1, 0, 0),
        (2, 0, 0),
        (3, 0, 0),
        (4, 0, 0),
        (5, 0, 0),  # top left
        (5, 0, 1),  # top middle
        (5, 0, 3),  # top right
        (5, 3, 3),  # bottom right
        (5, 3, 0),  # bottom left
        (5, 3, 2),  # bottom middle
        (5, 2, 0),  # left middle
        (5, 2, 3),  # right middle
        ]
    for test_tile in test_tiles:
        tile = Tile(metatile_pyramid, *test_tile)
        left, bottom, right, top = tile.bounds(pixelbuffer)
        control_shape = (
            int(round((top-bottom)/tile.pixel_y_size)),
            int(round((right-left)/tile.pixel_x_size))
            )
        try:
            assert tile.shape(pixelbuffer) == control_shape
            print "OK: tile pixelbuffer shape for metatile ", test_tile
        except AssertionError:
            print "ERROR: tile pixelbuffer shape for metatile", test_tile
            print tile.id, tile.shape(pixelbuffer), control_shape

    """intersecting function"""
    try:
        # equal metatiling:
        tile = Tile(TilePyramid("geodetic", metatiling=1), 3, 4, 5)
        pyramid = TilePyramid("geodetic", metatiling=1)
        assert len(tile.intersecting(pyramid)) == 1
        assert tile.intersecting(pyramid)[0].id == tile.id

        # different metatiling:
        tile = Tile(TilePyramid("geodetic", metatiling=1), 3, 4, 5)
        pyramid_metatiling = 2
        pyramid = TilePyramid("geodetic", metatiling=pyramid_metatiling)
        assert len(tile.intersecting(pyramid)) == 1
        intersect = tile.intersecting(pyramid)[0]
        assert tile.bbox().within(intersect.bbox())
        tile = Tile(TilePyramid("geodetic", metatiling=8), 13, 4, 5)
        pyramid_metatiling = 2
        pyramid = TilePyramid("geodetic", metatiling=pyramid_metatiling)
        assert len(tile.intersecting(pyramid)) == 16
        for intersect in tile.intersecting(pyramid):
            assert intersect.bbox().within(tile.bbox())
        tile_list = set(
            tile.id
            for tile in tile.intersecting(pyramid)
        )
        reversed_list = set(
            tile.id
            for tile in pyramid.intersecting(tile)
        )
        assert not len(tile_list.symmetric_difference(reversed_list))
        print "OK: intersecting function"
    except:
        print "ERROR: intersecting function"
        raise

    """affine objects"""
    try:
        for metatiling in [1, 2, 4, 8, 16]:
            pyramid = TilePyramid("geodetic", metatiling=metatiling)
            for zoom in range(22):
                tile = pyramid.tile(zoom, 0, 0)
                assert tile.pixel_x_size == tile.pixel_y_size
                assert tile.affine()[0] == -tile.affine()[4]
                assert tile.affine(pixelbuffer=10)[0] == \
                    -tile.affine(pixelbuffer=10)[4]
        print "OK: Affine objects"
    except:
        print "ERROR: Affine objects"
        raise