diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5927295..c24faf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: strategy: matrix: python-version: - - '3.7' - '3.8' - '3.9' - '3.10' @@ -33,7 +32,7 @@ jobs: - name: Install dependencies run: | poetry env use "${{ matrix.python-version }}" - poetry install + poetry install --all-extras - name: Run tox targets for ${{ matrix.python-version }} run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0687d6b..546074b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,9 +19,9 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade - args: [ --py37-plus ] + args: [ --py38-plus ] - repo: https://github.com/PyCQA/isort - rev: v5.11.3 + rev: 5.11.4 hooks: - id: isort - repo: https://github.com/psf/black diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b30566..42034ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ Version 2.0.0 * Delete the `round_fn` argument as Python 2 has been dropped * Use `pyproject.toml` and Poetry to replace the `setup.py` file * Use `geom_type` property instead of deprecated `type` +* Add the possibility to give a coordinates transformer +* Add a `geojson` option. See [#107](https://github.com/tilezen/mapbox-vector-tile/issues/107) +* Refactor the options using the `per_layer_options` and `default_options` dictionaries. +* Add the option `max_geometry_validate_tries`. + Version 1.2.1 ------------- diff --git a/README.md b/README.md index ff4f759..a4d3505 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,19 @@ Mapbox Vector Tile Installation ------------ -mapbox-vector-tile is compatible with Python 3.7 or newer. It is listed on PyPi as `mapbox-vector-tile`. The +mapbox-vector-tile is compatible with Python 3.8 or newer. It is listed on PyPi as `mapbox-vector-tile`. The recommended way to install is via `pip`: ```shell pip install mapbox-vector-tile ``` -Note that `mapbox-vector-tile` depends on [Shapely](https://pypi.python.org/pypi/Shapely), a Python library for computational geometry which requires a library called [GEOS](https://trac.osgeo.org/geos/). Please see [Shapely's instructions](https://pypi.python.org/pypi/Shapely#installing-shapely) for information on how to install its prerequisites. +An extra dependency has been defined to install [`pyproj`](https://pyproj4.github.io/pyproj/stable/). This is useful +when changing the Coordinate Reference System when encoding or decoding tiles. + +```shell +pip install mapbox-vector-tile[proj] +``` Encoding -------- @@ -29,6 +34,12 @@ following keys * `geometry`: representation of the feature geometry in WKT, WKB, or a shapely geometry. Coordinates are relative to the tile, scaled in the range `[0, 4096)`. See below for example code to perform the necessary transformation. *Note* that `GeometryCollection` types are not supported, and will trigger a `ValueError`. * `properties`: a dictionary with a few keys and their corresponding values. +The encoding operation accepts options which can be defined per layer using the `per_layer_options` argument. If +there is missing layer or missing options values in the `per_layer_options`, the options of `default_options` are +taken into account. Finally, global default values are used. See the docstring of the `encode` method for more +details about the available options and their global default values. + + ```python >>> import mapbox_vector_tile @@ -63,7 +74,7 @@ following keys } ]) - '\x1aH\n\x05water\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aD\n\x03air\x12\x15\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\t\t\xbe\x02\xb6\x03\n\x81\x1b\x00\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x06\n\x04flew(\x80 x\x02' + b'\x1aH\n\x05water\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03uid\x1a\x03foo\x1a\x03cat"\x02 {"\x05\n\x03bar"\x06\n\x04flew(\x80 x\x02\x1aD\n\x03air\x12\x15\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\t\t\xbe\x02\xb6\x03\n\x81\x1b\x00\x1a\x03uid\x1a\x03foo\x1a\x03cat"\x03 \xd2\t"\x05\n\x03bar"\x06\n\x04flew(\x80 x\x02' # Using WKB @@ -72,7 +83,7 @@ following keys "name": "water", "features": [ { - "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", + "geometry":b"\x01\x03\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", "properties":{ "uid":123, "foo":"bar", @@ -85,7 +96,7 @@ following keys "name": "air", "features": [ { - "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", + "geometry":b"\x01\x03\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", "properties":{ "uid":1234, "foo":"bar", @@ -96,7 +107,7 @@ following keys } ]) - '\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02' + b'\x1aH\n\x05water\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03uid\x1a\x03foo\x1a\x03cat"\x02 {"\x05\n\x03bar"\x06\n\x04flew(\x80 x\x02\x1aG\n\x03air\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03uid\x1a\x03foo\x1a\x03cat"\x03 \xd2\t"\x05\n\x03bar"\x06\n\x04flew(\x80 x\x02' ``` ### Coordinate transformations for encoding @@ -169,6 +180,56 @@ Note that this example may not have anything visible within the tile when render Also note that the spec allows the extents to be modified, even though they are often set to 4096 by convention. `mapbox-vector-tile` assumes an extent of 4096. +```python + import mapbox_vector_tile + from pyproj import Transformer + from shapely.geometry import LineString + + SRID_LNGLAT = 4326 + SRID_SPHERICAL_MERCATOR = 3857 + direct_transformer = Transformer.from_crs(crs_from=SRID_LNGLAT, crs_to=SRID_SPHERICAL_MERCATOR, always_xy=True) + + lnglat_line = LineString(((-122.1, 45.1), (-122.2, 45.2))) + + # Encode the tile + tile_pbf = mapbox_vector_tile.encode({ + "name": "my-layer", + "features": [{ + "geometry": lnglat_line.wkt, + "properties": {"stuff": "things"}, + }] + }, + default_options={"transformer": direct_transformer.transform}) + + # Decode the tile + reverse_transformer = Transformer.from_crs(crs_from=SRID_SPHERICAL_MERCATOR, crs_to=SRID_LNGLAT, always_xy=True) + mapbox_vector_tile.decode(tile=tile_pbf, default_options={"transformer": reverse_transformer.transform}) + + { + "my-layer": { + "extent": 4096, + "version": 1, + "features": [ + { + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.10000156433787, 45.09999871982179], + [-122.20000202176608, 45.20000292038091] + ] + }, + "properties": { + "stuff": "things" + }, + "id": 0, + "type": "Feature" + } + ], + "type": "FeatureCollection" + } + } +``` + ### Quantization The encoder also has options to quantize the data for you via the `quantize_bounds` option. When encoding, pass in the bounds in the form (minx, miny, maxx, maxy) and the coordinates will be scaled appropriately during encoding. @@ -186,7 +247,7 @@ mapbox_vector_tile.encode([ } ] } - ], quantize_bounds=(10.0, 10.0, 20.0, 20.0)) + ], default_options={"quantize_bounds": (10.0, 10.0, 20.0, 20.0)}) ``` In this example, the coordinate that would get encoded would be (2048, 2048) @@ -211,7 +272,7 @@ mapbox_vector_tile.encode([ } ] } - ], quantize_bounds=(0.0, 0.0, 10.0, 10.0), extents=50) + ], default_options={"quantize_bounds": (0.0, 0.0, 10.0, 10.0), "extents":50}) ``` Decoding @@ -237,44 +298,60 @@ Decode method takes in a valid google.protobuf.message Tile and returns decoded } ``` +The decoding operation accepts options which can be defined per layer using the `per_layer_options` argument. If +there is missing layer or missing options values in the `per_layer_options`, the options of `default_options` are +taken into account. Finally, global default values are used. See the docstring of the `decode` method for more +details about the available options and their global default values. + ```python >>> import mapbox_vector_tile - >>> mapbox_vector_tile.decode('\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02') + >>> mapbox_vector_tile.decode(b'\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02') { - 'water': { - 'extent': 4096, - 'version': 2, - 'features': [{ - 'geometry': {'type': 'Polygon', 'coordinates': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]}, - 'properties': { - 'foo': 'bar', - 'uid': 123, - 'cat': 'flew' - }, - 'type': 3, - 'id': 1 - } - ] - }, - 'air': { - 'extent': 4096, - 'version': 2, - 'features': [{ - 'geometry': {'type': 'Polygon', 'coordinates': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]}, - 'properties': { - 'foo': 'bar', - 'uid': 1234, - 'balls': 'foo', - 'cat': 'flew' - }, - 'type': 3, - 'id': 1 - } - ] - } + "water": { + "extent": 4096, + "version": 2, + "features": [ + { + "geometry": { + "type": "Polygon", + "coordinates": [[[0,0],[0,1],[1,1],[1,0],[0,0]]] + }, + "properties": { + "foo": "bar", + "uid": 123, + "cat": "flew" + }, + "id": 1, + "type": "Feature" + } + ], + "type": "FeatureCollection" + }, + "air": { + "extent": 4096, + "version": 2, + "features": [ + { + "geometry": { + "type": "Polygon", + "coordinates": [[[0,0],[0,1],[1,1],[1,0],[0,0]]] + }, + "properties": { + "foo": "bar", + "uid": 1234, + "balls": "foo", + "cat": "flew" + }, + "id": 1, + "type": "Feature" + } + ], + "type": "FeatureCollection" + } } + ``` Here's how you might decode a tile from a file. @@ -288,6 +365,10 @@ Here's how you might decode a tile from a file. >>> f.write(repr(decoded_data)) ``` +The `decode` function has a `geojson` option which enforces a GeoJson RFC7946 compatible result. Its default value +is `True`. To enforce the behaviour of versions <2.0.0, please use `geojson=False`. + + Use native protobuf library for performance ------------------------------------------ diff --git a/bench/bench_encode.py b/bench/bench_encode.py index 26be632..4761003 100755 --- a/bench/bench_encode.py +++ b/bench/bench_encode.py @@ -41,7 +41,7 @@ def run_test(layers): for layer in layers: layer_description = {"features": layer, "name": "bar"} profiler.enable() - encode(layer_description, on_invalid_geometry=on_invalid_geometry_ignore) + encode(layer_description, default_options={"on_invalid_geometry": on_invalid_geometry_ignore}) profiler.disable() if i % 100 == 0: print(f"{i} tiles produced") diff --git a/mapbox_vector_tile/__init__.py b/mapbox_vector_tile/__init__.py index b588f0c..16caad1 100644 --- a/mapbox_vector_tile/__init__.py +++ b/mapbox_vector_tile/__init__.py @@ -1,25 +1,90 @@ from mapbox_vector_tile import decoder, encoder -def decode(tile, y_coord_down=False): - vector_tile = decoder.TileData() - message = vector_tile.get_message(tile, y_coord_down) +def decode(tile, per_layer_options=None, default_options=None): + """Decode the provided `tile` + + Args: + tile: + The tile to decode. + + per_layer_options: + An optional dictionary containing per layer options. The keys are the layer names and the values are + options as described further. If an option is missing for a layer or if a layer is missing, the values of + `default_options` are taken first. Finally, the global default options are used. + + default_options: + These options are taken for layers without entry in `per_layer_options`. For all missing options values, + the global default values are taken. + + Returns: + The decoded layers data. + + Notes: + The possible options are: + * `y_coord_down`: it suppresses flipping the y coordinate values during encoding when set to `True`. + Default to `False`. + * `transformer`: a function transforming the coordinates of geometry object. It takes two floats (`x` + and `y`) as arguments and retrieves the transformed coordinates `x_transformed`, `y_transformed`. Default to + `None`. + * `geojson`: when set to `False`, the behaviour of mapbox-vector-tile version 1.* is used. When set + to `False`, the retrieved dictionary is a valid geojson file. Default to `True`. + """ + vector_tile = decoder.TileData(pbf_data=tile, per_layer_options=per_layer_options, default_options=default_options) + message = vector_tile.get_message() return message -def encode( - layers, - quantize_bounds=None, - y_coord_down=False, - extents=4096, - on_invalid_geometry=None, - check_winding_order=True, -): - vector_tile = encoder.VectorTile(extents, on_invalid_geometry, check_winding_order=check_winding_order) +def encode(layers, per_layer_options=None, default_options=None): + """Encode the `layers` into a MVT tile. + + Args: + layers: + The layer data to encode. + + per_layer_options: + An optional dictionary containing per layer options. The keys are the layer names and the values are + options as described further. If an option is missing for a layer or if a layer is missing, the values of + `default_options` are taken first. Finally, the global default options are used. + + default_options: + These options are taken for layers without entry in `per_layer_options`. For all missing options values, + the global default values are taken. + + Returns: + The encoded tile. + + Notes: + The possible options are: + * `y_coord_down`: it suppresses flipping the y coordinate values during encoding when set to `True`. + Default to `False`. + * `transformer`: a function transforming the coordinates of geometry object. It takes two floats (`x` + and `y`) as arguments and retrieves the transformed coordinates `x_transformed`, `y_transformed`. Default to + `None`. + * `quantize_bounds`: bounds in the form (minx, miny, maxx, maxy) used to scale coordinates during + encoding. Default to `None`. + * `extents`: extents of the tile which is passed through to the layer in the pbf, and honored during any + quantization or y coordinate flipping. Default to 4096. + * `on_invalid_geometry`: a function taking a shapely shape as argument and retrieving an optional + valid geometry. Default to None. In the file `encoder.py`, three possible functions are defined: + * `on_invalid_geometry_raise`: it raises an error if an invalid geometry exists. + * `on_invalid_geometry_ignore`: it ignores the invalid geometry and replaces it with a `None`. + * `on_invalid_geometry_make_valid`: it tries to make the geometry valid. If it fails, retrieves `None`. + * `check_winding_order`: it forces the check of the winding order for polygons. Default to True. + * `max_geometry_validate_tries`: the number of tries when trying to enforce the good winding order. Default + to 5. + """ + vector_tile = encoder.VectorTile(default_options=default_options) + if per_layer_options is None: + per_layer_options = dict() if isinstance(layers, list): for layer in layers: - vector_tile.add_features(layer["features"], layer["name"], quantize_bounds, y_coord_down) + layer_name = layer["name"] + layer_options = per_layer_options.get(layer_name, None) + vector_tile.add_layer(features=layer["features"], name=layer_name, options=layer_options) else: - vector_tile.add_features(layers["features"], layers["name"], quantize_bounds, y_coord_down) + layer_name = layers["name"] + layer_options = per_layer_options.get(layer_name, None) + vector_tile.add_layer(features=layers["features"], name=layer_name, options=layer_options) return vector_tile.tile.SerializeToString() diff --git a/mapbox_vector_tile/decoder.py b/mapbox_vector_tile/decoder.py index c6ae238..3c66198 100644 --- a/mapbox_vector_tile/decoder.py +++ b/mapbox_vector_tile/decoder.py @@ -4,6 +4,7 @@ CMD_LINE_TO, CMD_MOVE_TO, CMD_SEG_END, + get_decode_options, LINESTRING, POINT, POLYGON, @@ -12,14 +13,19 @@ class TileData: - def __init__(self): + def __init__(self, pbf_data, per_layer_options=None, default_options=None): self.tile = vector_tile.tile() - - def get_message(self, pbf_data, y_coord_down=False): self.tile.ParseFromString(pbf_data) + self.default_options = default_options + self.per_layer_options = per_layer_options if per_layer_options is not None else dict() + def get_message(self): tile = {} for layer in self.tile.layers: + layer_name = layer.name + layer_options = self.per_layer_options.get(layer_name, None) + layer_options = get_decode_options(layer_options=layer_options, default_options=self.default_options) + keys = layer.keys vals = layer.values @@ -34,11 +40,24 @@ def get_message(self, pbf_data, y_coord_down=False): value = self.parse_value(val) props[key] = value - geometry = self.parse_geometry(feature.geometry, feature.type, layer.extent, y_coord_down) - new_feature = {"geometry": geometry, "properties": props, "id": feature.id, "type": feature.type} + geometry = self.parse_geometry( + geom=feature.geometry, + ftype=feature.type, + extent=layer.extent, + y_coord_down=layer_options["y_coord_down"], + transformer=layer_options["transformer"], + ) + if layer_options["geojson"]: + new_feature = {"geometry": geometry, "properties": props, "id": feature.id, "type": "Feature"} + else: + new_feature = {"geometry": geometry, "properties": props, "id": feature.id, "type": feature.type} features.append(new_feature) - tile[layer.name] = {"extent": layer.extent, "version": layer.version, "features": features} + tile_data = {"extent": layer.extent, "version": layer.version, "features": features} + if layer_options["geojson"]: + tile_data["type"] = "FeatureCollection" + + tile[layer_name] = tile_data return tile @staticmethod @@ -70,8 +89,7 @@ def _ensure_polygon_closed(coords): if coords and coords[0] != coords[-1]: coords.append(coords[0]) - @classmethod - def parse_geometry(cls, geom, ftype, extent, y_coord_down): # noqa:C901 + def parse_geometry(self, geom, ftype, extent, y_coord_down, transformer): # noqa:C901 # [9 0 8192 26 0 10 2 0 0 2 15] i = 0 coords = [] @@ -82,19 +100,18 @@ def parse_geometry(cls, geom, ftype, extent, y_coord_down): # noqa:C901 while i != len(geom): item = bin(geom[i]) ilen = len(item) - cmd = int(cls.zero_pad(item[(ilen - CMD_BITS) : ilen]), 2) - cmd_len = int(cls.zero_pad(item[: ilen - CMD_BITS]), 2) + cmd = int(self.zero_pad(item[(ilen - CMD_BITS) : ilen]), 2) + cmd_len = int(self.zero_pad(item[: ilen - CMD_BITS]), 2) i = i + 1 if cmd == CMD_SEG_END: if ftype == POLYGON: - cls._ensure_polygon_closed(coords) + self._ensure_polygon_closed(coords) parts.append(coords) coords = [] - elif cmd == CMD_MOVE_TO or cmd == CMD_LINE_TO: - + elif cmd in (CMD_MOVE_TO, CMD_LINE_TO): if coords and cmd == CMD_MOVE_TO: if ftype in (LINESTRING, POLYGON): # multi line string or polygon our encoder includes CMD_SEG_END to denote the end of a @@ -103,7 +120,7 @@ def parse_geometry(cls, geom, ftype, extent, y_coord_down): # noqa:C901 # for polygons, we want to ensure that it is closed if ftype == POLYGON: - cls._ensure_polygon_closed(coords) + self._ensure_polygon_closed(coords) parts.append(coords) coords = [] @@ -127,7 +144,10 @@ def parse_geometry(cls, geom, ftype, extent, y_coord_down): # noqa:C901 if not y_coord_down: y = extent - y - coords.append([x, y]) + if transformer is None: + coords.append([x, y]) + else: + coords.append([*transformer(x, y)]) if ftype == POINT: if len(coords) == 1: @@ -153,7 +173,7 @@ def parse_geometry(cls, geom, ftype, extent, y_coord_down): # noqa:C901 winding = 0 for ring in parts: - a = cls._area_sign(ring) + a = self._area_sign(ring) if a == 0: continue if winding == 0: diff --git a/mapbox_vector_tile/encoder.py b/mapbox_vector_tile/encoder.py index 157105d..07458a9 100644 --- a/mapbox_vector_tile/encoder.py +++ b/mapbox_vector_tile/encoder.py @@ -11,6 +11,7 @@ from mapbox_vector_tile.Mapbox import vector_tile_pb2 as vector_tile from mapbox_vector_tile.geom_encoder import GeometryEncoder from mapbox_vector_tile.polygon import make_it_valid +from mapbox_vector_tile.utils import get_encode_options def on_invalid_geometry_raise(shape): @@ -26,17 +27,12 @@ def on_invalid_geometry_make_valid(shape): class VectorTile: - def __init__(self, extents, on_invalid_geometry=None, max_geometry_validate_tries=5, check_winding_order=True): - if extents <= 0: - raise ValueError(f"The extents must be positive. {extents} provided.") - + def __init__(self, default_options=None): self.tile = vector_tile.tile() - self.extents = extents - self.on_invalid_geometry = on_invalid_geometry - self.check_winding_order = check_winding_order - self.max_geometry_validate_tries = max_geometry_validate_tries + self.default_options = default_options self.layer = None + self.layer_options = None self.key_idx = 0 self.val_idx = 0 self.seen_keys_idx = {} @@ -44,16 +40,17 @@ def __init__(self, extents, on_invalid_geometry=None, max_geometry_validate_trie self.seen_values_bool_idx = {} self.seen_layer_names = set() - def add_features(self, features, layer_name, quantize_bounds=None, y_coord_down=False): - if not layer_name: - raise ValueError(f"A layer name can not be empty. {layer_name!r} was provided.") - if layer_name in self.seen_layer_names: - raise ValueError(f"The layer name {layer_name!r} already exists in the vector tile.") - self.seen_layer_names.add(layer_name) + def add_layer(self, name, features, options=None): + if not name: + raise ValueError(f"A layer name can not be empty. {name!r} was provided.") + if name in self.seen_layer_names: + raise ValueError(f"The layer name {name!r} already exists in the vector tile.") + self.seen_layer_names.add(name) self.layer = self.tile.layers.add() - self.layer.name = layer_name + self.layer_options = get_encode_options(layer_options=options, default_options=self.default_options) + self.layer.name = name self.layer.version = 2 - self.layer.extent = self.extents + self.layer.extent = self.layer_options["extents"] self.key_idx = 0 self.val_idx = 0 @@ -74,68 +71,69 @@ def add_features(self, features, layer_name, quantize_bounds=None, y_coord_down= if shape.is_empty: continue - if quantize_bounds: - shape = self.quantize(shape, quantize_bounds) - if self.check_winding_order: - shape = self.enforce_winding_order(shape, y_coord_down) + if self.layer_options["quantize_bounds"]: + shape = self.quantize(shape) + if self.layer_options["check_winding_order"]: + shape = self.enforce_winding_order(shape) if shape is not None and not shape.is_empty: - self.add_feature(feature, shape, y_coord_down) + self.add_feature(feature, shape) - def enforce_winding_order(self, shape, y_coord_down, n_try=1): + def enforce_winding_order(self, shape, n_try=1): if shape.geom_type == "MultiPolygon": # If we are a multipolygon, we need to ensure that the winding orders of the constituent polygons are # correct. In particular, the winding order of the interior rings need to be the opposite of the exterior # ones, and all interior rings need to follow the exterior one. This is how the end of one polygon and # the beginning of another are signaled. - shape = self.enforce_multipolygon_winding_order(shape, y_coord_down, n_try) + shape = self.enforce_multipolygon_winding_order(shape=shape, n_try=n_try) elif shape.geom_type == "Polygon": # Ensure that polygons are also oriented with the appropriate winding order. Their exterior rings must # have a clockwise order, which is translated into a clockwise order in MVT's tile-local coordinates with # the Y axis in "screen" (i.e: +ve down) configuration. Note that while the Y axis flips, we also invert # the Y coordinate to get the tile-local value, which means the clockwise orientation is unchanged. - shape = self.enforce_polygon_winding_order(shape, y_coord_down, n_try) + shape = self.enforce_polygon_winding_order(shape=shape, n_try=n_try) # other shapes just get passed through return shape - def quantize(self, shape, bounds): - minx, miny, maxx, maxy = bounds + def quantize(self, shape): + minx, miny, maxx, maxy = self.layer_options["quantize_bounds"] + extents = self.layer_options["extents"] def fn(x, y, z=None): - xfac = self.extents / (maxx - minx) - yfac = self.extents / (maxy - miny) + xfac = extents / (maxx - minx) + yfac = extents / (maxy - miny) x = xfac * (x - minx) y = yfac * (y - miny) return round(x), round(y) return transform(fn, shape) - def handle_shape_validity(self, shape, y_coord_down, n_try): + def handle_shape_validity(self, shape, n_try): if shape.is_valid: return shape - if n_try >= self.max_geometry_validate_tries: + if n_try >= self.layer_options["max_geometry_validate_tries"]: # ensure that we don't recurse indefinitely with an invalid geometry handler that doesn't validate # geometries return None - if self.on_invalid_geometry: - shape = self.on_invalid_geometry(shape) + if self.layer_options["on_invalid_geometry"]: + shape = self.layer_options["on_invalid_geometry"](shape) if shape is not None and not shape.is_empty: # This means that we have a handler that might have altered the geometry. We'll run through the process # again, but keep track of which attempt we are on to terminate the recursion. - shape = self.enforce_winding_order(shape, y_coord_down, n_try + 1) + shape = self.enforce_winding_order(shape=shape, n_try=n_try + 1) return shape - def enforce_multipolygon_winding_order(self, shape, y_coord_down, n_try): + def enforce_multipolygon_winding_order(self, shape, n_try): assert shape.geom_type == "MultiPolygon" parts = [] for part in shape.geoms: - part = self.enforce_polygon_winding_order(part, y_coord_down, n_try) + part = self.enforce_polygon_winding_order(shape=part, n_try=n_try) if part is not None and not part.is_empty: if part.geom_type == "MultiPolygon": parts.extend(part.geoms) @@ -150,10 +148,10 @@ def enforce_multipolygon_winding_order(self, shape, y_coord_down, n_try): else: oriented_shape = MultiPolygon(parts) - oriented_shape = self.handle_shape_validity(oriented_shape, y_coord_down, n_try) + oriented_shape = self.handle_shape_validity(oriented_shape, n_try) return oriented_shape - def enforce_polygon_winding_order(self, shape, y_coord_down, n_try): + def enforce_polygon_winding_order(self, shape, n_try): assert shape.geom_type == "Polygon" def fn(point): @@ -166,33 +164,36 @@ def fn(point): if len(shape.interiors) > 0: rings = [self.apply_map(fn, ring.coords) for ring in shape.interiors] - sign = 1.0 if y_coord_down else -1.0 + sign = 1.0 if self.layer_options["y_coord_down"] else -1.0 oriented_shape = orient(Polygon(exterior, rings), sign=sign) - oriented_shape = self.handle_shape_validity(oriented_shape, y_coord_down, n_try) + oriented_shape = self.handle_shape_validity(oriented_shape, n_try) return oriented_shape @staticmethod def apply_map(fn, x): return list(map(fn, x)) - @staticmethod - def _load_geometry(geometry_spec): + def _load_geometry(self, geometry_spec): if isinstance(geometry_spec, BaseGeometry): - return geometry_spec - - if isinstance(geometry_spec, dict): - return shapely_shape(geometry_spec) - - try: - return load_wkb(geometry_spec) - except Exception: + geom = geometry_spec + elif isinstance(geometry_spec, dict): + geom = shapely_shape(geometry_spec) + else: try: - return load_wkt(geometry_spec) + geom = load_wkb(geometry_spec) except Exception: - return None + try: + geom = load_wkt(geometry_spec) + except Exception: + return None + + if self.layer_options["transformer"] is None: + return geom + else: + return transform(self.layer_options["transformer"], geom) - def add_feature(self, feature, shape, y_coord_down): - geom_encoder = GeometryEncoder(y_coord_down, self.extents) + def add_feature(self, feature, shape): + geom_encoder = GeometryEncoder(self.layer_options["y_coord_down"], self.layer_options["extents"]) geometry = geom_encoder.encode(shape) feature_type = self._get_feature_type(shape) diff --git a/mapbox_vector_tile/optimise.py b/mapbox_vector_tile/optimise.py index 05753d1..919def7 100644 --- a/mapbox_vector_tile/optimise.py +++ b/mapbox_vector_tile/optimise.py @@ -1,6 +1,6 @@ from collections import namedtuple -from mapbox_vector_tile.Mapbox.vector_tile_pb2 import tile +from mapbox_vector_tile.Mapbox import vector_tile_pb2 as vector_tile from mapbox_vector_tile.utils import CMD_LINE_TO, CMD_MOVE_TO, zig_zag_decode, zig_zag_encode @@ -208,7 +208,7 @@ def optimise_tile(tile_bytes): multilinestrings to save a few bytes. """ - t = tile() + t = vector_tile.tile() t.ParseFromString(tile_bytes) for layer in t.layers: diff --git a/mapbox_vector_tile/utils.py b/mapbox_vector_tile/utils.py index a648325..15f9fc2 100644 --- a/mapbox_vector_tile/utils.py +++ b/mapbox_vector_tile/utils.py @@ -1,3 +1,6 @@ +# +# Geometry manipulation +# # Command size CMD_BITS = 3 @@ -27,3 +30,114 @@ def zig_zag_decode(n): """Return the signed integer corresponding to the "zig-zag" encoded unsigned input integer. This encoding is used for MVT geometry deltas.""" return (n >> 1) ^ (-(n & 1)) + + +# +# Options management +# + +DEFAULT_ENCODE_OPTIONS = { + "y_coord_down": False, + "transformer": None, + "quantize_bounds": None, + "extents": 4096, + "on_invalid_geometry": None, + "check_winding_order": True, + "max_geometry_validate_tries": 5, +} + +DEFAULT_DECODE_OPTIONS = {"y_coord_down": False, "transformer": None, "geojson": True} + + +def _get_options(layer_options, default_options, global_default_options, operation_name): + """Get the entire options dictionary filled using: first, the provided `layer_options`, then the provided + `default_options` and finally filled using the provided `global_default_options`. + + Args: + layer_options: + The options for the current layer. + + default_options: + The default options of the operation. + + global_default_options: + The global default options for the operation. + + operation_name: + The name of the current operation. + + Returns: + The options to use to operate the layer. + """ + if default_options is None: + default_options = global_default_options + + if layer_options is None: + layer_options = default_options + + result = global_default_options.copy() + result.update(default_options) + result.update(layer_options) + + result_keys = set(result.keys()) + expected_keys = set(global_default_options.keys()) + extra_keys = result_keys.difference(expected_keys) + if extra_keys: + extra_keys_msg = ", ".join(f"{str(x)!r}" for x in sorted(extra_keys)) + raise ValueError(f"The following options are not allowed for {operation_name} a tile: {extra_keys_msg}.") + + return result + + +def get_encode_options(layer_options, default_options): + """Get the entire encoding options dictionary filled using: first, the provided `layer_options`, then the provided + `default_options` and finally filled using the global default options + + Args: + layer_options: + The options for the current layer. + + default_options: + The default options of the encoding operation. + + Returns: + The options to use for encoding the layer. + """ + result = _get_options( + layer_options=layer_options, + default_options=default_options, + global_default_options=DEFAULT_ENCODE_OPTIONS, + operation_name="encoding", + ) + + # Checks on final values + extents = result["extents"] + max_geometry_validate_tries = result["max_geometry_validate_tries"] + if extents <= 0: + raise ValueError(f"The extents must be positive. {extents} provided.") + if max_geometry_validate_tries <= 0: + raise ValueError(f"The max_geometry_validate_tries must be positive. {max_geometry_validate_tries} provided.") + + return result + + +def get_decode_options(layer_options, default_options): + """Get the entire decoding options dictionary filled using: first, the provided `layer_options`, then the provided + `default_options` and finally filled using the global default options + + Args: + layer_options: + The options for the current layer. + + default_options: + The default options of the decoding operation. + + Returns: + The options to use for decoding the layer. + """ + return _get_options( + layer_options=layer_options, + default_options=default_options, + global_default_options=DEFAULT_DECODE_OPTIONS, + operation_name="decoding", + ) diff --git a/poetry.lock b/poetry.lock index 2cbf4b6..4e301c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,11 +1,19 @@ [[package]] name = "cachetools" -version = "5.2.0" +version = "5.2.1" description = "Extensible memoizing collections and decorators" category = "dev" optional = false python-versions = "~=3.7" +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = true +python-versions = ">=3.6" + [[package]] name = "chardet" version = "5.1.0" @@ -24,7 +32,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 [[package]] name = "coverage" -version = "7.0.0" +version = "7.0.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -46,36 +54,19 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.8.2" +version = "3.9.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "importlib-metadata" -version = "5.2.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "numpy" -version = "1.24.0" +version = "1.24.1" description = "Fundamental package for array computing in Python" category = "main" optional = false @@ -83,7 +74,7 @@ python-versions = ">=3.8" [[package]] name = "packaging" -version = "22.0" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false @@ -91,15 +82,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.6.0" +version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] -test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -109,9 +100,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -132,9 +120,20 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pyproj" +version = "3.4.1" +description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" +category = "main" +optional = true +python-versions = ">=3.8" + +[package.dependencies] +certifi = "*" + [[package]] name = "pyproject-api" -version = "1.2.1" +version = "1.4.0" description = "API to interact with the python pyproject.toml based projects" category = "dev" optional = false @@ -148,19 +147,6 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=5.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17)", "wheel (>=0.38.4)"] -[[package]] -name = "shapely" -version = "1.8.5.post1" -description = "Geometric objects, predicates, and operations" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -all = ["numpy", "pytest", "pytest-cov"] -test = ["pytest", "pytest-cov"] -vectorized = ["numpy"] - [[package]] name = "shapely" version = "2.0.0" @@ -186,7 +172,7 @@ python-versions = ">=3.7" [[package]] name = "tox" -version = "4.0.16" +version = "4.2.6" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -196,27 +182,17 @@ python-versions = ">=3.7" cachetools = ">=5.2" chardet = ">=5.1" colorama = ">=0.4.6" -filelock = ">=3.8.2" -importlib-metadata = {version = ">=5.2", markers = "python_version < \"3.8\""} +filelock = ">=3.9" packaging = ">=22" -platformdirs = ">=2.6" +platformdirs = ">=2.6.2" pluggy = ">=1" pyproject-api = ">=1.2.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} virtualenv = ">=20.17.1" [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-argparse-cli (>=1.10)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.8)"] -testing = ["build[virtualenv] (>=0.9)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.3)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.11.1)", "psutil (>=5.9.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.8.2)"] - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" -optional = false -python-versions = ">=3.7" +docs = ["furo (>=2022.12.7)", "sphinx (>=6)", "sphinx-argparse-cli (>=1.10)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +testing = ["build[virtualenv] (>=0.9)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.3)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.12)", "psutil (>=5.9.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.8.2)"] [[package]] name = "virtualenv" @@ -229,34 +205,28 @@ python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] -[[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[extras] +proj = ["pyproj"] [metadata] lock-version = "1.1" -python-versions = "^3.7" -content-hash = "d63f31b25babba2f0e66b97b3991973b1eb6230066d889daa3f541165bf3e322" +python-versions = "^3.8" +content-hash = "3f5209bbdf1f29bebd1d306fe04d4297e1f6d455997a653543db5c9710609bbe" [metadata.files] cachetools = [ - {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, - {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, + {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, + {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, +] +certifi = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] chardet = [ {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, @@ -267,107 +237,103 @@ colorama = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ - {file = "coverage-7.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2569682d6ea9628da8d6ba38579a48b1e53081226ec7a6c82b5024b3ce5009f"}, - {file = "coverage-7.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ec256a592b497f26054195f7d7148892aca8c4cdcc064a7cc66ef7a0455b811"}, - {file = "coverage-7.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5885a4ceb6dde34271bb0adafa4a248a7f589c89821e9da3110c39f92f41e21b"}, - {file = "coverage-7.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d43d406a4d73aa7f855fa44fa77ff47e739b565b2af3844600cdc016d01e46b9"}, - {file = "coverage-7.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18df11efa615b79b9ecc13035a712957ff6283f7b244e57684e1c092869f541"}, - {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f6a4bf5bdee93f6817797beba7086292c2ebde6df0d5822e0c33f8b05415c339"}, - {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:33efe89cd0efef016db19d8d05aa46631f76793de90a61b6717acb202b36fe60"}, - {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96b5b1f1079e48f56bfccf103bcf44d48b9eb5163f1ea523fad580f15d3fe5e0"}, - {file = "coverage-7.0.0-cp310-cp310-win32.whl", hash = "sha256:fb85b7a7a4b204bd59d6d0b0c8d87d9ffa820da225e691dfaffc3137dc05b5f6"}, - {file = "coverage-7.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:793dcd9d42035746fc7637df4336f7581df19d33c5c5253cf988c99d8e93a8ba"}, - {file = "coverage-7.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d564142a03d3bc8913499a458e931b52ddfe952f69b6cd4b24d810fd2959044a"}, - {file = "coverage-7.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0a8b0e86bede874bf5da566b02194fbb12dd14ce3585cabd58452007f272ba81"}, - {file = "coverage-7.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e645c73cbfc4577d93747d3f793115acf6f907a7eb9208fa807fdcf2da1964a4"}, - {file = "coverage-7.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de06e7585abe88c6d38c1b73ce4c3cb4c1a79fbb0da0d0f8e8689ef5729ec60d"}, - {file = "coverage-7.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a30b646fbdd5bc52f506e149fa4fbdef82432baf6b81774e61ec4e3b43b9cbde"}, - {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:db8141856dc9be0917413df7200f53accf1d84c8b156868e6af058a1ea8e903a"}, - {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:59e71912c7fc78d08a567ee65656123878f49ca1b5672e660ea70bf8dfbebf8f"}, - {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b8f7cd942dda3795fc9eadf303cc53a422ac057e3b70c2ad6d4276ec6a83a541"}, - {file = "coverage-7.0.0-cp311-cp311-win32.whl", hash = "sha256:bf437a04b9790d3c9cd5b48e9ce9aa84229040e3ae7d6c670a55118906113c5a"}, - {file = "coverage-7.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7e1bb36b4e57a2d304322021b35d4e4a25fa0d501ba56e8e51efaebf4480556"}, - {file = "coverage-7.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:215f40ef86f1958a1151fa7fad2b4f2f99534c4e10a34a1e065eba3f19ef8868"}, - {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae088eb1cbdad8206931b1bf3f11dee644e038a9300be84d3e705e29356e5b1d"}, - {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9071e197faa24837b967bc9aa0b9ef961f805a75f1ee3ea1f3367f55cd46c3c"}, - {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f1e6d9c70d45a960d3f3d781ea62b167fdf2e0e1f6bb282b96feea653adb923"}, - {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9fadd15f9fcfd7b16d9cccce9f5e6ec6f9b8df860633ad9aa62c2b14c259560f"}, - {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:10b6246cae61896ab4c7568e498e492cbb73a2dfa4c3af79141c43cf806f929a"}, - {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a8785791c2120af114ea7a06137f7778632e568a5aa2bbfc3b46c573b702af74"}, - {file = "coverage-7.0.0-cp37-cp37m-win32.whl", hash = "sha256:30220518dd89c4878908d73f5f3d1269f86e9e045354436534587a18c7b9da85"}, - {file = "coverage-7.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bc904aa96105d73357de03de76336b1e3db28e2b12067d36625fd9646ab043fd"}, - {file = "coverage-7.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2331b7bd84a1be79bd17ca8e103ce38db8cbf7cb354dc56e651ba489cf849212"}, - {file = "coverage-7.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e907db8bdd0ad1253a33c20fdc5f0f6209d271114a9c6f1fcdf96617343f7ca0"}, - {file = "coverage-7.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0deee68e0dae1d6e3fe6943c76d7e66fbeb6519bd08e4e5366bcc28a8a9aca"}, - {file = "coverage-7.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fff0f08bc5ffd0d78db821971472b4adc2ee876b86f743e46d634fb8e3c22f"}, - {file = "coverage-7.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a290b7921c1c05787b953e5854d394e887df40696f21381cc33c4e2179bf50ac"}, - {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:100546219af59d2ad82d4575de03a303eb27b75ea36ffbd1677371924d50bcbc"}, - {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c1ba6e63b831112b9484ff5905370d89e43d4316bac76d403031f60d61597466"}, - {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c685fc17d6f4f1a3833e9dac27d0b931f7ccb52be6c30d269374203c7d0204a2"}, - {file = "coverage-7.0.0-cp38-cp38-win32.whl", hash = "sha256:8938f3a10f45019b502020ba9567b97b6ecc8c76b664b421705c5406d4f92fe8"}, - {file = "coverage-7.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:c4b63888bef2928d0eca12cbce0760cfb696acb4fe226eb55178b6a2a039328a"}, - {file = "coverage-7.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cda63459eb20652b22e038729a8f5063862c189a3963cb042a764b753172f75e"}, - {file = "coverage-7.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e06abac1a4aec1ff989131e43ca917fc7bd296f34bf0cfe86cbf74343b21566d"}, - {file = "coverage-7.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b94ad926e933976627f040f96dd1d9b0ac91f8d27e868c30a28253b9b6ac2d"}, - {file = "coverage-7.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6b4af31fb49a2ae8de1cd505fa66c403bfcc5066e845ac19d8904dcfc9d40da"}, - {file = "coverage-7.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36b62f0220459e528ad5806cc7dede71aa716e067d2cb10cb4a09686b8791fba"}, - {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:43ec1935c6d6caab4f3bc126d20bd709c0002a175d62208ebe745be37a826a41"}, - {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8593c9baf1f0f273afa22f5b45508b76adc7b8e94e17e7d98fbe1e3cd5812af2"}, - {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fee283cd36c3f14422d9c1b51da24ddbb5e1eed89ad2480f6a9f115df38b5df8"}, - {file = "coverage-7.0.0-cp39-cp39-win32.whl", hash = "sha256:97c0b001ff15b8e8882995fc07ac0a08c8baf8b13c1145f3f12e0587bbb0e335"}, - {file = "coverage-7.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8dbf83a4611c591b5de65069b6fd4dd3889200ed270cd2f7f5ac765d3842889f"}, - {file = "coverage-7.0.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:bcaf18e46668057051a312c714a4548b81f7e8fb3454116ad97be7562d2a99e4"}, - {file = "coverage-7.0.0.tar.gz", hash = "sha256:9a175da2a7320e18fc3ee1d147639a2b3a8f037e508c96aa2da160294eb50e17"}, + {file = "coverage-7.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf91db39324e9939a9db919ee4fb42a1a23634a056616dae891a030e89f87ba"}, + {file = "coverage-7.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55121fe140d7e42cb970999b93cf1c2b24484ce028b32bbd00238bb25c13e34a"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c027fbb83a8c78a6e06a0302ea1799fdb70e5cda9845a5e000545b8e2b47ea39"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf82db5b7f16b51ec32fe0bd2da0805b177c807aa8bfb478c7e6f893418c284"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ba5cc54baf3c322c4388de2a43cc95f7809366f0600e743e5aae8ea9d1038b2"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:260854160083f8275a9d9d49a05ab0ffc7a1f08f2ccccbfaec94a18aae9f407c"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ea45f0dba5a993e93b158f1a9dcfff2770e3bcabf2b80dbe7aa15dce0bcb3bf3"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6abc91f6f8b3cc0ae1034e2c03f38769fba1952ab70d0b26953aa01691265c39"}, + {file = "coverage-7.0.4-cp310-cp310-win32.whl", hash = "sha256:053cdc47cae08257051d7e934a0de4d095b60eb8a3024fa9f1b2322fa1547137"}, + {file = "coverage-7.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e9e94f2612ee549a4b3ee79cbc61bceed77e69cf38cfa05858bae939a886d16"}, + {file = "coverage-7.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5caa9dd91dcc5f054350dc57a02e053d79633907b9ccffff999568d13dcd19f8"}, + {file = "coverage-7.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:efc200fa75d9634525b40babc7a16342bd21c101db1a58ef84dc14f4bf6ac0fd"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1791e5f74c5b52f76e83fe9f4bb9571cf76d40ee0c51952ee1e4ee935b7e98b9"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d9201cfa5a98652b9cef36ab202f17fe3ea83f497b4ba2a8ed39399dfb8fcd4"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d8ef6865cb6834cab2b72fff20747a55c714b57b675f7e11c9624fe4f7cb45"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b84076e3de192fba0f95e279ac017b64c7c6ecd4f09f36f13420f5bed898a9c7"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:dcfbf8ffc046f20d75fd775a92c378f6fc7b9bded6c6f2ab88b6b9cb5805a184"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4665a714af31f160403c2e448fb2fef330719d2e04e836b08d60d612707c1041"}, + {file = "coverage-7.0.4-cp311-cp311-win32.whl", hash = "sha256:2e59aef3fba5758059208c9eff10ae7ded3629e797972746ec33b56844f69411"}, + {file = "coverage-7.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:2b854f7985b48122b6fe346631e86d67b63293f8255cb59a93d79e3d9f1574e3"}, + {file = "coverage-7.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e44b60b0b49aa85d548d392a2dca2c6a581cd4084e72e9e16bd58bd86ec20816"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2904d7a0388911c61e7e3beefe48c29dfccaba938fc1158f63190101a21e04c2"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc74b64bfa89e2f862ea45dd6ac1def371d7cc883b76680d20bdd61a6f3daa20"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06046f54e719da21c79f98ecc0962581d1aee0b3798dc6b12b1217da8bf93f4"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bc9c77004970a364a1e5454cf7cb884e4277592b959c287689b2a0fd027ef552"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0815a09b32384e8ff00a5939ec9cd10efce8742347e019c2daca1a32f5ac2aae"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a78a80d131c067d67d8a6f9bd3d3f7ea7eac82c1c7259f97d7ab73f723da9d55"}, + {file = "coverage-7.0.4-cp37-cp37m-win32.whl", hash = "sha256:2b5936b624fbe711ed02dfd86edd678822e5ee68da02b6d231e5c01090b64590"}, + {file = "coverage-7.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a63922765ee49d5b4c32afb2cd5516812c8665f3b78e64a0dd005bdfabf991b1"}, + {file = "coverage-7.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d68f2f7bddb3acdd3b36ef7f334b9d14f30b93e094f808fbbd8d288b8f9e2f9b"}, + {file = "coverage-7.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dafdba3b2b9010abab08cb8c0dc6549bfca6e1630fe14d47b01dca00d39e694"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0322354757b47640535daabd2d56384ff3cad2896248fc84d328c5fad4922d5c"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e8267466662aff93d66fa72b9591d02122dfc8a729b0a43dd70e0fb07ed9b37"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f684d88eb4924ed0630cf488fd5606e334c6835594bb5fe36b50a509b10383ed"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:70c294bb15ba576fb96b580db35895bf03749d683df044212b74e938a7f6821f"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:34c0457e1ba450ae8b22dc8ea2fd36ada1010af61291e4c96963cd9d9633366f"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b75aff2c35ceaa299691e772f7bf7c8aeab25f46acea2be3dd04cccb914a9860"}, + {file = "coverage-7.0.4-cp38-cp38-win32.whl", hash = "sha256:6c5554d55668381e131577f20e8f620d4882b04ad558f7e7f3f1f55b3124c379"}, + {file = "coverage-7.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c82f34fafaf5bc05d222fcf84423d6e156432ca35ca78672d4affd0c09c6ef6c"}, + {file = "coverage-7.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8dfb5fed540f77e814bf4ec79619c241af6b4578fa1093c5e3389bbb7beab3f"}, + {file = "coverage-7.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee32a080bab779b71c4d09a3eb5254bfca43ee88828a683dab27dfe8f582516e"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dfbee0bf0d633be3a2ab068f5a5731a70adf147d0ba17d9f9932b46c7c5782b"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32dc010713455ac0fe2fddb0e48aa43875cc7eb7b09768df10bad8ce45f9c430"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cb88a3019ad042eaa69fc7639ef077793fedbf313e89207aa82fefe92c97ebd"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73bc6114aab7753ca784f87bcd3b7613bc797aa255b5bca45e5654070ae9acfb"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92f135d370fcd7a6fb9659fa2eb716dd2ca364719cbb1756f74d90a221bca1a7"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3d485e6ec6e09857bf2115ece572d666b7c498377d4c70e66bb06c63ed177c2"}, + {file = "coverage-7.0.4-cp39-cp39-win32.whl", hash = "sha256:c58921fcd9914b56444292e7546fe183d079db99528142c809549ddeaeacd8e9"}, + {file = "coverage-7.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:f092d9f2ddaa30235d33335fbdb61eb8f3657af519ef5f9dd6bdae65272def11"}, + {file = "coverage-7.0.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:cb8cfa3bf3a9f18211279458917fef5edeb5e1fdebe2ea8b11969ec2ebe48884"}, + {file = "coverage-7.0.4.tar.gz", hash = "sha256:f6c4ad409a0caf7e2e12e203348b1a9b19c514e7d078520973147bf2d3dcbc6f"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] filelock = [ - {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, - {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, -] -importlib-metadata = [ - {file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"}, - {file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"}, + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, ] numpy = [ - {file = "numpy-1.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e73a1f4f5b74a42abb55bc2b3d869f1b38cbc8776da5f8b66bf110284f7a437"}, - {file = "numpy-1.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9387c7d6d50e8f8c31e7bfc034241e9c6f4b3eb5db8d118d6487047b922f82af"}, - {file = "numpy-1.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ad6a024a32ee61d18f5b402cd02e9c0e22c0fb9dc23751991b3a16d209d972e"}, - {file = "numpy-1.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73cf2c5b5a07450f20a0c8e04d9955491970177dce8df8d6903bf253e53268e0"}, - {file = "numpy-1.24.0-cp310-cp310-win32.whl", hash = "sha256:cec79ff3984b2d1d103183fc4a3361f5b55bbb66cb395cbf5a920a4bb1fd588d"}, - {file = "numpy-1.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:4f5e78b8b710cd7cd1a8145994cfffc6ddd5911669a437777d8cedfce6c83a98"}, - {file = "numpy-1.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4445f472b246cad6514cc09fbb5ecb7aab09ca2acc3c16f29f8dca6c468af501"}, - {file = "numpy-1.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec3e5e8172a0a6a4f3c2e7423d4a8434c41349141b04744b11a90e017a95bad5"}, - {file = "numpy-1.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9168790149f917ad8e3cf5047b353fefef753bd50b07c547da0bdf30bc15d91"}, - {file = "numpy-1.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada6c1e9608ceadaf7020e1deea508b73ace85560a16f51bef26aecb93626a72"}, - {file = "numpy-1.24.0-cp311-cp311-win32.whl", hash = "sha256:f3c4a9a9f92734a4728ddbd331e0124eabbc968a0359a506e8e74a9b0d2d419b"}, - {file = "numpy-1.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:90075ef2c6ac6397d0035bcd8b298b26e481a7035f7a3f382c047eb9c3414db0"}, - {file = "numpy-1.24.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0885d9a7666cafe5f9876c57bfee34226e2b2847bfb94c9505e18d81011e5401"}, - {file = "numpy-1.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e63d2157f9fc98cc178870db83b0e0c85acdadd598b134b00ebec9e0db57a01f"}, - {file = "numpy-1.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8960f72997e56781eb1c2ea256a70124f92a543b384f89e5fb3503a308b1d3"}, - {file = "numpy-1.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f8e0df2ecc1928ef7256f18e309c9d6229b08b5be859163f5caa59c93d53646"}, - {file = "numpy-1.24.0-cp38-cp38-win32.whl", hash = "sha256:fe44e925c68fb5e8db1334bf30ac1a1b6b963b932a19cf41d2e899cf02f36aab"}, - {file = "numpy-1.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:d7f223554aba7280e6057727333ed357b71b7da7422d02ff5e91b857888c25d1"}, - {file = "numpy-1.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ab11f6a7602cf8ea4c093e091938207de3068c5693a0520168ecf4395750f7ea"}, - {file = "numpy-1.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12bba5561d8118981f2f1ff069ecae200c05d7b6c78a5cdac0911f74bc71cbd1"}, - {file = "numpy-1.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9af91f794d2d3007d91d749ebc955302889261db514eb24caef30e03e8ec1e41"}, - {file = "numpy-1.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1ddfac6a82d4f3c8e99436c90b9c2c68c0bb14658d1684cdd00f05fab241f5"}, - {file = "numpy-1.24.0-cp39-cp39-win32.whl", hash = "sha256:ac4fe68f1a5a18136acebd4eff91aab8bed00d1ef2fdb34b5d9192297ffbbdfc"}, - {file = "numpy-1.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:667b5b1f6a352419e340f6475ef9930348ae5cb7fca15f2cc3afcb530823715e"}, - {file = "numpy-1.24.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4d01f7832fa319a36fd75ba10ea4027c9338ede875792f7bf617f4b45056fc3a"}, - {file = "numpy-1.24.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb0490f0a880700a6cc4d000384baf19c1f4df59fff158d9482d4dbbca2b239"}, - {file = "numpy-1.24.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0104d8adaa3a4cc60c2777cab5196593bf8a7f416eda133be1f3803dd0838886"}, - {file = "numpy-1.24.0.tar.gz", hash = "sha256:c4ab7c9711fe6b235e86487ca74c1b092a6dd59a3cb45b63241ea0a148501853"}, + {file = "numpy-1.24.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7"}, + {file = "numpy-1.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9"}, + {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"}, + {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398"}, + {file = "numpy-1.24.1-cp310-cp310-win32.whl", hash = "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2"}, + {file = "numpy-1.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2"}, + {file = "numpy-1.24.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8"}, + {file = "numpy-1.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032"}, + {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1"}, + {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9"}, + {file = "numpy-1.24.1-cp311-cp311-win32.whl", hash = "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36"}, + {file = "numpy-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51"}, + {file = "numpy-1.24.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407"}, + {file = "numpy-1.24.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954"}, + {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36"}, + {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7"}, + {file = "numpy-1.24.1-cp38-cp38-win32.whl", hash = "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1"}, + {file = "numpy-1.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c"}, + {file = "numpy-1.24.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6"}, + {file = "numpy-1.24.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7"}, + {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700"}, + {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf"}, + {file = "numpy-1.24.1-cp39-cp39-win32.whl", hash = "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f"}, + {file = "numpy-1.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566"}, + {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"}, ] packaging = [ - {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, - {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] platformdirs = [ - {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, - {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -429,50 +395,48 @@ pyclipper = [ {file = "pyclipper-1.3.0.post4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b43cf984448bbb74a3e2ec21716ca06437350232803d86ad590b0e4d3f45fd35"}, {file = "pyclipper-1.3.0.post4.tar.gz", hash = "sha256:b73b19d2a1b895edcacaf4acb441e13e99b9e5fd53b9c0dfd2e1326e2bf68a7a"}, ] +pyproj = [ + {file = "pyproj-3.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e463c687007861a9949909211986850cfc2e72930deda0d06449ef2e315db534"}, + {file = "pyproj-3.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f87f16b902c8b2af007295c63a435f043db9e40bd45e6f96962c7b8cd08fdb5"}, + {file = "pyproj-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c60d112d8f1621a606b7f2adb0b1582f80498e663413d2ba9f5df1c93d99f432"}, + {file = "pyproj-3.4.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f38dea459e22e86326b1c7d47718a3e10c7a27910cf5eb86ea2679b8084d0c4e"}, + {file = "pyproj-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a53acbde511a7a9e1873c7f93c68f35b8c3653467b77195fe18e847555dcb7a"}, + {file = "pyproj-3.4.1-cp310-cp310-win32.whl", hash = "sha256:0c7b32382ae22a9bf5b690d24c7b4c0fb89ba313c3a91ef1a8c54b50baf10954"}, + {file = "pyproj-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:6bdac3bc1899fcc4021be06d303b342923fb8311fe06f8d862c348a1a0e78b41"}, + {file = "pyproj-3.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd9f9c409f465834988ce0aa8c1ed496081c6957f2e5ef40ed28de04397d3c0b"}, + {file = "pyproj-3.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0406f64ff59eb3342efb102c9f31536430aa5cde5ef0bfabd5aaccb73dd8cd5a"}, + {file = "pyproj-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a98fe3e53be428e67ae6a9ee9affff92346622e0e3ea0cbc15dce939b318d395"}, + {file = "pyproj-3.4.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0189fdd7aa789542a7a623010dfff066c5849b24397f81f860ec3ee085cbf55c"}, + {file = "pyproj-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3f75b030cf811f040c90a8758a20115e8746063e4cad0d0e941a4954d1219b"}, + {file = "pyproj-3.4.1-cp311-cp311-win32.whl", hash = "sha256:ef8c30c62fe4e386e523e14e1e83bd460f745bd2c8dfd0d0c327f9460c4d3c0c"}, + {file = "pyproj-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d1e7f42da205e0534831ae9aa9cee0353ab8c1aab2c369474adbb060294d98a"}, + {file = "pyproj-3.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a5eada965e8ac24e783f2493d1d9bcd11c5c93959bd43558224dd31d9faebd1c"}, + {file = "pyproj-3.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:19f5de1a7c3b81b676d846350d4bdf2ae6af13b9a450d1881706f088ecad0e2c"}, + {file = "pyproj-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ec7d2b7f2773d877927abc72e2229ef8530c09181be0e28217742bae1bc4f5"}, + {file = "pyproj-3.4.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a30d78e619dae5cd1bb69addae2f1e5f8ee1b4a8ab4f3d954e9eaf41948db506"}, + {file = "pyproj-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32e1d12340ad93232b7ea4dc1a4f4b21fa9fa9efa4b293adad45be7af6b51ec"}, + {file = "pyproj-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ce50126dad7cd4749ab86fc4c8b54ec0898149ce6710ab5c93c76a54a4afa249"}, + {file = "pyproj-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:129234afa179c8293b010ea4f73655ff7b20b5afdf7fac170f223bcf0ed6defd"}, + {file = "pyproj-3.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:231c038c6b65395c41ae3362320f03ce8054cb54dc63556e605695e5d461a27e"}, + {file = "pyproj-3.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9d82df555cf19001bac40e1de0e40fb762dec785685b77edd6993286c01b7f7"}, + {file = "pyproj-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c0d1ac9ef5a4d2e6501a4b30136c55f1e1db049d1626cc313855c4f97d196d"}, + {file = "pyproj-3.4.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97065fe82e80f7e2740e7897a0e36e8defc0a3614927f0276b4f1d1ea1ef66fa"}, + {file = "pyproj-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd633f3b8ca6eb09135dfaf06f09e2869deb139985aab26d728e8a60c9938b9"}, + {file = "pyproj-3.4.1-cp39-cp39-win32.whl", hash = "sha256:da96319b137cfd66f0bae0e300cdc77dd17af4785b9360a9bdddb1d7176a0bbb"}, + {file = "pyproj-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:7aef19d5a0a3b2d6b17f7dc9a87af722e71139cd1eea7eb82ed062a8a4b0e272"}, + {file = "pyproj-3.4.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8078c90cea07d53e3406c7c84cbf76a2ac0ffc580c365f13801575486b9d558c"}, + {file = "pyproj-3.4.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:321b82210dc5271558573d0874b9967c5a25872a28d0168049ddabe8bfecffce"}, + {file = "pyproj-3.4.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a5425cd2a0b16f5f944d49165196eebaa60b898a08c404a644c29e6a7a04b3"}, + {file = "pyproj-3.4.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d70ca5933cddbe6f51396006fb9fc78bc2b1f9d28775922453c4b04625a7efb"}, + {file = "pyproj-3.4.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c240fe6bcb5c325b50fc967d5458d708412633f4f05fefc7fb14c14254ebf421"}, + {file = "pyproj-3.4.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef76abfee1a0676ef973470abe11e22998750f2bd944afaf76d44ad70b538c06"}, + {file = "pyproj-3.4.1.tar.gz", hash = "sha256:261eb29b1d55b1eb7f336127344d9b31284d950a9446d1e0d1c2411f7dd8e3ac"}, +] pyproject-api = [ - {file = "pyproject_api-1.2.1-py3-none-any.whl", hash = "sha256:155d5623453173b7b4e9379a3146ccef2d52335234eb2d03d6ba730e7dad179c"}, - {file = "pyproject_api-1.2.1.tar.gz", hash = "sha256:093c047d192ceadcab7afd6b501276bf2ce44adf41cb9c313234518cddd20818"}, + {file = "pyproject_api-1.4.0-py3-none-any.whl", hash = "sha256:c34226297781efdd1ba4dfb74ce21076d9a8360e2125ea31803c1a02c76b2460"}, + {file = "pyproject_api-1.4.0.tar.gz", hash = "sha256:ac85c1f82e0291dbae5a7739dbb9a990e11ee4034c9b5599ea714f07a24ecd71"}, ] shapely = [ - {file = "Shapely-1.8.5.post1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d048f93e42ba578b82758c15d8ae037d08e69d91d9872bca5a1895b118f4e2b0"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99ab0ddc05e44acabdbe657c599fdb9b2d82e86c5493bdae216c0c4018a82dee"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a2f0da0109e81e0c101a2b4cd8412f73f5f299e7b5b2deaf64cd2a100ac118"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6fe855e7d45685926b6ba00aaeb5eba5862611f7465775dacd527e081a8ced6d"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec14ceca36f67cb48b34d02d7f65a9acae15cd72b48e303531893ba4a960f3ea"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-win32.whl", hash = "sha256:21776184516a16bf82a0c3d6d6a312b3cd15a4cabafc61ee01cf2714a82e8396"}, - {file = "Shapely-1.8.5.post1-cp310-cp310-win_amd64.whl", hash = "sha256:a354199219c8d836f280b88f2c5102c81bb044ccea45bd361dc38a79f3873714"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:783bad5f48e2708a0e2f695a34ed382e4162c795cb2f0368b39528ac1d6db7ed"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a23ef3882d6aa203dd3623a3d55d698f59bfbd9f8a3bfed52c2da05a7f0f8640"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab38f7b5196ace05725e407cb8cab9ff66edb8e6f7bb36a398e8f73f52a7aaa2"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d086591f744be483b34628b391d741e46f2645fe37594319e0a673cc2c26bcf"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4728666fff8cccc65a07448cae72c75a8773fea061c3f4f139c44adc429b18c3"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-win32.whl", hash = "sha256:84010db15eb364a52b74ea8804ef92a6a930dfc1981d17a369444b6ddec66efd"}, - {file = "Shapely-1.8.5.post1-cp311-cp311-win_amd64.whl", hash = "sha256:48dcfffb9e225c0481120f4bdf622131c8c95f342b00b158cdbe220edbbe20b6"}, - {file = "Shapely-1.8.5.post1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2fd15397638df291c427a53d641d3e6fd60458128029c8c4f487190473a69a91"}, - {file = "Shapely-1.8.5.post1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a74631e511153366c6dbe3229fa93f877e3c87ea8369cd00f1d38c76b0ed9ace"}, - {file = "Shapely-1.8.5.post1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:66bdac74fbd1d3458fa787191a90fa0ae610f09e2a5ec398c36f968cc0ed743f"}, - {file = "Shapely-1.8.5.post1-cp36-cp36m-win32.whl", hash = "sha256:6d388c0c1bd878ed1af4583695690aa52234b02ed35f93a1c8486ff52a555838"}, - {file = "Shapely-1.8.5.post1-cp36-cp36m-win_amd64.whl", hash = "sha256:be9423d5a3577ac2e92c7e758bd8a2b205f5e51a012177a590bc46fc51eb4834"}, - {file = "Shapely-1.8.5.post1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5d7f85c2d35d39ff53c9216bc76b7641c52326f7e09aaad1789a3611a0f812f2"}, - {file = "Shapely-1.8.5.post1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:adcf8a11b98af9375e32bff91de184f33a68dc48b9cb9becad4f132fa25cfa3c"}, - {file = "Shapely-1.8.5.post1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:753ed0e21ab108bd4282405b9b659f2e985e8502b1a72b978eaa51d3496dee19"}, - {file = "Shapely-1.8.5.post1-cp37-cp37m-win32.whl", hash = "sha256:65b21243d8f6bcd421210daf1fabb9de84de2c04353c5b026173b88d17c1a581"}, - {file = "Shapely-1.8.5.post1-cp37-cp37m-win_amd64.whl", hash = "sha256:370b574c78dc5af3a198a6da5d9b3d7c04654bd2ef7e80e80a3a0992dfb2d9cd"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:532a55ee2a6c52d23d6f7d1567c8f0473635f3b270262c44e1b0c88096827e22"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3480657460e939f45a7d359ef0e172a081f249312557fe9aa78c4fd3a362d993"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b65f5d530ba91e49ffc7c589255e878d2506a8b96ffce69d3b7c4500a9a9eaf8"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:147066da0be41b147a61f8eb805dea3b13709dbc873a431ccd7306e24d712bc0"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c2822111ddc5bcfb116e6c663e403579d0fe3f147d2a97426011a191c43a7458"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-win32.whl", hash = "sha256:2e0a8c2e55f1be1312b51c92b06462ea89e6bb703fab4b114e7a846d941cfc40"}, - {file = "Shapely-1.8.5.post1-cp38-cp38-win_amd64.whl", hash = "sha256:0d885cb0cf670c1c834df3f371de8726efdf711f18e2a75da5cfa82843a7ab65"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0b4ee3132ee90f07d63db3aea316c4c065ed7a26231458dda0874414a09d6ba3"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:02dd5d7dc6e46515d88874134dc8fcdc65826bca93c3eecee59d1910c42c1b17"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6a9a4a31cd6e86d0fbe8473ceed83d4fe760b19d949fb557ef668defafea0f6"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:38f0fbbcb8ca20c16451c966c1f527cc43968e121c8a048af19ed3e339a921cd"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78fb9d929b8ee15cfd424b6c10879ce1907f24e05fb83310fc47d2cd27088e40"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-win32.whl", hash = "sha256:8e59817b0fe63d34baedaabba8c393c0090f061917d18fc0bcc2f621937a8f73"}, - {file = "Shapely-1.8.5.post1-cp39-cp39-win_amd64.whl", hash = "sha256:e9c30b311de2513555ab02464ebb76115d242842b29c412f5a9aa0cac57be9f6"}, - {file = "Shapely-1.8.5.post1.tar.gz", hash = "sha256:ef3be705c3eac282a28058e6c6e5503419b250f482320df2172abcbea642c831"}, {file = "shapely-2.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7266080d39946395ba4b31fa35b9b7695e0a4e38ccabf0c67e2936caf9f9b054"}, {file = "shapely-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a7ba97c97d85c1f07c57f9524c45128ef2bf8279061945d78052c78862b357f"}, {file = "shapely-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4ed31658fd0799eaa3569982aab1a5bc8fcf25ec196606bf137ee4fa984be88"}, @@ -517,18 +481,10 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ - {file = "tox-4.0.16-py3-none-any.whl", hash = "sha256:1ba98d4a67b815403e616cf6ded707aeb0fadff10702928f9b990274c9703e9f"}, - {file = "tox-4.0.16.tar.gz", hash = "sha256:968fc4e27110defdf15972893cb15fe1669f338c8408d8835077462fb07e07fe"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "tox-4.2.6-py3-none-any.whl", hash = "sha256:fb79b3e4b788491949576a9c80c2d56419eac994567c3591e24bb2788b5901d0"}, + {file = "tox-4.2.6.tar.gz", hash = "sha256:ecf224a4f3a318adcdd71aa8fe15ffd31f14afd6a9845a43ffd63950a7325538"}, ] virtualenv = [ {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, -] diff --git a/pyproject.toml b/pyproject.toml index 58366a4..fcc61dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ exclude = [ "**/*.wkt", ] classifiers = [ - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -33,13 +32,14 @@ requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" protobuf = "^4.21" -shapely = [ - { version = "<2", python = "<3.8" }, - { version = "*", python = ">=3.8" } -] +shapely = "^2.0.0" pyclipper = "^1.3.0" +pyproj = { version = "^3.4.1", optional = true } + +[tool.poetry.extras] +proj = ["pyproj"] [tool.poetry.group.test.dependencies] tox = "^4.0.16" @@ -56,7 +56,7 @@ extend-exclude = ''' )/ ) ''' -target-version = ["py37", "py38", "py39", "py310", "py311"] +target-version = ["py38", "py39", "py310", "py311"] # Isort [tool.isort] diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 9583673..7e83b6b 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -5,6 +5,7 @@ import unittest import mapbox_vector_tile +from mapbox_vector_tile.utils import DEFAULT_DECODE_OPTIONS, get_decode_options class BaseTestCase(unittest.TestCase): @@ -12,6 +13,27 @@ def test_decoder(self): vector_tile = b'\x1aI\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 x\x02' # noqa self.assertEqual( mapbox_vector_tile.decode(vector_tile), + { + "water": { + "version": 2, + "extent": 4096, + "type": "FeatureCollection", + "features": [ + { + "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]}, + "properties": {"foo": "bar", "baz": "foo", "uid": 123}, + "id": 1, + "type": "Feature", + } + ], + }, + }, + ) + + def test_decoder_geojson(self): + vector_tile = b'\x1aI\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 x\x02' # noqa + self.assertEqual( + mapbox_vector_tile.decode(vector_tile, default_options={"geojson": False}), { "water": { "version": 2, @@ -40,6 +62,7 @@ def test_decode_polygon_no_cmd_seg_end(self): "water": { "version": 2, "extent": 4096, + "type": "FeatureCollection", "features": [ { "geometry": { @@ -51,7 +74,7 @@ def test_decode_polygon_no_cmd_seg_end(self): }, "properties": {}, "id": 0, - "type": 3, + "type": "Feature", } ], }, @@ -66,14 +89,43 @@ def test_nondefault_extent(self): "water": { "version": 2, "extent": 8192, + "type": "FeatureCollection", "features": [ { "geometry": {"type": "LineString", "coordinates": [[8000, 7000], [4000, 3500], [0, 0]]}, "id": 1, "properties": {"baz": "foo", "foo": "bar", "uid": 123}, - "type": 2, + "type": "Feature", } ], } }, ) + + def test_options(self): + layer_options_1 = {"y_coord_down": True, "transformer": "my_function"} + layer_options_2 = {"geojson": True} + default_options = {"geojson": False} + self.assertEqual( + get_decode_options(layer_options=layer_options_1, default_options=default_options), + {**layer_options_1, "geojson": False}, + ) + self.assertEqual( + get_decode_options(layer_options=layer_options_2, default_options=default_options), + {**layer_options_2, "y_coord_down": False, "transformer": None}, + ) + self.assertEqual( + get_decode_options(layer_options=layer_options_2, default_options=None), + {**layer_options_2, "y_coord_down": False, "transformer": None}, + ) + self.assertEqual( + get_decode_options(layer_options=None, default_options=layer_options_1), + {**layer_options_1, "geojson": True}, + ) + self.assertEqual(get_decode_options(layer_options=None, default_options=None), DEFAULT_DECODE_OPTIONS) + + def test_options_error(self): + expected_result = "The following options are not allowed for decoding a tile: 'opt', 'unknown'." + with self.assertRaises(ValueError) as ex: + get_decode_options(layer_options={"geojson": False, "unknown": 23}, default_options={"opt": 42}) + self.assertEqual(str(ex.exception), expected_result) diff --git a/tests/test_encoder.py b/tests/test_encoder.py index 6b1158e..b93111a 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -7,6 +7,7 @@ import mapbox_vector_tile from mapbox_vector_tile import decode, encode +from mapbox_vector_tile.utils import DEFAULT_ENCODE_OPTIONS, get_encode_options class BaseTestCase(unittest.TestCase): @@ -111,7 +112,9 @@ def test_encoder_multipolygon(self): def test_encoder_multipolygon_w_hole(self): self.assertRoundTrip( - input_geometry="MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))", # noqa + input_geometry="MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), " + "((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), " + "(30 20, 20 15, 20 25, 30 20)))", expected_geometry={ "type": "MultiPolygon", "coordinates": [ @@ -401,7 +404,10 @@ def test_too_small_linestring(self): shape = shapely.wkt.loads("LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)") features = [dict(geometry=shape, properties={})] - pbf = encode({"name": "foo", "features": features}, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode( + {"name": "foo", "features": features}, + default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}, + ) result = decode(pbf) features = result["foo"]["features"] self.assertEqual(0, len(features)) @@ -497,7 +503,7 @@ def test_quantize(self): features = [feature] source = dict(name="layername", features=features) bounds = 10.0, 10.0, 20.0, 20.0 - pbf = encode(source, quantize_bounds=bounds) + pbf = encode(source, default_options={"quantize_bounds": bounds}) result = decode(pbf) act_feature = result["layername"]["features"][0] act_geom = act_feature["geometry"] @@ -512,8 +518,8 @@ def test_y_coord_down(self): feature = dict(geometry=shape, properties=props) features = [feature] source = dict(name="layername", features=features) - pbf = encode(source, y_coord_down=True) - result = decode(pbf, y_coord_down=True) + pbf = encode(source, default_options={"y_coord_down": True}) + result = decode(pbf, default_options={"y_coord_down": True}) act_feature = result["layername"]["features"][0] act_geom = act_feature["geometry"] exp_geom = {"type": "Point", "coordinates": [10, 10]} @@ -528,9 +534,9 @@ def test_quantize_and_y_coord_down(self): features = [feature] source = dict(name="layername", features=features) bounds = 0.0, 0.0, 50.0, 50.0 - pbf = encode(source, quantize_bounds=bounds, y_coord_down=True) + pbf = encode(source, default_options={"quantize_bounds": bounds, "y_coord_down": True}) - result_decode_no_flip = decode(pbf, y_coord_down=True) + result_decode_no_flip = decode(pbf, default_options={"y_coord_down": True}) act_feature = result_decode_no_flip["layername"]["features"][0] act_geom = act_feature["geometry"] exp_geom = {"type": "Point", "coordinates": [2458, 2458]} @@ -553,7 +559,7 @@ def test_custom_extent(self): features = [feature] source = dict(name="layername", features=features) bounds = 0.0, 0.0, 10.0, 10.0 - pbf = encode(source, quantize_bounds=bounds, extents=50) + pbf = encode(source, default_options={"quantize_bounds": bounds, "extents": 50}) result = decode(pbf) act_feature = result["layername"]["features"][0] act_geom = act_feature["geometry"] @@ -573,7 +579,7 @@ def test_invalid_geometry_ignore(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_ignore) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_ignore}) result = decode(pbf) self.assertEqual(0, len(result["layername"]["features"])) @@ -589,10 +595,9 @@ def test_invalid_geometry_raise(self): feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) with self.assertRaises(Exception): - encode(source, on_invalid_geometry=on_invalid_geometry_raise) + encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_raise}) def test_invalid_geometry_make_valid(self): - import shapely.geometry import shapely.wkt from mapbox_vector_tile import encode @@ -603,7 +608,7 @@ def test_invalid_geometry_make_valid(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}) result = decode(pbf) self.assertEqual(1, len(result["layername"]["features"])) valid_geometry = result["layername"]["features"][0]["geometry"] @@ -627,7 +632,7 @@ def test_bowtie_self_touching(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}) result = decode(pbf) self.assertEqual(1, len(result["layername"]["features"])) valid_geometries = result["layername"]["features"][0]["geometry"] @@ -651,7 +656,7 @@ def test_bowtie_self_crossing(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}) result = decode(pbf) self.assertEqual(1, len(result["layername"]["features"])) valid_geometries = result["layername"]["features"][0]["geometry"] @@ -679,12 +684,10 @@ def test_make_valid_self_crossing(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}) result = decode(pbf) self.assertEqual(1, len(result["layername"]["features"])) valid_geometries = result["layername"]["features"][0]["geometry"] - geom_type = result["layername"]["features"][0]["type"] - self.assertEqual(3, geom_type) # 3 means POLYGON self.assertEqual(valid_geometries["type"], "MultiPolygon") multipolygon = shapely.geometry.shape(valid_geometries) self.assertTrue(multipolygon.is_valid) @@ -699,7 +702,6 @@ def test_make_valid_self_crossing(self): self.assertEqual(50, multipolygon.area) def test_validate_generates_rounding_error(self): - import shapely.geometry import shapely.wkt from mapbox_vector_tile import encode @@ -710,7 +712,7 @@ def test_validate_generates_rounding_error(self): self.assertFalse(shape.is_valid) feature = dict(geometry=shape, properties={}) source = dict(name="layername", features=[feature]) - pbf = encode(source, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode(source, default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}) result = decode(pbf) features = result["layername"]["features"] self.assertEqual(1, len(features)) @@ -748,8 +750,10 @@ def test_quantize_makes_mutlipolygon_invalid(self): features = [dict(geometry=shape, properties={})] pbf = encode( {"name": "foo", "features": features}, - quantize_bounds=quantize_bounds, - on_invalid_geometry=on_invalid_geometry_make_valid, + default_options={ + "quantize_bounds": quantize_bounds, + "on_invalid_geometry": on_invalid_geometry_make_valid, + }, ) result = decode(pbf) features = result["foo"]["features"] @@ -771,7 +775,10 @@ def test_flipped_geometry_produces_multipolygon(self): """3449 1939))""" ) features = [dict(geometry=shape, properties={})] - pbf = encode({"name": "foo", "features": features}, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode( + {"name": "foo", "features": features}, + default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}, + ) result = decode(pbf) features = result["foo"]["features"] self.assertEqual(1, len(features)) @@ -799,8 +806,10 @@ def test_make_valid_can_return_multipolygon(self): features = [dict(geometry=shape, properties={})] pbf = encode( {"name": "foo", "features": features}, - quantize_bounds=(-10018754.1713946, 11271098.44281893, -8766409.899970269, 12523442.714243261), - on_invalid_geometry=on_invalid_geometry_make_valid, + default_options={ + "quantize_bounds": (-10018754.1713946, 11271098.44281893, -8766409.899970269, 12523442.714243261), + "on_invalid_geometry": on_invalid_geometry_make_valid, + }, ) result = decode(pbf) features = result["foo"]["features"] @@ -826,7 +835,10 @@ def test_too_small_geometry(self): "LINESTRING (3065.656210384849 3629.831662879646, 3066.458953567231 3629.725941289478)" ) features = [dict(geometry=shape, properties={})] - pbf = encode({"name": "foo", "features": features}, on_invalid_geometry=on_invalid_geometry_make_valid) + pbf = encode( + {"name": "foo", "features": features}, + default_options={"on_invalid_geometry": on_invalid_geometry_make_valid}, + ) result = decode(pbf) features = result["foo"]["features"] self.assertEqual(0, len(features)) @@ -882,8 +894,11 @@ def test_example_multi_polygon(self): 15, # 1 x close path ] - tile = VectorTile(4096) - tile.add_features([dict(geometry=input_geometry)], "example_layer", quantize_bounds=None, y_coord_down=True) + tile = VectorTile(default_options={"extents": 4096, "quantize_bounds": None, "y_coord_down": True}) + tile.add_layer( + name="example_layer", + features=[dict(geometry=input_geometry)], + ) self.assertEqual(1, len(tile.layer.features)) f = tile.layer.features[0] self.assertEqual(expected_commands, list(f.geometry)) @@ -937,8 +952,8 @@ def test_example_multi_polygon_y_up(self): 15, # 1 x close path ] - tile = VectorTile(20) - tile.add_features([dict(geometry=input_geometry)], "example_layer", quantize_bounds=None, y_coord_down=False) + tile = VectorTile(default_options={"extents": 20, "quantize_bounds": None, "y_coord_down": False}) + tile.add_layer(name="example_layer", features=[dict(geometry=input_geometry)]) self.assertEqual(1, len(tile.layer.features)) f = tile.layer.features[0] self.assertEqual(expected_commands, list(f.geometry)) @@ -961,8 +976,8 @@ def test_issue_57(self): 15, # 1 x close path ] - tile = VectorTile(4096) - tile.add_features([dict(geometry=input_geometry)], "example_layer", quantize_bounds=None, y_coord_down=True) + tile = VectorTile(default_options={"extents": 4096, "quantize_bounds": None, "y_coord_down": True}) + tile.add_layer(name="example_layer", features=[dict(geometry=input_geometry)]) self.assertEqual(1, len(tile.layer.features)) f = tile.layer.features[0] self.assertEqual(expected_commands, list(f.geometry)) @@ -978,7 +993,7 @@ def test_duplicate_layer_name(self): features = [feature] source = [dict(name="layername", features=features), dict(name="layername", features=features)] with self.assertRaises(ValueError) as ex: - encode(source, extents=4096) + encode(source, default_options={"extents": 4096}) self.assertEqual(str(ex.exception), "The layer name 'layername' already exists in the vector tile.") def test_empty_layer_name(self): @@ -990,23 +1005,23 @@ def test_empty_layer_name(self): features = [feature] source = [dict(name="", features=features)] with self.assertRaises(ValueError) as ex: - encode(source, extents=4096) + encode(source, default_options={"extents": 4096}) self.assertEqual(str(ex.exception), "A layer name can not be empty. '' was provided.") source = [dict(name=None, features=features)] with self.assertRaises(ValueError) as ex: - encode(source, extents=4096) + encode(source, default_options={"extents": 4096}) self.assertEqual(str(ex.exception), "A layer name can not be empty. None was provided.") def test_empty_layer(self): from mapbox_vector_tile import encode # No content - res = encode([], extents=4096) + res = encode([], default_options={"extents": 4096}) self.assertEqual(res, b"") # Layer without feature - res = encode(dict(name="layer", features=[]), extents=4096) + res = encode(dict(name="layer", features=[]), default_options={"extents": 4096}) self.assertEqual(res, b"\x1a\x0c\n\x05layer(\x80 x\x02") def test_invalid_extent(self): @@ -1018,9 +1033,124 @@ def test_invalid_extent(self): features = [feature] source = [dict(name="layername", features=features)] with self.assertRaises(ValueError) as ex: - encode(source, extents=0) + encode(source, default_options={"extents": 0}) self.assertEqual(str(ex.exception), "The extents must be positive. 0 provided.") with self.assertRaises(ValueError) as ex: - encode(source, extents=-2.3) + encode(source, default_options={"extents": -2.3}) self.assertEqual(str(ex.exception), "The extents must be positive. -2.3 provided.") + + +class TransformerTestCase(unittest.TestCase): + def test_transformer(self): + from shapely.geometry import shape + from shapely.wkt import loads + + def forward_transformer(x, y): + return x + 5, y + 2 + + def backward_transformer(x, y): + return x - 5, y - 2 + + source = { + "name": "water", + "features": [ + { + "geometry": """LINESTRING (76097 5861479, 418535 6066942, 526159 5685368, 878380 5910399,""" + """1514336 5900615, 1426281 5558177)""", + "properties": {"uid": 123, "foo": "bar", "cat": "flew"}, + } + ], + } + + encoded = encode(source, default_options={"transformer": forward_transformer, "extents": 4096}) + decoded = decode(encoded, default_options={"transformer": backward_transformer}) + + # Source data + layer_name = source["name"] + nb_features = len(source["features"]) + + # Output data + layer = decoded[layer_name] + features = layer["features"] + self.assertEqual(nb_features, len(features)) + for source_feature, destination_feature in zip(source["features"], features): + self.assertEqual(source_feature["properties"], destination_feature["properties"]) + source_geometry = loads(source_feature["geometry"]) + destination_geometry = shape(destination_feature["geometry"]) + self.assertTrue(source_geometry.equals_exact(destination_geometry, tolerance=1e-6)) + + +class OptionsTestCase(unittest.TestCase): + def test_options(self): + layer_options_1 = {"y_coord_down": True, "transformer": "my_function"} + layer_options_2 = {"quantize_bounds": (10.0, 10.0, 20.0, 20.0)} + default_options = {"extents": 42} + self.assertEqual( + get_encode_options(layer_options=layer_options_1, default_options=default_options), + { + **layer_options_1, + "check_winding_order": True, + "extents": 42, + "max_geometry_validate_tries": 5, + "on_invalid_geometry": None, + "quantize_bounds": None, + }, + ) + self.assertEqual( + get_encode_options(layer_options=layer_options_2, default_options=default_options), + { + **layer_options_2, + "check_winding_order": True, + "extents": 42, + "max_geometry_validate_tries": 5, + "on_invalid_geometry": None, + "transformer": None, + "y_coord_down": False, + }, + ) + self.assertEqual( + get_encode_options(layer_options=layer_options_2, default_options=None), + { + **layer_options_2, + "check_winding_order": True, + "extents": 4096, + "max_geometry_validate_tries": 5, + "on_invalid_geometry": None, + "transformer": None, + "y_coord_down": False, + }, + ) + self.assertEqual( + get_encode_options(layer_options=None, default_options=layer_options_1), + { + **layer_options_1, + "check_winding_order": True, + "extents": 4096, + "max_geometry_validate_tries": 5, + "on_invalid_geometry": None, + "quantize_bounds": None, + }, + ) + self.assertEqual(get_encode_options(layer_options=None, default_options=None), DEFAULT_ENCODE_OPTIONS) + + def test_options_error(self): + expected_result = "The following options are not allowed for encoding a tile: 'opt', 'unknown'." + with self.assertRaises(ValueError) as ex: + get_encode_options(layer_options={"y_coord_down": False, "unknown": 23}, default_options={"opt": 42}) + self.assertEqual(str(ex.exception), expected_result) + + expected_result = "The max_geometry_validate_tries must be positive. -3 provided." + with self.assertRaises(ValueError) as ex: + get_encode_options( + layer_options={"y_coord_down": False, "max_geometry_validate_tries": -3}, + default_options={"extents": 42}, + ) + self.assertEqual(str(ex.exception), expected_result) + + expected_result = "The extents must be positive. 0 provided." + with self.assertRaises(ValueError) as ex: + get_encode_options( + layer_options={"y_coord_down": False, "max_geometry_validate_tries": 25}, default_options={"extents": 0} + ) + self.assertEqual(str(ex.exception), expected_result) diff --git a/tox.ini b/tox.ini index b0846f5..38eda49 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311 +envlist = py38,py39,py310,py311 [tox:.package] isolated_build = true