Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

use ST_TileEnvelope to generate tiles #24

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions rest_framework_mvt/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
23 changes: 19 additions & 4 deletions rest_framework_mvt/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions sphinx/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/<int:z>/<int:x>/<int:y>/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 <https://docs.mapbox.com/vector-tiles/reference/>`_
Expand Down
21 changes: 11 additions & 10 deletions test/unit/test_mvt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={})

Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
47 changes: 31 additions & 16 deletions test/unit/test_mvt_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Copy link
Member

Choose a reason for hiding this comment

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

I think this comment can be removed.

request_factory = RequestFactory()
request = request_factory.get("/hello", {"tile": "2/1/1"})

response = base_mvt_view.get(request)

Expand All @@ -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)

Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

Is this still TODO?

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
Expand Down