From cc6fd55a9e5099f3c3853632ffc073bc7f0f0eb0 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 1 Nov 2017 19:51:43 +0000 Subject: [PATCH 1/7] Handle small numerical precision issues in conversion of bounding box to tile coverage. --- tests/test_query_rawr.py | 60 ++++++++++++++++++++++++++++++++++++++++ tilequeue/query/rawr.py | 38 +++++++++++++++++++------ tilequeue/tile.py | 9 ++++-- 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/tests/test_query_rawr.py b/tests/test_query_rawr.py index ece716a5..f3dace70 100644 --- a/tests/test_query_rawr.py +++ b/tests/test_query_rawr.py @@ -671,3 +671,63 @@ def min_zoom_fn(shape, props, fid, meta): self.assertEquals(1, read_row.get('__id__')) self.assertEquals({'min_zoom': 11, 'highway': 'secondary'}, read_row.get('__testlayer_properties__')) + + +class TestTileFootprint(unittest.TestCase): + + def test_single_tile(self): + from tilequeue.query.rawr import _tiles + from raw_tiles.tile import Tile + from raw_tiles.util import bbox_for_tile + + tile = Tile(15, 5241, 12665) + + zoom = tile.z + unpadded_bounds = bbox_for_tile(tile.z, tile.x, tile.y) + tiles = _tiles(zoom, unpadded_bounds) + + self.assertEquals([tile], list(tiles)) + + def test_multiple_tiles(self): + from tilequeue.query.rawr import _tiles + from raw_tiles.tile import Tile + from raw_tiles.util import bbox_for_tile + + tile = Tile(15, 5241, 12665) + + # query at one zoom higher - should get 4 child tiles. + zoom = tile.z + 1 + unpadded_bounds = bbox_for_tile(tile.z, tile.x, tile.y) + tiles = list(_tiles(zoom, unpadded_bounds)) + + self.assertEquals(4, len(tiles)) + for child in tiles: + self.assertEquals(tile, child.parent()) + + def test_corner_overlap(self): + # a box around the corner of a tile should return the four + # neighbours of that tile. + from tilequeue.query.rawr import _tiles + from raw_tiles.tile import Tile + from raw_tiles.util import bbox_for_tile + + tile = Tile(15, 5241, 12665) + + zoom = tile.z + tile_bbox = bbox_for_tile(tile.z, tile.x, tile.y) + + # extract the top left corner + x = tile_bbox[0] + y = tile_bbox[3] + # make a small bounding box around that + w = 10 + unpadded_bounds = (x - w, y - w, + x + w, y + w) + tiles = set(_tiles(zoom, unpadded_bounds)) + + expected = set() + for dx in (0, -1): + for dy in (0, -1): + expected.add(Tile(tile.z, tile.x + dx, tile.y + dy)) + + self.assertEquals(expected, tiles) diff --git a/tilequeue/query/rawr.py b/tilequeue/query/rawr.py index b154b448..8157e945 100644 --- a/tilequeue/query/rawr.py +++ b/tilequeue/query/rawr.py @@ -246,25 +246,47 @@ def transit_relations(self, rel_id): return set(self.relations_using_rel(rel_id)) +def _snapping_round(num, eps, resolution): + """ + Return num snapped to within eps of an integer, or int(resolution(num)). + """ + + rounded = round(num) + delta = abs(num - rounded) + if delta < eps: + return int(rounded) + else: + return int(resolution(num)) + + # yield all the tiles at the given zoom level which intersect the given bounds. def _tiles(zoom, unpadded_bounds): - from tilequeue.tile import mercator_point_to_coord + from tilequeue.tile import mercator_point_to_coord_fractional from raw_tiles.tile import Tile + import math minx, miny, maxx, maxy = unpadded_bounds - topleft = mercator_point_to_coord(zoom, minx, miny) - bottomright = mercator_point_to_coord(zoom, maxx, maxy) + topleft = mercator_point_to_coord_fractional(zoom, minx, maxy) + bottomright = mercator_point_to_coord_fractional(zoom, maxx, miny) # make sure that the bottom right coordinate is below and to the right # of the top left coordinate. it can happen that the coordinates are # mixed up due to small numerical precision artefacts being enlarged # by the conversion to integer and y-coordinate flip. assert topleft.zoom == bottomright.zoom - bottomright.column = max(bottomright.column, topleft.column) - bottomright.row = max(bottomright.row, topleft.row) - - for x in range(int(topleft.column), int(bottomright.column) + 1): - for y in range(int(topleft.row), int(bottomright.row) + 1): + minx = min(topleft.column, bottomright.column) + maxx = max(topleft.column, bottomright.column) + miny = min(topleft.row, bottomright.row) + maxy = max(topleft.row, bottomright.row) + + eps = 1.0e-5 + minx = _snapping_round(minx, eps, math.floor) + maxx = _snapping_round(maxx, eps, math.ceil) + miny = _snapping_round(miny, eps, math.floor) + maxy = _snapping_round(maxy, eps, math.ceil) + + for x in range(minx, maxx): + for y in range(miny, maxy): tile = Tile(zoom, x, y) yield tile diff --git a/tilequeue/tile.py b/tilequeue/tile.py index 6006afe6..0e443972 100644 --- a/tilequeue/tile.py +++ b/tilequeue/tile.py @@ -104,13 +104,18 @@ def reproject_mercator_to_lnglat(x, y, *unused_coords): half_earth_circum = earth_circum / 2 -def mercator_point_to_coord(z, x, y): +def mercator_point_to_coord_fractional(z, x, y): coord = Coordinate( column=x + half_earth_circum, row=half_earth_circum - y, zoom=coord_mercator_point_zoom, ) - coord = coord.zoomTo(z).container() + coord = coord.zoomTo(z) + return coord + + +def mercator_point_to_coord(z, x, y): + coord = mercator_point_to_coord_fractional(z, x, y).container() return coord From baba54bce32bba5ee2bed3e115018e3ff3944484 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 1 Nov 2017 19:54:39 +0000 Subject: [PATCH 2/7] Be consistent about line/linestring usage. Move it all to Enum-land instead. --- tilequeue/query/__init__.py | 14 ++---------- tilequeue/query/common.py | 44 +++++++++++++++++++++++++++++++++++++ tilequeue/query/rawr.py | 34 +++++----------------------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/tilequeue/query/__init__.py b/tilequeue/query/__init__.py index 9db1faad..74999f45 100644 --- a/tilequeue/query/__init__.py +++ b/tilequeue/query/__init__.py @@ -141,7 +141,7 @@ def _make_rawr_fetcher(cfg, layer_data, query_cfg, io_pool): def _make_layer_info(layer_data, process_yaml_cfg): - from tilequeue.query.common import LayerInfo + from tilequeue.query.common import LayerInfo, parse_shape_types layers = {} functions = _parse_yaml_functions(process_yaml_cfg) @@ -149,23 +149,13 @@ def _make_layer_info(layer_data, process_yaml_cfg): for layer_datum in layer_data: name = layer_datum['name'] min_zoom_fn, props_fn = functions[name] - shape_types = _parse_shape_types(layer_datum['geometry_types']) + shape_types = parse_shape_types(layer_datum['geometry_types']) layer_info = LayerInfo(min_zoom_fn, props_fn, shape_types) layers[name] = layer_info return layers -def _parse_shape_types(inputs): - outputs = set() - for value in inputs: - if value.startswith('Multi'): - value = value[len('Multi'):] - outputs.add(value.lower()) - - return outputs or None - - def _parse_yaml_functions(process_yaml_cfg): from tilequeue.command import make_output_calc_mapping from tilequeue.command import make_min_zoom_calc_mapping diff --git a/tilequeue/query/common.py b/tilequeue/query/common.py index 3d6ef067..226346c6 100644 --- a/tilequeue/query/common.py +++ b/tilequeue/query/common.py @@ -2,6 +2,7 @@ from collections import defaultdict from itertools import izip from tilequeue.process import Source +from enum import Enum def namedtuple_with_defaults(name, props, defaults): @@ -20,6 +21,49 @@ def allows_shape_type(self, shape): return typ in self.shape_types +class ShapeType(Enum): + point = 1 + line = 2 + polygon = 3 + + +# determine the shape type from the raw WKB bytes. this means we don't have to +# parse the WKB, which can be an expensive operation for large polygons. +def wkb_shape_type(wkb): + reverse = ord(wkb[0]) == 1 + type_bytes = map(ord, wkb[1:5]) + if reverse: + type_bytes.reverse() + typ = type_bytes[3] + if typ == 1 or typ == 4: + return ShapeType.point + elif typ == 2 or typ == 5: + return ShapeType.line + elif typ == 3 or typ == 6: + return ShapeType.polygon + else: + assert False, "WKB shape type %d not understood." % (typ,) + + +def parse_shape_types(inputs): + lookup = { + 'point': ShapeType.point, + 'multipoint': ShapeType.point, + 'linestring': ShapeType.line, + 'multilinestring': ShapeType.line, + 'polygon': ShapeType.polygon, + 'multipolygon': ShapeType.polygon, + } + outputs = set() + for value in inputs: + t = lookup.get(value.lower()) + if t is None: + raise ValueError("%r not understood as shape type" % value) + outputs.add(t) + + return outputs or None + + def deassoc(x): """ Turns an array consisting of alternating key-value pairs into a diff --git a/tilequeue/query/rawr.py b/tilequeue/query/rawr.py index 8157e945..ec57fc38 100644 --- a/tilequeue/query/rawr.py +++ b/tilequeue/query/rawr.py @@ -8,11 +8,12 @@ from tilequeue.query.common import mz_is_interesting_transit_relation from tilequeue.query.common import shape_type_lookup from tilequeue.query.common import name_keys +from tilequeue.query.common import wkb_shape_type +from tilequeue.query.common import ShapeType from tilequeue.transform import calculate_padded_bounds from tilequeue.utils import CoordsByParent from raw_tiles.tile import shape_tile_coverage from math import floor -from enum import Enum class Relation(object): @@ -55,30 +56,6 @@ def bbox(self): return box(*self.bounds()) -class ShapeType(Enum): - point = 1 - line = 2 - polygon = 3 - - -# determine the shape type from the raw WKB bytes. this means we don't have to -# parse the WKB, which can be an expensive operation for large polygons. -def _wkb_shape(wkb): - reverse = ord(wkb[0]) == 1 - type_bytes = map(ord, wkb[1:5]) - if reverse: - type_bytes.reverse() - typ = type_bytes[3] - if typ == 1 or typ == 4: - return ShapeType.point - elif typ == 2 or typ == 5: - return ShapeType.line - elif typ == 3 or typ == 6: - return ShapeType.polygon - else: - assert False, "WKB shape type %d not understood." % (typ,) - - # return true if the tuple of values corresponds to, and each is an instance # of, the tuple of types. this is used to make sure that argument lists are # the right "shape" before destructuring (splatting?) them in a function call. @@ -151,7 +128,7 @@ def add_feature(self, fid, shape_wkb, props): if fid < 0: return - shape_type = _wkb_shape(shape_wkb) + shape_type = wkb_shape_type(shape_wkb) if is_station_or_stop(fid, None, props) and \ shape_type == ShapeType.point: # must be a station or stop node @@ -392,12 +369,11 @@ def _index_feature(self, feature, osm, source): layer_min_zooms = feature.layer_min_zooms # grab the shape type without decoding the WKB to save time. - shape_type = _wkb_shape(shape.wkb) + shape_type = wkb_shape_type(shape.wkb) meta = _make_meta(source, fid, shape_type, osm) for layer_name, info in self.layers.items(): - shape_type_str = shape_type.name - if info.shape_types and shape_type_str not in info.shape_types: + if info.shape_types and shape_type not in info.shape_types: continue min_zoom = info.min_zoom_fn(shape, props, fid, meta) if min_zoom is not None: From 6b73ffccb70aee3c037ace05cacdf696abfa9b23 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 1 Nov 2017 19:55:28 +0000 Subject: [PATCH 3/7] Handle missing nodes, ways and relations in the index. This can happen because we don't have perfect information about bi-directional linkages when building the index and, rather than index every element, the index is selective about what it stores. When the selection doesn't anticipate relation membership, it can result in a reference to a non-indexed element. At the moment, I don't think this is a problem, as those elements wouldn't have been "interesting" anyway. --- tilequeue/query/common.py | 17 +++++++++++++---- tilequeue/query/rawr.py | 18 ++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tilequeue/query/common.py b/tilequeue/query/common.py index 226346c6..15fb5931 100644 --- a/tilequeue/query/common.py +++ b/tilequeue/query/common.py @@ -220,7 +220,7 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): seed_relations = set() for rel_id in candidate_relations: rel = osm.relation(rel_id) - if mz_is_interesting_transit_relation(rel.tags): + if rel and mz_is_interesting_transit_relation(rel.tags): seed_relations.add(rel_id) del candidate_relations @@ -246,8 +246,11 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): stations_and_stops = set() for rel_id in all_relations: rel = osm.relation(rel_id) + if not rel: + continue for node_id in rel.node_ids: - if is_station_or_stop(*osm.node(node_id)): + node = osm.node(node_id) + if node and is_station_or_stop(*node): stations_and_stops.add(node_id) if node_id: @@ -258,7 +261,8 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): stations_and_lines = set() for node_id in stations_and_stops: for way_id in osm.ways_using_node(node_id): - if is_station_or_line(*osm.way(way_id)): + way = osm.way(way_id) + if way and is_station_or_line(*way): stations_and_lines.add(way_id) if way_id: @@ -273,7 +277,8 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): for i in ids: for rel_id in lookup(i): rel = osm.relation(rel_id) - if rel.tags.get('type') == 'route' and \ + if rel and \ + rel.tags.get('type') == 'route' and \ rel.tags.get('route') in ('subway', 'light_rail', 'tram', 'train', 'railway'): all_routes.add(rel_id) @@ -281,6 +286,8 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): routes_lookup = defaultdict(set) for rel_id in all_routes: rel = osm.relation(rel_id) + if not rel: + continue route = rel.tags.get('route') if route: route_name = mz_transit_route_name(rel.tags) @@ -380,6 +387,8 @@ def layer_properties(fid, shape, props, layer_name, zoom, osm): mz_is_bus_route = False for rel_id in osm.relations_using_way(fid): rel = osm.relation(rel_id) + if not rel: + continue typ, route, network, ref = [rel.tags.get(k) for k in ( 'type', 'route', 'network', 'ref')] if route and (network or ref): diff --git a/tilequeue/query/rawr.py b/tilequeue/query/rawr.py index ec57fc38..91e9c48b 100644 --- a/tilequeue/query/rawr.py +++ b/tilequeue/query/rawr.py @@ -199,7 +199,7 @@ def ways_using_node(self, node_id): def relation(self, rel_id): "Returns the Relation object with the given ID." - return self.relations[rel_id] + return self.relations.get(rel_id) def way(self, way_id): """ @@ -207,7 +207,7 @@ def way(self, way_id): given way. """ - return self.ways[way_id] + return self.ways.get(way_id) def node(self, node_id): """ @@ -215,7 +215,7 @@ def node(self, node_id): given node. """ - return self.nodes[node_id] + return self.nodes.get(node_id) def transit_relations(self, rel_id): "Return transit relations containing the relation with the given ID." @@ -304,14 +304,20 @@ def _make_meta(source, fid, shape_type, osm): # fetch ways and relations for any node if fid >= 0 and shape_type == ShapeType.point: for way_id in osm.ways_using_node(fid): - ways.append(osm.way(way_id)) + way = osm.way(way_id) + if way: + ways.append(way) for rel_id in osm.relations_using_node(fid): - rels.append(osm.relation(rel_id)) + rel = osm.relation(rel_id) + if rel: + rels.append(rel) # and relations for any way if fid >= 0 and shape_type == ShapeType.line: for rel_id in osm.relations_using_way(fid): - rels.append(osm.relation(rel_id)) + rel = osm.relation(rel_id) + if rel: + rels.append(rel) # have to transform the Relation object into a dict, which is # what the functions called on this data expect. From 4b26024411c95856f1b373156cf892f5b206a4e1 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 1 Nov 2017 19:58:32 +0000 Subject: [PATCH 4/7] Store sorted list of route names, rather than set. Some versions of ujson seem to be unhappy with serialising a set. --- tilequeue/query/common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tilequeue/query/common.py b/tilequeue/query/common.py index 15fb5931..5f335b85 100644 --- a/tilequeue/query/common.py +++ b/tilequeue/query/common.py @@ -292,11 +292,11 @@ def mz_calculate_transit_routes_and_score(osm, node_id, way_id, rel_id): if route: route_name = mz_transit_route_name(rel.tags) routes_lookup[route].add(route_name) - trains = routes_lookup['train'] - subways = routes_lookup['subway'] - light_rails = routes_lookup['light_rail'] - trams = routes_lookup['tram'] - railways = routes_lookup['railway'] + trains = list(sorted(routes_lookup['train'])) + subways = list(sorted(routes_lookup['subway'])) + light_rails = list(sorted(routes_lookup['light_rail'])) + trams = list(sorted(routes_lookup['tram'])) + railways = list(sorted(routes_lookup['railway'])) del routes_lookup # if a station is an interchange between mainline rail and subway or From 43a00aecc3bc26819d8ad84a48783e25148d34dc Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 1 Nov 2017 19:59:33 +0000 Subject: [PATCH 5/7] Cache the bounds of a LazyShape, and use that to skip unnecessary calls to geometry intersection. --- tilequeue/query/rawr.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tilequeue/query/rawr.py b/tilequeue/query/rawr.py index 91e9c48b..e822f9eb 100644 --- a/tilequeue/query/rawr.py +++ b/tilequeue/query/rawr.py @@ -287,12 +287,21 @@ class _LazyShape(object): def __init__(self, wkb): self.wkb = wkb self.obj = None + self._bounds = None def __getattr__(self, name): if self.obj is None: self.obj = wkb_loads(self.wkb) return getattr(self.obj, name) + @property + def bounds(self): + if self.obj is None: + self.obj = wkb_loads(self.wkb) + if self._bounds is None: + self._bounds = self.obj.bounds + return self._bounds + _Metadata = namedtuple('_Metadata', 'source ways relations') @@ -581,7 +590,11 @@ def _parse_row(self, zoom, unpadded_bounds, bbox, source, fid, shape, pad_factor = 1.1 clip_box = calculate_padded_bounds( pad_factor, unpadded_bounds) - clip_shape = clip_box.intersection(shape) + # don't need to clip if geom is fully within the clipping box + if box(*shape.bounds).within(clip_box): + clip_shape = shape + else: + clip_shape = clip_box.intersection(shape) read_row['__geometry__'] = bytes(clip_shape.wkb) if generate_label_placement: From 2cb637f24074236053bdc1b82aa4322afdde38a8 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 2 Nov 2017 10:27:38 +0000 Subject: [PATCH 6/7] Move ShapeType parsing into a class method of ShapeType. This means that the lookup dictionary isn't being created for each call, and that the code is "adjacent" to the Enum it depends on, which hopefully makes it easier to understand. --- tests/test_query_common.py | 45 +++++++++++++++++++++++++++++++++++++ tilequeue/query/__init__.py | 4 ++-- tilequeue/query/common.py | 34 +++++++++++++--------------- 3 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 tests/test_query_common.py diff --git a/tests/test_query_common.py b/tests/test_query_common.py new file mode 100644 index 00000000..b576e045 --- /dev/null +++ b/tests/test_query_common.py @@ -0,0 +1,45 @@ +import unittest + + +class TestCommon(unittest.TestCase): + + def test_parse_shape_types(self): + from tilequeue.query.common import ShapeType + + def _test(expects, inputs): + self.assertEquals(set(expects), ShapeType.parse_set(inputs)) + + # basic types + _test([ShapeType.point], ['point']) + _test([ShapeType.line], ['line']) + _test([ShapeType.polygon], ['polygon']) + + # should be case insensitive + _test([ShapeType.point], ['Point']) + _test([ShapeType.line], ['LINE']) + _test([ShapeType.polygon], ['Polygon']) + + # should handle OGC-style names, including Multi* + _test([ShapeType.point], ['MultiPoint']) + _test([ShapeType.line], ['LineString']) + _test([ShapeType.line], ['MultiLineString']) + _test([ShapeType.polygon], ['MultiPolygon']) + + # should handle multiple, repeated names + _test([ShapeType.point], ['Point', 'MultiPoint']) + _test([ + ShapeType.point, + ShapeType.line, + ShapeType.polygon, + ], [ + 'Point', 'MultiPoint', + 'Line', 'LineString', 'MultiLineString', + 'Polygon', 'MultiPolygon' + ]) + + # should return None rather than an empty set. + self.assertEquals(None, ShapeType.parse_set([])) + + # should throw KeyError if the name isn't recognised + with self.assertRaises(KeyError): + ShapeType.parse_set(['MegaPolygon']) diff --git a/tilequeue/query/__init__.py b/tilequeue/query/__init__.py index 74999f45..cdf8429f 100644 --- a/tilequeue/query/__init__.py +++ b/tilequeue/query/__init__.py @@ -141,7 +141,7 @@ def _make_rawr_fetcher(cfg, layer_data, query_cfg, io_pool): def _make_layer_info(layer_data, process_yaml_cfg): - from tilequeue.query.common import LayerInfo, parse_shape_types + from tilequeue.query.common import LayerInfo, ShapeType layers = {} functions = _parse_yaml_functions(process_yaml_cfg) @@ -149,7 +149,7 @@ def _make_layer_info(layer_data, process_yaml_cfg): for layer_datum in layer_data: name = layer_datum['name'] min_zoom_fn, props_fn = functions[name] - shape_types = parse_shape_types(layer_datum['geometry_types']) + shape_types = ShapeType.parse_set(layer_datum['geometry_types']) layer_info = LayerInfo(min_zoom_fn, props_fn, shape_types) layers[name] = layer_info diff --git a/tilequeue/query/common.py b/tilequeue/query/common.py index 5f335b85..eb08ee28 100644 --- a/tilequeue/query/common.py +++ b/tilequeue/query/common.py @@ -26,6 +26,21 @@ class ShapeType(Enum): line = 2 polygon = 3 + # aliases, don't use these directly! + multipoint = 1 + linestring = 2 + multilinestring = 2 + multipolygon = 3 + + @classmethod + def parse_set(cls, inputs): + outputs = set() + for value in inputs: + t = cls[value.lower()] + outputs.add(t) + + return outputs or None + # determine the shape type from the raw WKB bytes. this means we don't have to # parse the WKB, which can be an expensive operation for large polygons. @@ -45,25 +60,6 @@ def wkb_shape_type(wkb): assert False, "WKB shape type %d not understood." % (typ,) -def parse_shape_types(inputs): - lookup = { - 'point': ShapeType.point, - 'multipoint': ShapeType.point, - 'linestring': ShapeType.line, - 'multilinestring': ShapeType.line, - 'polygon': ShapeType.polygon, - 'multipolygon': ShapeType.polygon, - } - outputs = set() - for value in inputs: - t = lookup.get(value.lower()) - if t is None: - raise ValueError("%r not understood as shape type" % value) - outputs.add(t) - - return outputs or None - - def deassoc(x): """ Turns an array consisting of alternating key-value pairs into a From a189daee560cda49fa2da917435c15f122595ed1 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 3 Nov 2017 15:58:16 +0000 Subject: [PATCH 7/7] Update EmptyToiIntersector interface to return timing. --- tilequeue/rawr.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tilequeue/rawr.py b/tilequeue/rawr.py index d35004cd..ad1b43bc 100644 --- a/tilequeue/rawr.py +++ b/tilequeue/rawr.py @@ -261,8 +261,13 @@ def __call__(self, coords): hits=0, misses=len(coords), n_toi=0, + cached=False, ) - return [], metrics + timing = dict( + fetch=0, + intersect=0, + ) + return [], metrics, timing class RawrTileGenerationPipeline(object):