From 2f82b051cdb5afc943c1e269bacc075dd1a3b6cd Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 16 Feb 2024 11:05:53 +0100 Subject: [PATCH 1/6] improve documentation --- README.md | 279 +++++++++++++++++++++++++++++++++++++----- docs/usage.rst | 200 +++++++++++++++++++++++++----- vectortiles/mixins.py | 20 ++- 3 files changed, 434 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 405bb46..3d79602 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,19 @@ pip install django-vectortiles ``` * By default, postgis backend is enabled. -* Ensure you have psycopg2 set and installed +* Ensure you have psycopg installed #### If you don't want to use Postgis and / or PostgreSQL ```bash pip install django-vectortiles[python] ``` -* This will incude mapbox_vector_tiles package and its dependencies -* Set VECTOR_TILES_BACKEND to "vectortiles.backends.python" +* This will include mapbox_vector_tiles package and its dependencies +* Set ```VECTOR_TILES_BACKEND="vectortiles.backends.python"``` in your project settings. ### Examples +Let's create vector tiles with your city geometries. + * assuming you have ```django.contrib.gis``` in your ```INSTALLED_APPS``` and a gis compatible database backend ```python @@ -37,35 +39,90 @@ pip install django-vectortiles[python] from django.contrib.gis.db import models -class Feature(models.Model): - geom = models.GeometryField(srid=4326) +class City(models.Model): name = models.CharField(max_length=250) + city_code = models.CharField(max_length=10, unique=True) + population = models.IntegerField(default=0) + geom = models.MultiPolygonField(srid=4326) ``` #### Simple Example: ```python -from yourapp.models import Feature +from yourapp.models import City + +# in a vector_layers.py file +from vectortiles import VectorLayer + + +class CityVL(VectorLayer): + model = City + id = "cities" # layer id / name in tile + tile_fields = ("name", "city_code") # add name and city_code properties in each tile feature + min_zoom = 9 # don't embed city borders at low zoom levels + +# in your view file + +from yourapp.vector_layers import CityVL + +from vectortiles.views import MVTView + + +class CityTileView(MVTView): + layer_classes = [CityVL] + + +# in your urls file +from django.urls import path +from yourapp import views + +urlpatterns = [ + ... + CityTileView.get_url(), # serve tiles at default /tiles///. You can override url prefix and tile scheme in class attributes. + ... +] +``` +#### Example with multiple layers + +Suppose you want to make a map with your city borders, and a point in each city center that shows a popup with city name, population an area. + +```python +from django.contrib.gis.db.models.functions import Centroid, Area +from yourapp.models import City # in a vector_layers.py file from vectortiles import VectorLayer -class FeatureVectorLayer(VectorLayer): - model = Feature - vector_tile_layer_name = "features" - vector_tile_fields = ("name",) +class CityVectorLayer(VectorLayer): + model = City + id = "cities" + tile_fields = ('city_code', "name") + min_zoom = 10 + + +class CityCentroidVectorLayer(VectorLayer): + queryset = City.objects.annotate( + centroid=Centroid("geom"), # compute the city centroïd + area=Area("geom"), # compute the city centroïd + ) + geom_field = "centroid" # use the centroid field as geometry feature + id = "city_centroïds" + tile_fields = ('name', 'city_code', 'area', 'population') # add area and population properties in each tile feature + min_zoom = 7 # let's show city name at zoom 7 + + # in your view file -from yourapp.vector_layers import FeatureVectorLayer +from yourapp.vector_layers import CityVectorLayer, CityCentroidVectorLayer from vectortiles.views import MVTView -class FeatureTileView(MVTView): - layer_classes = [FeatureVectorLayer] +class CityTileView(MVTView): + layer_classes = [CityVectorLayer, CityCentroidVectorLayer] # in your urls file @@ -74,7 +131,7 @@ from yourapp import views urlpatterns = [ ... - path('tiles///', views.FeatureTileView.as_view(), name="feature-tile"), + views.CityTileView.get_url(), # serve tiles at default /tiles/// ... ] ``` @@ -86,21 +143,24 @@ urlpatterns = [ from django.urls import reverse -from vectortiles.views import TileJSONView -from yourapp.vector_layers import FeatureVectorLayer +from vectortiles.views import MVTView, TileJSONView +from yourapp.vector_layers import CityVectorLayer, CityCentroidVectorLayer -class FeatureTileJSONView(TileJSONView): +class CityTileBaseView: + layer_classes = [CityVectorLayer, CityCentroidVectorLayer] + + +class CityTileView(CityTileBaseView, MVTView): + pass + + +class CityTileJSONView(CityTileBaseView, TileJSONView): """Simple model TileJSON View""" - name = "My features dataset" + name = "My city dataset" attribution = "@JEC Data" - description = "My dataset" - layer_classes = [FeatureVectorLayer] - - def get_tile_url(self): - """ Base MVTView Url used to generates urls in TileJSON in a.tiles.xxxx/{z}/{x}/{y} format """ - return str(reverse("feature-tile", args=(0, 0, 0))).replace("0/0/0", "{z}/{x}/{y}") + description = "My vity dataset" # in your urls file @@ -109,11 +169,12 @@ from yourapp import views urlpatterns = [ ... - path('tiles///', views.FeatureTileView.as_view(), name="feature-tile"), - path("feature/tiles.json", views.FeatureTileJSONView.as_view(), name="feature-tilejson"), + CityTileView.get_url(), # serve tiles at default /tiles/// + CityTileJSONView.get_url(name="city-tilejson"), # serve tilejson at /tiles.json ... ] -] + +# if you want to use multiple domains, you can set the allowed hosts and vector tiles urls in your settings file # in your settings file ALLOWED_HOSTS = [ "a.tiles.xxxx", @@ -130,9 +191,167 @@ VECTOR_TILES_URLS = [ ``` -#### Usage without PostgreSQL / PostGIS -Just import and use vectortiles.mapbox.view.MVTView instead of vectortiles.postgis.view.MVTView + +Now, any tile requested at http://you_url/tiles/{z}/{x}/{y} that intersects a city will return a vector tile with two layers, `cities` with border geometries and `city_code` property, and `city_centroïds` with center geometry and `city_name` property. + +```html + + + + + + + City map + + + + +
+ + + + + +``` #### Usage with Django Rest Framework @@ -152,7 +371,7 @@ docker-compose run /code/venv/bin/python ./manage.py test ##### Local -* Install python and django requirements (python 3.6+, django 2.2+) +* Install python and django requirements (python 3.8+, django 3.2+) * Install geodjango requirements * Have a postgresql / postgis 2.4+ enabled database * Use a virtualenv diff --git a/docs/usage.rst b/docs/usage.rst index 239787d..dfc3467 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,49 +1,183 @@ USAGE ===== +A vector tile is composed by vector layers which represent different kind of data. Each layer is composed by features. -Simple model view -***************** +To start using django-vectortiles, you need GeoDjango models with geometries. -.. code-block:: python +Then, you need to describe how your data will be embed in tiles. + +Start by creating vector layers for your data... +.. code-block:: python # in your app models.py from django.contrib.gis.db import models - class Feature(models.Model): - geom = models.GeometryField(srid=4326) + + class City(models.Model): name = models.CharField(max_length=250) + city_code = models.CharField(max_length=10, unique=True) + population = models.IntegerField(default=0) + geom = models.MultiPolygonField(srid=4326) + + + # in a vector_layers.py file (for example) + from vectortiles import VectorLayer + from your_app.models import City + + + class CityVectorLayer(VectorLayer): + model = City # your model, as django conventions you can use queryset or get_queryset method instead) + id = "cities" # layer id in you vector layer. each class attribute can be defined by get_{attribute} method + tile_fields = ('city_code', "name") # fields to include in tile + min_zoom = 10 # minimum zoom level to include layer. Take care of this, as it could be a performance issue. Try to not embed data that will no be shown in your style definition. + # all attributes available in vector layer definition can be defined + + +Well. your vector layer is ready. next step is to create a tile class and a view to serve it. + + +Simple layer tile view +********************** + +.. code-block:: python # in your view file - from django.views.generic import ListView - from vectortiles.postgis.views import MVTView - from yourapp.models import Feature - - - class FeatureTileView(MVTView, ListView): - model = Feature - vector_tile_layer_name = "features" # name for data layer in vector tile - vector_tile_fields = ('name',) # model fields or queryset annotates to include in tile - # vector_tile_content_type = "application/x-protobuf" # if you want to use custom content_type - # vector_tile_queryset = None # define a queryset for your features - # vector_tile_queryset_limit = None # as queryset could not be sliced, set here a limit for your features per tile - # vector_tile_geom_name = "geom" # geom field to consider in qs - # vector_tile_extent = 4096 # tile extent - # vector_tile_buffer = 256 # buffer around tile + from your_app.vector_layers import CityVectorLayer + from your_app.views import MVTView + + + class CityTileView(MVTView): + layer_classes = [CityVectorLayer, CityCentroidVectorLayer] # you can use get_layer_classes method, or directly get_layers instead # in your urls file from django.urls import path from yourapp import views + urlpatterns = [ + ... + views.CityTileView.get_url(), # serve tiles at default /tiles/// + ... + ] + +Multiple layer tile view +************************ + +As vector tile layer permit it, you can embed multiple layers in your tile. +Let's create a second layer. + + +.. code-block:: python + + # in your app models.py + class State(models.Model): + name = models.CharField(max_length=250) + state_code = models.CharField(max_length=10, unique=True) + geom = models.MultiPolygonField(srid=4326) + + # in vector_layers.py file + class StateVectorLayer(VectorLayer): + model = State + id = "states" + tile_fields = ('state_code', "name") + min_zoom = 3 + + # in your view file + class CityAndStateTileView(MVTView): + layer_classes = [CityVectorLayer, StateVectorLayer] + + # in your urls file urlpatterns = [ ... - path('tiles///', views.FeatureTileView.as_view(), name="feature-tile"), + views.CityAndStateTileView.get_url(), # serve tiles at default /tiles/// ... ] -Related model view -****************** +Using TileJSON +************** + +It's a good practice to use tilejson to tell to your map library how to gt your tiles and their defintion. +django-vectortiles permit that. + +TileJSON and tile views share some data, as vactor layers definition. So we need to factorize some things. + +.. code-block:: python + + # in your view file + + class CityAndStateBaseLayer: + # mixin for your two views + layer_classes = [CityVectorLayer, StateVectorLayer] + prefix_url = 'city-and-states' # as tilejson need to known tiles URL, we need to define a url prefix for our tiles + + class CityAndStateTileView(CityAndStateBaseLayer, MVTView): + pass + + + class CityAndStateTileJSON(CityAndStateBaseLayer, TileJSONView): + pass + + + # in your urls file + urlpatterns = [ + ... + views.CityAndStateTileView.get_url(), # serve tiles at /city-and-states/// + views.CityAndStateTileJSON.get_url(), # serve tilejson at /city-and-states/tiles.json + ... + ] + +Now you can use your tiles with a map library like MapLibre or Mapbox GL JS, directly wit hthe tileJSON provided. + +.. warning:: + + By default, it's your browser URL that will be used to generate tile url in tilejson. Take care about django and SSL configuration (django settings, web server headers) if you want to generate an URL with https:// + +.. note:: + + If your application is hosted on server with many workers, and you want to optimized tile loading, you can add several urls in your tilejson file. + + .. code-block:: python + + # add in your settings.py file + ALLOWED_HOSTS = [ + "a.tiles.xxxx", + "b.tiles.xxxx", + "c.tiles.xxxx", + ... + ] + + VECTOR_TILES_URLS = [ + "https://a.tiles.xxxx", + "https://b.tiles.xxxx", + "https://c.tiles.xxxx", + ] + + With these settings, each tilejson file will contain several urls, and your map library will be able to parallel load tiles at time. + + +More complex multiple layer tile view +************************************* + +You can customize geometry data embed in your tiles. + + +.. code-block:: python + + class CityCentroidVectorLayer(VectorLayer): + queryset = City.objects.annotate( + centroid=Centroid("geom"), # compute the city centroïd + area=Area("geom"), # compute the city area + ) + geom_field = "centroid" # use the centroid field as geometry feature + id = "city_centroids" + tile_fields = ('name', 'city_code', 'area', 'population') # add area and population properties in each tile feature + min_zoom = 7 # let's show city name at zoom 7 + + + +Complex related model tile view +******************************* .. code-block:: python @@ -62,9 +196,7 @@ Related model view # in your views.py file from django.views.generic import DetailView - from vectortiles.mixins import BaseVectorTileView - from vectortiles.postgis.views import MVTView - from yourapp.models import Layer + from your_app.models import Layer class LayerTileView(MVTView, DetailView): @@ -84,7 +216,7 @@ Related model view # in your urls file from django.urls import path - from yourapp import views + from your_app import views urlpatterns = [ @@ -103,10 +235,10 @@ Django Rest Framework class FeatureAPIView(BaseVectorTile, APIView): - vector_tile_queryset = Feature.objects.all() - vector_tile_layer_name = "features" - vector_tile_fields = ('name', ) - vector_tile_queryset_limit = 100 + queryset = Feature.objects.all() + id = "features" + tile_fields = ('name', ) + queryset_limit = 100 renderer_classes = (MVTRenderer, ) def get(self, request, *args, **kwargs): @@ -124,9 +256,9 @@ Django Rest Framework class FeatureViewSet(BaseVectorTile, viewsets.ModelViewSet): queryset = Feature.objects.all() - vector_tile_layer_name = "features" - vector_tile_fields = ('name', ) - vector_tile_queryset_limit = 100 + id = "features" + tile_fields = ('name', ) + queryset_limit = 100 @action(detail=False, methods=['get'], renderer_classes=(MVTRenderer, ), url_path='tiles/(?P\d+)/(?P\d+)/(?P\d+)', url_name='tile') diff --git a/vectortiles/mixins.py b/vectortiles/mixins.py index 4bffae0..66430f7 100644 --- a/vectortiles/mixins.py +++ b/vectortiles/mixins.py @@ -1,11 +1,15 @@ from urllib.parse import unquote, urljoin +from django.urls import path +from django.views.defaults import page_not_found + from vectortiles import settings as app_settings class BaseVectorView: layer_classes = None layers = None + prefix_tiles_url = "tiles" def get_layer_classes(self): return self.layer_classes or [] @@ -69,7 +73,7 @@ def get_min_zoom(self): return layers_min_zoom if layers_min_zoom is not None else 0 def get_max_zoom(self): - """Get tilejson manzoom from layers or self.man_zoom""" + """Get tilejson maxzoom from layers or self.max_zoom""" try: # maximum zoom level from layers layers_max_zoom = min(layer.get_max_zoom() for layer in self.get_layers()) @@ -159,3 +163,17 @@ def get_layer_tiles(self, z, x, y): def get_content_status(self, z, x, y): content = self.get_layer_tiles(z, x, y) return (content, 200) if content else (content, 204) + + def get_base_url(self): + pass + + def get_default_url_pattern(self): + return "{z}/{x}/{y}" + + def get_default_url_matrix(self): + pattern = self.get_default_url_pattern() + return f"{pattern.replace('{z}', '').replace('{x}', '').replace('{y}', '')}" + + def get_url(self, prefix=None, url_name=None): + """ Generate URL to serve vector tiles with required parameters""" + return path(f"{prefix or self.prefix_tiles_url}/{self.get_default_url_matrix()}", self.as_view(), name=url_name,) From 79994bd5bbcd32b950e322693a90cf063c9f95aa Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 16 Feb 2024 11:28:31 +0100 Subject: [PATCH 2/6] improve documentation --- README.md | 243 +----------------- docs/development.rst | 26 ++ docs/usage.rst | 163 +++++++++++- .../test_app/templates/index.html | 2 - 4 files changed, 188 insertions(+), 246 deletions(-) create mode 100644 docs/development.rst diff --git a/README.md b/README.md index 3d79602..6e29704 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,6 @@ class CityTileView(MVTView): # in your urls file -from django.urls import path from yourapp import views urlpatterns = [ @@ -136,246 +135,6 @@ urlpatterns = [ ] ``` -#### Use TileJSON and multiple domains: - -```python -# in your view file - -from django.urls import reverse - -from vectortiles.views import MVTView, TileJSONView -from yourapp.vector_layers import CityVectorLayer, CityCentroidVectorLayer - - -class CityTileBaseView: - layer_classes = [CityVectorLayer, CityCentroidVectorLayer] - - -class CityTileView(CityTileBaseView, MVTView): - pass - - -class CityTileJSONView(CityTileBaseView, TileJSONView): - """Simple model TileJSON View""" - - name = "My city dataset" - attribution = "@JEC Data" - description = "My vity dataset" - - -# in your urls file -from django.urls import path -from yourapp import views - -urlpatterns = [ - ... - CityTileView.get_url(), # serve tiles at default /tiles/// - CityTileJSONView.get_url(name="city-tilejson"), # serve tilejson at /tiles.json - ... -] - -# if you want to use multiple domains, you can set the allowed hosts and vector tiles urls in your settings file -# in your settings file -ALLOWED_HOSTS = [ - "a.tiles.xxxx", - "b.tiles.xxxx", - "c.tiles.xxxx", - ... -] - -VECTOR_TILES_URLS = [ - "https://a.tiles.xxxx", - "https://b.tiles.xxxx", - "https://c.tiles.xxxx", -] - -``` - - - Now, any tile requested at http://you_url/tiles/{z}/{x}/{y} that intersects a city will return a vector tile with two layers, `cities` with border geometries and `city_code` property, and `city_centroïds` with center geometry and `city_name` property. -```html - - - - - - - City map - - - - -
- - - - - -``` - -#### Usage with Django Rest Framework - -django-vectortiles can be used with DRF if `renderer_classes` of the view is overridden (see [DRF docs](https://www.django-rest-framework.org/api-guide/renderers/#custom-renderers)). Simply use the right BaseMixin and action on viewsets, or directly a GET method in an APIView. See [documentation](https://django-vectortiles.readthedocs.io/en/latest/usage.html#django-rest-framework) for more details. - -#### Development - -##### With docker and docker-compose - -Copy ```.env.dist``` to ```.env``` and fill ```SECRET_KEY``` and ```POSTGRES_PASSWORD``` - -```bash -docker-compose build -# docker-compose up -docker-compose run /code/venv/bin/python ./manage.py test -``` - -##### Local - -* Install python and django requirements (python 3.8+, django 3.2+) -* Install geodjango requirements -* Have a postgresql / postgis 2.4+ enabled database -* Use a virtualenv - -```bash -pip install .[dev] -U -``` +Read full documentation for examples, as multiple layers, cache policy, mapblibre integration, etc. \ No newline at end of file diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..233d457 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,26 @@ +DEVELOPMENT +=========== + +With docker and docker-compose +****************************** + +Copy ```.env.dist``` to ```.env``` and fill ```SECRET_KEY``` and ```POSTGRES_PASSWORD``` + +.. code-block:: bash + + docker-compose build + # docker-compose up + docker-compose run /code/venv/bin/python ./manage.py test + + +Local +***** + +* Install python and django requirements (python 3.8+, django 3.2+) +* Install geodjango requirements +* Have a postgresql / postgis 2.4+ enabled database +* Use a virtualenv + +.. code-block:: bash + + pip install .[dev] -U diff --git a/docs/usage.rst b/docs/usage.rst index dfc3467..4121dec 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -10,6 +10,7 @@ Then, you need to describe how your data will be embed in tiles. Start by creating vector layers for your data... .. code-block:: python + # in your app models.py from django.contrib.gis.db import models @@ -277,5 +278,163 @@ then use http://your-domain/features/tiles/{z}/{x}/{y}.pbf MapLibre Example **************** -.. literalinclude:: ../test_vectortiles/test_app/templates/index.html - :language: html +.. code-block:: html + + + + + + + + MapBox / MapLibre example + + + + +
+ + + + + + + +Cache policy +************ \ No newline at end of file diff --git a/test_vectortiles/test_app/templates/index.html b/test_vectortiles/test_app/templates/index.html index b9223c5..096cf1c 100644 --- a/test_vectortiles/test_app/templates/index.html +++ b/test_vectortiles/test_app/templates/index.html @@ -13,13 +13,11 @@ } - {# #}
-{##}