diff --git a/rest_framework_mvt/managers.py b/rest_framework_mvt/managers.py index c392449..ca72c54 100644 --- a/rest_framework_mvt/managers.py +++ b/rest_framework_mvt/managers.py @@ -16,7 +16,7 @@ def __init__(self, *args, geo_col="geom", source_name=None, **kwargs): self.geo_col = geo_col self.source_name = source_name - def intersect(self, bbox="", limit=-1, offset=0, filters={}): + def intersect(self, xyz, limit=-1, offset=0, filters={}): """ Args: bbox (str): A string representing a bounding box, e.g., '-90,29,-89,35'. @@ -45,7 +45,19 @@ def intersect(self, bbox="", limit=-1, offset=0, filters={}): limit = "ALL" if limit == -1 else limit query, parameters = self._build_query(filters=filters) with self._get_connection().cursor() as cursor: - cursor.execute(query, [str(bbox), str(bbox)] + parameters + [limit, offset]) + cursor.execute( + query, + [ + str(xyz[2]), + str(xyz[0]), + str(xyz[1]), + str(xyz[2]), + str(xyz[0]), + str(xyz[1]), + ] + + parameters + + [limit, offset], + ) mvt = cursor.fetchall()[-1][-1] # should always return one tile on success return mvt @@ -82,7 +94,7 @@ def _build_query(self, filters={}): SELECT NULL AS id, ST_AsMVT(q, 'default', 4096, 'mvt_geom') FROM (SELECT {select_statement} ST_AsMVTGeom(ST_Transform({table}.{self.geo_col}, 3857), - ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 4326), 3857), 4096, 0, false) AS mvt_geom + ST_Transform(ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326), 3857), 4096, 0, false) AS mvt_geom FROM {table} WHERE {parameterized_where_clause} LIMIT %s @@ -109,7 +121,7 @@ def _create_where_clause_with_params(self, table, filters): extra_wheres = " AND " + sql.split("WHERE")[1].strip() if params else "" where_clause = ( f"ST_Intersects(ST_Transform({table}.{self.geo_col}, 4326), " - f"ST_SetSRID(ST_GeomFromText(%s), 4326)){extra_wheres}" + f"ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)){extra_wheres}" ) return where_clause, list(params) diff --git a/rest_framework_mvt/views.py b/rest_framework_mvt/views.py index fe8eb8e..6eed92f 100644 --- a/rest_framework_mvt/views.py +++ b/rest_framework_mvt/views.py @@ -1,7 +1,6 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.serializers import ValidationError -from rest_framework_gis.filters import TMSTileFilter from rest_framework_mvt.renderers import BinaryRenderer from rest_framework_mvt.schemas import MVT_SCHEMA @@ -25,17 +24,33 @@ def get(self, request, *args, **kwargs): :py:class:`rest_framework.response.Response`: Standard DRF response object """ params = request.GET.dict() - if params.pop("tile", None) is not None: + tilex = tiley = tilez = None + + # zxy may come from path or parameters. + if "z" in kwargs and "x" in kwargs and "y" in kwargs: + tilex = kwargs["x"] + tiley = kwargs["y"] + tilez = kwargs["z"] + if "tile" in params: + zxy = params.pop("tile", "").split("/") + if len(zxy) == 3: + tilez = zxy[0] + tilex = zxy[1] + tiley = zxy[2] + + if tilez and tilex and tiley: try: limit, offset = self._validate_paginate( params.pop("limit", None), params.pop("offset", None) ) except ValidationError: limit, offset = None, None - bbox = TMSTileFilter().get_filter_bbox(request) try: mvt = self.model.vector_tiles.intersect( - bbox=bbox, limit=limit, offset=offset, filters=params + [tilex, tiley, tilez], + limit=limit, + offset=offset, + filters=params, ) status = 200 if mvt else 204 except ValidationError: diff --git a/sphinx/index.rst b/sphinx/index.rst index 309d7ed..12df9d9 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -66,6 +66,20 @@ The following requests should now be enabled: GET api/v1/data/example.mvt?tile=1/0/0&my_column=foo&limit=10&offset=10 HTTP/1.1 +Alternatively tile x, y and z values may be provided as path keyword arguments: + +.. code-block:: python + + from rest_framework_mvt.views import mvt_view_factory + + urlpatterns = [ + path("api/v1/data////example.mvt", mvt_view_factory(Example)), + ] + +.. sourcecode:: http + + GET api/v1/data/1/0/0/example.mvt HTTP/1.1 + References ========== - `Mapbox Vector Tile Introduction `_ diff --git a/test/unit/test_mvt_manager.py b/test/unit/test_mvt_manager.py index 2e8b7f8..3929f17 100644 --- a/test/unit/test_mvt_manager.py +++ b/test/unit/test_mvt_manager.py @@ -51,7 +51,7 @@ def test_mvt_manager_intersect__calls__build_query(get_conn, mvt_manager): mvt_manager._build_query = MagicMock() mvt_manager._build_query.return_value = ("foo", ["bar"]) - mvt_manager.intersect(bbox="", limit=10, offset=7) + mvt_manager.intersect([1, 0, 0], limit=10, offset=7) mvt_manager._build_query.assert_called_once_with(filters={}) @@ -66,9 +66,9 @@ def test_mvt_manager_build_query__all(get_conn, only, mvt_manager): SELECT NULL AS id, ST_AsMVT(q, 'default', 4096, 'mvt_geom') FROM (SELECT other_column, city, ST_AsMVTGeom(ST_Transform(test_table.jazzy_geo, 3857), - ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 4326), 3857), 4096, 0, false) AS mvt_geom + ST_Transform(ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326), 3857), 4096, 0, false) AS mvt_geom FROM test_table - WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_SetSRID(ST_GeomFromText(%s), 4326)) + WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)) LIMIT %s OFFSET %s) AS q; """.strip() @@ -90,9 +90,9 @@ def test_mvt_manager_build_query__no_geo_col(get_conn, only, mvt_manager_no_col) SELECT NULL AS id, ST_AsMVT(q, 'default', 4096, 'mvt_geom') FROM (SELECT other_column, city, ST_AsMVTGeom(ST_Transform(test_table.geom, 3857), - ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 4326), 3857), 4096, 0, false) AS mvt_geom + ST_Transform(ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326), 3857), 4096, 0, false) AS mvt_geom FROM test_table - WHERE ST_Intersects(ST_Transform(test_table.geom, 4326), ST_SetSRID(ST_GeomFromText(%s), 4326)) + WHERE ST_Intersects(ST_Transform(test_table.geom, 4326), ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)) LIMIT %s OFFSET %s) AS q; """.strip() @@ -120,9 +120,9 @@ def test_mvt_manager_build_query__filter(get_conn, only, orm_filter, mvt_manager SELECT NULL AS id, ST_AsMVT(q, 'default', 4096, 'mvt_geom') FROM (SELECT other_column, city, ST_AsMVTGeom(ST_Transform(test_table.jazzy_geo, 3857), - ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 4326), 3857), 4096, 0, false) AS mvt_geom + ST_Transform(ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326), 3857), 4096, 0, false) AS mvt_geom FROM test_table - WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_SetSRID(ST_GeomFromText(%s), 4326)) AND (city = %s) + WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)) AND (city = %s) LIMIT %s OFFSET %s) AS q; """.strip() @@ -151,9 +151,9 @@ def test_mvt_manager_build_query__multiple_filters( SELECT NULL AS id, ST_AsMVT(q, 'default', 4096, 'mvt_geom') FROM (SELECT other_column, city, ST_AsMVTGeom(ST_Transform(test_table.jazzy_geo, 3857), - ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 4326), 3857), 4096, 0, false) AS mvt_geom + ST_Transform(ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326), 3857), 4096, 0, false) AS mvt_geom FROM test_table - WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_SetSRID(ST_GeomFromText(%s), 4326)) AND (city = %s AND other_column = %s) + WHERE ST_Intersects(ST_Transform(test_table.jazzy_geo, 4326), ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)) AND (city = %s AND other_column = %s) LIMIT %s OFFSET %s) AS q; """.strip() @@ -210,7 +210,8 @@ def test_mvt_manager_create_where_clause_with_params(get_conn, orm_filter, mvt_m orm_filter.assert_called_once_with(col_1="filter_1", foreign_key=1) query_filter.sql_with_params.assert_called_once() assert parameterized_where_clause == ( - "ST_Intersects(ST_Transform(my_schema.my_table.jazzy_geo, 4326), ST_SetSRID(ST_GeomFromText(%s), 4326)) " + "ST_Intersects(ST_Transform(my_schema.my_table.jazzy_geo, 4326), ST_Transform(ST_TileEnvelope(%s, %s, %s), 4326)) " 'AND ("my_schema"."my_table"."col_1" = %s AND "my_schema"."my_table"."foreign_key_id" = %s)' ) + assert where_clause_parameters == ["filter_1", 1] diff --git a/test/unit/test_mvt_view.py b/test/unit/test_mvt_view.py index d5c0f8b..cfc433d 100644 --- a/test/unit/test_mvt_view.py +++ b/test/unit/test_mvt_view.py @@ -2,17 +2,19 @@ from rest_framework_mvt.views import BaseMVTView from rest_framework.serializers import ValidationError +from django.test.client import RequestFactory -@patch("rest_framework_mvt.views.TMSTileFilter") -def test_BaseMVTView__get(tile_filter): +def test_BaseMVTView__get(): base_mvt_view = BaseMVTView(geo_col="geom") model = MagicMock() vector_tiles = MagicMock() vector_tiles.intersect.return_value = b"mvt goes here" model.vector_tiles = vector_tiles base_mvt_view.model = model - request = MagicMock(query_params={"tile": "2/1/1"}) + # request = MagicMock(query_params={"tile": "2/1/1"}) + request_factory = RequestFactory() + request = request_factory.get("/hello", {"tile": "2/1/1"}) response = base_mvt_view.get(request) @@ -21,16 +23,29 @@ def test_BaseMVTView__get(tile_filter): assert response.content_type == "application/vnd.mapbox-vector-tile" vector_tiles.intersect.assert_called_once() + # Test path sources tile kwargs. + request = request_factory.get("/hello") + response = base_mvt_view.get(request, z=2, x=1, y=1) + assert response.status_code == 200 + assert response.data == b"mvt goes here" + assert response.content_type == "application/vnd.mapbox-vector-tile" + + # Test no tile arguments. + response = base_mvt_view.get(request) + assert response.status_code == 400 + assert response.data == b"" + assert response.content_type == "application/vnd.mapbox-vector-tile" -@patch("rest_framework_mvt.views.TMSTileFilter") -def test_BaseMVTView__intersects_validation_error_returns_400(tile_filter): + +def test_BaseMVTView__intersects_validation_error_returns_400(): base_mvt_view = BaseMVTView(geo_col="geom") model = MagicMock() vector_tiles = MagicMock() vector_tiles.intersect.side_effect = ValidationError("Invalid Parameters") model.vector_tiles = vector_tiles base_mvt_view.model = model - request = MagicMock(query_params={"tile": "2/1/1"}) + request_factory = RequestFactory() + request = request_factory.get("/hello", {"tile": "2/1/1"}) response = base_mvt_view.get(request) @@ -39,40 +54,40 @@ def test_BaseMVTView__intersects_validation_error_returns_400(tile_filter): assert response.content_type == "application/vnd.mapbox-vector-tile" -@patch("rest_framework_mvt.views.TMSTileFilter") -def test_BaseMVTView__does_not_pass_in_pagination_as_filters(tile_filter): +def test_BaseMVTView__does_not_pass_in_pagination_as_filters(): base_mvt_view = BaseMVTView(geo_col="geom") model = MagicMock() vector_tiles = MagicMock() vector_tiles.intersect.return_value = b"mvt goes here" model.vector_tiles = vector_tiles base_mvt_view.model = model - request = MagicMock(query_params={"tile": "2/1/1", "limit": 1, "offset": 1}) + request_factory = RequestFactory() + request = request_factory.get("/hello", {"tile": "2/1/1", "limit": 1, "offset": 1}) response = base_mvt_view.get(request) assert response.status_code == 200 assert response.data == b"mvt goes here" assert response.content_type == "application/vnd.mapbox-vector-tile" - request.GET.dict().pop.assert_called_with("offset", None) + + # TODO: fix this assertion. + # request.GET.dict().pop.assert_called_with("offset", None) vector_tiles.intersect.assert_called_with( - bbox=tile_filter().get_filter_bbox(), + ["1", "1", "2"], limit=1, offset=1, - filters=request.GET.dict(), + filters={}, ) -@patch("rest_framework_mvt.views.TMSTileFilter") -def test_BaseMVTView__validate_paginate(tile_filter): +def test_BaseMVTView__validate_paginate(): limit, offset = BaseMVTView._validate_paginate("10", "7") assert limit == 10 assert offset == 7 -@patch("rest_framework_mvt.views.TMSTileFilter") -def test_BaseMVTView__validate_paginate_raises_ValidationError(tile_filter): +def test_BaseMVTView__validate_paginate_raises_ValidationError(): try: limit, offset = BaseMVTView._validate_paginate("cat", "7") assert False