Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for preprocessed inline geojson layers #414

Merged
merged 14 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tilequeue/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ def parse_layer_data(query_cfg, buffer_cfg, cfg_path):
query_bounds_pad_fn=create_query_bounds_pad_fn(
buffer_cfg, layer_name),
tolerance=float(layer_config.get('tolerance', 1.0)),
pre_processed_layer_path=layer_config.get('pre_processed_layer_path'),
)
layer_data.append(layer_datum)
if layer_name in all_layer_names:
Expand Down Expand Up @@ -2176,6 +2177,8 @@ def tilequeue_meta_tile(cfg, args):
# each coord here is the unit of work now
pyramid_coords = [job_coord]
pyramid_coords.extend(coord_children_range(job_coord, zoom_stop))
# for brevity of testing it is sometimes convenient to reduce to a single child coord
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, say single child cood (here over XYZ placename)

# pyramid_coords = [Coordinate(zoom=13, column=2411, row=3080)]
coord_data = [dict(coord=x) for x in pyramid_coords]

try:
Expand Down
52 changes: 48 additions & 4 deletions tilequeue/process.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from __future__ import division

import itertools
import json
import os.path
from collections import defaultdict
from collections import namedtuple
from cStringIO import StringIO
from sys import getsizeof

from shapely import geometry
from shapely.geometry import GeometryCollection
from shapely.geometry import MultiPolygon
from shapely.geometry import shape
from shapely.wkb import loads
from zope.dottedname.resolve import resolve

Expand Down Expand Up @@ -69,12 +74,13 @@ def _sizeof(val):
'log',
])


# post-process all the layers simultaneously, which allows new
# layers to be created from processing existing ones (e.g: for
# computed centroids) or modifying layers based on the contents
# of other layers (e.g: projecting attributes, deleting hidden
# features, etc...)


def _postprocess_data(
feature_layers, post_process_data, nominal_zoom, unpadded_bounds,
log_fn=None):
Expand Down Expand Up @@ -112,7 +118,6 @@ def _log_fn(data):
# append it.
if layer is not None:
feature_layers.append(layer)

return feature_layers


Expand Down Expand Up @@ -146,7 +151,6 @@ def _cut_coord(
padded_bounds=padded_bounds,
)
cut_feature_layers.append(cut_feature_layer)

return cut_feature_layers


Expand Down Expand Up @@ -286,6 +290,12 @@ def process_coord_no_format(
# filter, and then transform each layer as necessary
for feature_layer in feature_layers:
layer_datum = feature_layer['layer_datum']
# inline layers are expected to be pre-processed
layer_path = layer_datum.get('pre_processed_layer_path')
if layer_path is not None:
processed_feature_layers.append(feature_layer)
continue

layer_name = layer_datum['name']
geometry_types = layer_datum['geometry_types']
padded_bounds = feature_layer['padded_bounds']
Expand Down Expand Up @@ -512,6 +522,33 @@ def _calculate_scale(scale, coord, nominal_zoom):
return scale


def _load_inline_layer(layer_path):
# we can optionally load geojson layers from file all we need is a file path. we expect the coordinates to already
# be in mercator projection and we only fill out the minimal structure of the feature tuple, ie there's no ID. if
# the path is given and the file exists, we expect to be able to load the layer's features. so if the json is
# malformed or shapely can't make sense of the geom then we'll raise
features = []

# has to exist
if not os.path.isfile(layer_path):
return features

# load the geojson into a shapely geometry collection
with open(layer_path) as fh:
fc = json.load(fh)
# skip if this isnt pseudo mercator
if fc['crs']['properties']['name'] != 'urn:ogc:def:crs:EPSG::3857':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern, is there any log we can emit to tell whether it happens, otherwise it is hard to debug if it not working as expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we can log as it was clear the person meant to load it but screwed something up. maybe same for the file extension

raise Exception('Pre-processed layers must be in pseudo mercator projection')
gc = GeometryCollection([shape(feature['geometry']) for feature in fc['features']])

# add the features geometries with their properties in tuples
for geom, feat in itertools.izip(gc.geoms, fc['features']):
props = feat['properties']
features.append((geom, props, props.get('id')))

return features


def format_coord(
coord, nominal_zoom, max_zoom_with_changes, processed_feature_layers, formats,
unpadded_bounds, cut_coords, buffer_cfg, extra_data, scale):
Expand Down Expand Up @@ -645,7 +682,7 @@ def convert_source_data_to_feature_layers(rows, layer_data, unpadded_bounds, zoo
# expecting downstream in the process_coord function
for layer_datum in layer_data:
layer_name = layer_datum['name']
layer_props = row_props_by_layer[layer_name]
layer_props = row_props_by_layer.get(layer_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any side-effect this might introduce?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think so. if you scroll up just a bit you'll see that this object has props for every layer except this new landmarks layer. those are set to either a value or to None. now there isnt one for landmarks so changing this to get will just add handling for landmarks not existing above. alternatively i could have added landmarks above and just set it to None but there are a lot of places in the code that say something like "TODO: stop hardcoding all the layers" so i didnt want to add to the mess.

if layer_props is not None:
props = common_props.copy()
props.update(layer_props)
Expand All @@ -665,6 +702,13 @@ def convert_source_data_to_feature_layers(rows, layer_data, unpadded_bounds, zoo

features_by_layer[layer_name].append(query_props)

# inline layers contain a path to their features inline in their datum
for layer_datum in layer_data:
layer_name = layer_datum['name']
layer_path = layer_datum.get('pre_processed_layer_path')
if layer_path is not None:
features_by_layer[layer_name].extend(_load_inline_layer(layer_path))

feature_layers = []
for layer_datum in layer_data:
layer_name = layer_datum['name']
Expand Down
3 changes: 3 additions & 0 deletions tilequeue/query/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def _make_layer_info(layer_data, process_yaml_cfg):
functions = _parse_yaml_functions(process_yaml_cfg)

for layer_datum in layer_data:
# preprocessed layers are not from the database and not found in the rawr tile
if layer_datum.get('pre_processed_layer_path') is not None:
continue
name = layer_datum['name']
min_zoom_fn, props_fn = functions[name]
shape_types = ShapeType.parse_set(layer_datum['geometry_types'])
Expand Down