Skip to content

Commit

Permalink
Updates from BIMS (#16)
Browse files Browse the repository at this point in the history
* Try to simplify the layers

* Add api to retrive context data

* Add IsAuthenticated

* Update flake8

* Add function to generate pmtiles (#13)

* Add support for pmtiles (#14)

* Add support for pmtiles

* Generate pmtiles in upload task

* update maputnik

---------

Co-authored-by: Danang Massandy <[email protected]>
  • Loading branch information
dimasciput and danangmassandy authored Nov 22, 2024
1 parent 60fecc9 commit 6449ade
Show file tree
Hide file tree
Showing 18 changed files with 712 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
exclude = */docs/*,*/.tox/*,*/.venv/*,.pycharm_helpers/*,*/migrations/*,docs/*,*/__init__.py,scripts/*,deployment/*,django_project/initialize.py
exclude = */tests/*,*/docs/*,*/.tox/*,*/.venv/*,.pycharm_helpers/*,*/migrations/*,docs/*,*/__init__.py,scripts/*,deployment/*,django_project/initialize.py
max-line-length = 79

# E12x continuation line indentation
Expand Down
15 changes: 12 additions & 3 deletions deployment/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@

FROM python:3.12.0-slim-bookworm AS prod

RUN apt-get update -y && \
apt-get install -y --no-install-recommends \
gcc gettext cron \
spatialite-bin libsqlite3-mod-spatialite \
gcc g++ make build-essential gettext cron \
spatialite-bin libsqlite3-mod-spatialite libsqlite3-dev \
python3-dev python3-gdal python3-psycopg2 python3-ldap \
python3-pip python3-pil python3-lxml python3-pylibmc \
uwsgi uwsgi-plugin-python3
uwsgi uwsgi-plugin-python3 \
gdal-bin \
libprotobuf-dev protobuf-compiler zlib1g-dev git && \
echo "Installing Tippecanoe" && \
git clone https://github.com/felt/tippecanoe.git /tmp/tippecanoe && \
cd /tmp/tippecanoe && \
make -j$(nproc) && make install && \
rm -rf /tmp/tippecanoe && \
apt-get clean && rm -rf /var/lib/apt/lists/*

# Install pip packages
ADD deployment/docker/requirements.txt /requirements.txt
Expand Down
14 changes: 13 additions & 1 deletion django_project/cloud_native_gis/admin/layer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# coding=utf-8
"""Cloud Native GIS."""

from django.contrib import admin
from django.utils.safestring import mark_safe

Expand Down Expand Up @@ -28,6 +27,17 @@ def start_upload_data(modeladmin, request, queryset):
import_data.delay(layer.pk)


@admin.action(description='Generate pmtiles')
def generate_pmtiles(modeladmin, request, queryset):
"""Generate pmtiles for layer."""
for layer in queryset:
success, message = layer.generate_pmtiles()
modeladmin.message_user(
request,
message,
level='success' if success else 'error')


@admin.register(Layer)
class LayerAdmin(admin.ModelAdmin):
"""Layer admin."""
Expand All @@ -39,6 +49,8 @@ class LayerAdmin(admin.ModelAdmin):
form = LayerForm
inlines = [LayerAttributeInline]
filter_horizontal = ['styles']
actions = [generate_pmtiles]


def get_form(self, request, *args, **kwargs):
"""Return form."""
Expand Down
97 changes: 97 additions & 0 deletions django_project/cloud_native_gis/api/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# coding=utf-8
"""Cloud Native GIS."""

from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from cloud_native_gis.utils.geometry import (
query_features
)
from cloud_native_gis.models.layer import Layer


class ContextAPIView(APIView):
"""
Context API endpoint for collection queries.
Only accessible to authenticated users.
Validates the query, processes data, and returns results.
"""

permission_classes = [IsAuthenticated]

def get(self, request):
"""Handle GET requests."""
try:
key = request.GET.get('key', None)
attributes = request.GET.get('attr', '')
x = request.GET.get('x', None)
y = request.GET.get('y', None)
if None in [key, x, y]:
raise KeyError('Required request argument ('
'registry, key, x, y) missing.')

srid = request.GET.get('srid', 4326)

x_list = x.split(',')
y_list = y.split(',')

if len(x_list) != len(y_list):
raise ValueError(
'The number of x and y coordinates must be the same')

try:
coordinates = [
(float(x), float(y)) for x, y in zip(x_list, y_list)]
except ValueError:
raise ValueError(
'All x and y values must be valid floats.')

try:
tolerance = float(request.GET.get('tolerance', 10.0))
except ValueError:
raise ValueError('Tolerance should be a float')

registry = request.GET.get('registry', '')
if registry.lower() not in [
'collection', 'service', 'group', 'native']:
raise ValueError('Registry should be "collection", '
'"service" or "group".')

outformat = request.GET.get('outformat', 'geojson').lower()
if outformat not in ['geojson', 'json']:
raise ValueError('Output format should be either '
'json or geojson')

data = []

if registry == 'native':
try:
layer = Layer.objects.get(unique_id=key)
if attributes:
attributes = attributes.split(',')
else:
attributes = layer.attribute_names
data = query_features(
layer.query_table_name,
field_names=attributes,
coordinates=coordinates,
tolerance=tolerance,
srid=srid
)
except Layer.DoesNotExist as e:
return Response(str(e), status=status.HTTP_404_NOT_FOUND)

# Todo : for non native layer
# point = parse_coord(x, y, srid)
# data = Worker(
# registry, key, point, tolerance, outformat).retrieve_all()

return Response(data, status=status.HTTP_200_OK)
except KeyError as e:
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response(
str(e), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
53 changes: 53 additions & 0 deletions django_project/cloud_native_gis/api/pmtile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# coding=utf-8
"""Cloud Native GIS."""
import os
import re

from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404

from cloud_native_gis.models import Layer


def serve_pmtiles(request, layer_uuid):
"""Serve pmtiles."""
layer = get_object_or_404(Layer, unique_id=layer_uuid)

if not layer.pmtile:
raise Http404("PMTile file not found for this layer.")

full_path = layer.pmtile.path

if not os.path.exists(full_path):
raise Http404("PMTile file does not exist.")

range_header = request.headers.get('Range')
if range_header:
range_match = re.match(
r'bytes=(\d+)-(\d*)', range_header)
if range_match:
start_byte = int(range_match.group(1))
end_byte = int(
range_match.group(2)) if (
range_match.group(2)) else (
os.path.getsize(full_path) - 1)

file_size = os.path.getsize(full_path)
content_length = end_byte - start_byte + 1
content_range = f'bytes {start_byte}-{end_byte}/{file_size}'

file = open(full_path, 'rb')
file.seek(start_byte)

response = HttpResponse(
file.read(content_length),
status=206,
content_type='application/octet-stream')
response['Content-Length'] = content_length
response['Content-Range'] = content_range
response['Accept-Ranges'] = 'bytes'
return response

return FileResponse(
open(full_path, 'rb'),
content_type='application/octet-stream')
18 changes: 18 additions & 0 deletions django_project/cloud_native_gis/migrations/0002_layer_pmtile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-10-09 14:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('cloud_native_gis', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='layer',
name='pmtile',
field=models.FileField(blank=True, help_text='Optional PMTile file associated with the layer.', null=True, upload_to='pmtile_files/'),
),
]
Loading

0 comments on commit 6449ade

Please sign in to comment.