-
Notifications
You must be signed in to change notification settings - Fork 137
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
Implement buffer_size overrides #399
Changes from 4 commits
fed934c
ba82c5a
f4e804c
996927d
917fe65
381f6bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||||||
from dataclasses import dataclass | ||||||||||
import os | ||||||||||
from pathlib import Path | ||||||||||
from typing import List, Union, Dict, Any, Callable | ||||||||||
from typing import List, Union, Dict, Any, Callable, Optional, NewType | ||||||||||
|
||||||||||
import sys | ||||||||||
import warnings | ||||||||||
|
@@ -10,13 +11,35 @@ | |||||||||
from .utils import print_err | ||||||||||
|
||||||||||
|
||||||||||
GetEnv = NewType('GetEnv', Callable[[str, Optional[str]], Optional[str]]) | ||||||||||
|
||||||||||
|
||||||||||
def tag_fields_to_sql(fields): | ||||||||||
"""Converts a list of fields stored in the tags hstore into a list of SQL fields: | ||||||||||
name:en => NULLIF(tags->'name:en', '') AS name:en | ||||||||||
""" | ||||||||||
return [f"NULLIF(tags->'{fld}', '') AS \"{fld}\"" for fld in fields] | ||||||||||
|
||||||||||
|
||||||||||
def assert_int(value, name: str, min_val: Optional[int] = None, max_val: Optional[int] = None, required=False): | ||||||||||
if value is None: | ||||||||||
if required: | ||||||||||
raise ValueError(f'Value {name} does not exist') | ||||||||||
return None | ||||||||||
elif isinstance(value, str): | ||||||||||
try: | ||||||||||
value = int(value) | ||||||||||
except ValueError: | ||||||||||
raise ValueError(f'Unable to parse {name} value "{value}" as an integer') | ||||||||||
elif not isinstance(value, int): | ||||||||||
raise ValueError(f'The {name} value was expected to be an integer, but found {type(value).__name__} "{value}"') | ||||||||||
if min_val is not None and value < min_val: | ||||||||||
raise ValueError(f'The {name} value {value} is less than the minimum allowed {min_val}') | ||||||||||
if max_val is not None and value > max_val: | ||||||||||
raise ValueError(f'The {name} value {value} is more than the maximum allowed {max_val}') | ||||||||||
return value | ||||||||||
|
||||||||||
|
||||||||||
@dataclass | ||||||||||
class ParsedData: | ||||||||||
data: Union[dict, str] | ||||||||||
|
@@ -70,14 +93,17 @@ def parse(layer_source: Union[str, Path, ParsedData]) -> 'Layer': | |||||||||
def __init__(self, | ||||||||||
layer_source: Union[str, Path, ParsedData], | ||||||||||
tileset: 'Tileset' = None, | ||||||||||
index: int = None): | ||||||||||
index: int = None, | ||||||||||
overrides: dict = None): | ||||||||||
""" | ||||||||||
:param layer_source: load layer from this source, e.g. a file | ||||||||||
:param tileset: parent tileset object (optional) | ||||||||||
:param index: layer's position index within the tileset | ||||||||||
:param overrides: additional override parameters for this layer | ||||||||||
""" | ||||||||||
self.tileset = tileset | ||||||||||
self.index = index | ||||||||||
self.overrides = overrides | ||||||||||
|
||||||||||
if isinstance(layer_source, ParsedData): | ||||||||||
self.filename = layer_source.path | ||||||||||
|
@@ -116,7 +142,7 @@ def __init__(self, | |||||||||
requires = requires.copy() # dict will be modified to detect unrecognized properties | ||||||||||
|
||||||||||
err = 'If set, "requires" parameter must be a map with optional "layers", "tables", and "functions" sub-elements. Each sub-element must be a string or a list of strings. If "requires" is a list or a string itself, it is treated as a list of layers. ' + \ | ||||||||||
'Optionally add "helpText" sub-element string to help the user with generating missing tables and functions.' | ||||||||||
'Optionally add "helpText" sub-element string to help the user with generating missing tables and functions.' | ||||||||||
self.requires_layers = get_requires_prop( | ||||||||||
requires, 'layers', | ||||||||||
err + '"requires.layers" must be an ID of another layer, or a list of layer IDs.') | ||||||||||
|
@@ -147,17 +173,6 @@ def __init__(self, | |||||||||
# osm_id column twice - once for feature_id, and once as an attribute | ||||||||||
raise ValueError('key_field_as_attribute=yes is not yet implemented') | ||||||||||
|
||||||||||
@deprecated(version='3.2.0', reason='use named properties instead') | ||||||||||
def __getitem__(self, attr): | ||||||||||
if attr in self.definition: | ||||||||||
return self.definition[attr] | ||||||||||
elif attr == 'fields': | ||||||||||
return {} | ||||||||||
elif attr == 'description': | ||||||||||
return '' | ||||||||||
else: | ||||||||||
raise KeyError | ||||||||||
|
||||||||||
def get_fields(self) -> List[str]: | ||||||||||
"""Get a list of field names this layer generates. | ||||||||||
Geometry field is not included.""" | ||||||||||
|
@@ -181,7 +196,49 @@ def description(self) -> str: | |||||||||
|
||||||||||
@property | ||||||||||
def buffer_size(self) -> int: | ||||||||||
return self.definition['layer']['buffer_size'] | ||||||||||
"""Each layer must have a default buffer size, with either `buffer_size` or `min_buffer_size` or both. | ||||||||||
If both are set, `buffer_size` must be >= `min_buffer_size`. | ||||||||||
min_buffer_size is only used when there is a global buffer size override, | ||||||||||
e.g. if global `buffer_size` is set to 0, and layer's `min_buffer_size` is set to 4, the result is 4. | ||||||||||
Per layer tileset overrides are allowed for both buffer_size and min_buffer_size. | ||||||||||
nyurik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
Per layer overrides have higher priority than global overrides, but less than ENV var. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding of this sentence is that env. variable has higher priority than per-layer min_buffer_size override.
Suggested change
As far as I understand the code, the final buffer size of a layer is
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not exactly, the overrides follow this logic (or at least it should):
Note that there is no global min_buffer_size override as that has very little value IMO. Also, while there is a fallback to 0 for both, the layer file requires either one to be present. Lastly, both vars are computed independently until the very end. |
||||||||||
""" | ||||||||||
size = assert_int(self.definition['layer'].get('buffer_size'), 'buffer_size', min_val=0) | ||||||||||
min_size = assert_int(self.definition['layer'].get('min_buffer_size'), 'min_buffer_size', min_val=0) | ||||||||||
if size is None and min_size is None: | ||||||||||
raise ValueError(f'Layer "{self.id}" is missing an integer buffer_size and/or min_buffer_size') | ||||||||||
elif size is not None and min_size is not None: | ||||||||||
if size < min_size: | ||||||||||
raise ValueError(f'Layer "{self.id}" has buffer_size less than min_buffer_size') | ||||||||||
elif size is None: | ||||||||||
# size is not set, use min_size as default | ||||||||||
size = min_size | ||||||||||
else: | ||||||||||
# size is set, min_size is not set | ||||||||||
min_size = 0 | ||||||||||
|
||||||||||
if self.tileset: | ||||||||||
val = assert_int(self.tileset.overrides.get('buffer_size'), 'buffer_size global override', min_val=0) | ||||||||||
if val is not None: | ||||||||||
size = val | ||||||||||
if self.overrides: | ||||||||||
val = assert_int(self.overrides.get('buffer_size'), 'buffer_size layer override', min_val=0) | ||||||||||
min_val = assert_int(self.overrides.get('min_buffer_size'), 'min_buffer_size layer override', min_val=0) | ||||||||||
if val is not None and min_val is not None and val < min_val: | ||||||||||
raise ValueError(f'Layer overrides for "{self.id}" have buffer_size less than min_buffer_size') | ||||||||||
if val is not None: | ||||||||||
size = val | ||||||||||
if min_val is not None: | ||||||||||
min_size = min_val | ||||||||||
# Allow empty env var to be the same as unset. | ||||||||||
getenv = self.tileset.getenv if self.tileset else os.getenv | ||||||||||
tbs = getenv('TILE_BUFFER_SIZE', '') | ||||||||||
val = assert_int(tbs if tbs != '' else None, 'TILE_BUFFER_SIZE env var', min_val=0) | ||||||||||
if val is not None: | ||||||||||
size = val | ||||||||||
if size < min_size: | ||||||||||
size = min_size | ||||||||||
return size | ||||||||||
|
||||||||||
@property | ||||||||||
def max_size(self) -> int: | ||||||||||
|
@@ -255,24 +312,33 @@ class Tileset: | |||||||||
filename: Path | ||||||||||
definition: dict | ||||||||||
layers: List[Layer] | ||||||||||
getenv: GetEnv | ||||||||||
|
||||||||||
@staticmethod | ||||||||||
def parse(tileset_source: Union[str, Path, ParsedData]) -> 'Tileset': | ||||||||||
return Tileset(tileset_source) | ||||||||||
|
||||||||||
def __init__(self, tileset_source: Union[str, Path, ParsedData]): | ||||||||||
def __init__(self, tileset_source: Union[str, Path, ParsedData], getenv: GetEnv = None): | ||||||||||
"""Create a new tileset from a file (str|Path), or already parsed. | ||||||||||
Optionally provide environment variables (used in unit testing).""" | ||||||||||
self.getenv = getenv or os.getenv | ||||||||||
if isinstance(tileset_source, ParsedData): | ||||||||||
self.filename = tileset_source.path | ||||||||||
self.definition = tileset_source.data | ||||||||||
data = tileset_source.data | ||||||||||
else: | ||||||||||
self.filename = Path(tileset_source).resolve() | ||||||||||
self.definition = parse_file(self.filename) | ||||||||||
data = parse_file(self.filename) | ||||||||||
|
||||||||||
self.definition = self.definition['tileset'] | ||||||||||
self.definition = data['tileset'] | ||||||||||
self.layers = [] | ||||||||||
self.layers_by_id = {} | ||||||||||
for index, layer_filename in enumerate(self.definition['layers']): | ||||||||||
layer = Layer(layer_filename, self, index) | ||||||||||
|
||||||||||
layer_obj: Union[str, Path, ParsedData, dict] | ||||||||||
for index, layer_obj in enumerate(self.definition['layers']): | ||||||||||
if isinstance(layer_obj, dict): | ||||||||||
layer = Layer(layer_obj['file'], self, index, layer_obj) | ||||||||||
else: | ||||||||||
layer = Layer(layer_obj, self, index, overrides=None) | ||||||||||
if layer.id in self.layers_by_id: | ||||||||||
raise ValueError(f"Layer '{layer.id}' is defined more than once") | ||||||||||
self.layers.append(layer) | ||||||||||
|
@@ -301,13 +367,6 @@ def __init__(self, tileset_source: Union[str, Path, ParsedData]): | |||||||||
|
||||||||||
validate_properties(self, f'Tileset {self.filename}') | ||||||||||
|
||||||||||
@deprecated(version='3.2.0', reason='use named properties instead') | ||||||||||
def __getitem__(self, attr): | ||||||||||
if attr in self.definition: | ||||||||||
return self.definition[attr] | ||||||||||
else: | ||||||||||
raise KeyError | ||||||||||
|
||||||||||
@property | ||||||||||
def attribution(self) -> str: | ||||||||||
return self.definition['attribution'] | ||||||||||
|
@@ -360,6 +419,10 @@ def minzoom(self) -> int: | |||||||||
def name(self) -> str: | ||||||||||
return self.definition['name'] | ||||||||||
|
||||||||||
@property | ||||||||||
def overrides(self) -> dict: | ||||||||||
return self.definition.get('overrides', {}) | ||||||||||
|
||||||||||
@property | ||||||||||
def pixel_scale(self) -> int: | ||||||||||
return self.definition['pixel_scale'] | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Python Unit Tests | ||
|
||
These tests can be quickly ran using `make bash` (from the root of the tools project): | ||
|
||
```bash | ||
host$ make bash | ||
|
||
openmaptiles@...:/tileset$ python -m unittest | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that
min_buffer_size
is also used with per-layer override.