Skip to content

Commit 4e0ee68

Browse files
committed
FMVLayer endpoints
1 parent 542664c commit 4e0ee68

7 files changed

Lines changed: 159 additions & 11 deletions

File tree

docker-compose.override.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ services:
1616
- postgres
1717
- rabbitmq
1818
- minio
19+
platform: 'linux/amd64'
1920

2021
celery:
2122
build:

uvdat/core/models/map_layers.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,18 +136,21 @@ def get_ground_frame_mapping(self) -> dict:
136136
geom = feature.geometry
137137

138138
if geom.geom_type == "Polygon":
139-
coords = list(geom.exterior.coords)
140-
if len(coords) >= 4:
141-
# Return first 4 unique corners, not repeating the closing point
142-
result[frame_id] = coords[:4]
139+
# Access first ring (exterior) without using .exterior
140+
if len(geom) > 0:
141+
coords = list(geom[0].coords)
142+
if len(coords) >= 4:
143+
result[frame_id] = coords[:4]
144+
143145
elif geom.geom_type == "MultiPolygon":
144-
# Pick the largest polygon by area, then return its first 4 corners
145-
largest = max(geom, key=lambda p: p.area)
146-
coords = list(largest.exterior.coords)
147-
if len(coords) >= 4:
148-
result[frame_id] = coords[:4]
146+
# Find largest polygon and access its first ring
147+
largest = max(geom.geoms, key=lambda p: p.area)
148+
if len(largest) > 0:
149+
coords = list(largest[0].coords)
150+
if len(coords) >= 4:
151+
result[frame_id] = coords[:4]
152+
149153
else:
150-
# Optional: log or skip unexpected geometries
151154
continue
152155

153156
return result

uvdat/core/rest/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .tasks import TasksAPIView
1616
from .user import UserViewSet
1717
from .vector_feature_table_data import VectorFeatureTableDataViewSet
18+
from .fmv import FMVLayerViewSet
1819

1920
__all__ = [
2021
ContextViewSet,
@@ -40,4 +41,5 @@
4041
TasksAPIView,
4142
MetadataFilterViewSet,
4243
DisplayConfigurationViewSet,
44+
FMVLayerViewSet,
4345
]

uvdat/core/rest/fmv.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from django.db import connection
2+
from django.http import HttpResponse
3+
from rest_framework.decorators import action
4+
from rest_framework.response import Response
5+
from rest_framework.viewsets import ViewSet
6+
from django.core.files.storage import default_storage
7+
8+
from uvdat.core.models import FMVLayer
9+
10+
FMV_TILE_SQL = """
11+
WITH tile_bounds AS (
12+
SELECT ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326) AS te
13+
),
14+
tilenvbounds as (
15+
SELECT
16+
ST_XMin(te) as xmin,
17+
ST_YMin(te) as ymin,
18+
ST_XMax(te) as xmax,
19+
ST_YMax(te) as ymax,
20+
(ST_XMax(te) - ST_XMin(te)) / 4 as segsize
21+
FROM tile_bounds
22+
),
23+
env as (
24+
SELECT ST_Segmentize(
25+
ST_MakeEnvelope(
26+
xmin,
27+
ymin,
28+
xmax,
29+
ymax,
30+
4326
31+
),
32+
segsize
33+
) as seg
34+
FROM tilenvbounds
35+
),
36+
bounds as (
37+
SELECT
38+
seg as geom,
39+
seg::box2d as b2d
40+
FROM env
41+
),
42+
vector_features AS (
43+
SELECT
44+
ST_AsMVTGeom(
45+
ST_Transform(geometry, 3857),
46+
ST_Transform((SELECT geom from bounds), 3857)
47+
) as geom,
48+
map_layer_id,
49+
id as fmvvectorfeatureid,
50+
properties
51+
FROM core_fmvvectorfeature
52+
WHERE ST_Intersects(geometry, (SELECT geom from bounds))
53+
AND map_layer_id = %(map_layer_id)s
54+
)
55+
SELECT ST_AsMVT(vector_features.*) AS mvt FROM vector_features;
56+
"""
57+
58+
class FMVLayerViewSet(ViewSet):
59+
"""
60+
ViewSet for accessing FMVLayer data and tiles.
61+
"""
62+
def retrieve(self, request, pk=None):
63+
try:
64+
layer = FMVLayer.objects.get(pk=pk)
65+
except FMVLayer.DoesNotExist:
66+
return Response({"detail": "Not found."}, status=404)
67+
68+
presigned_url = None
69+
if layer.fmv_video and hasattr(layer.fmv_video, 'name'):
70+
presigned_url = default_storage.url(layer.fmv_video.name)
71+
else:
72+
presigned_url = None
73+
data = {
74+
"id": layer.id,
75+
"name": layer.name,
76+
"bbox": list(layer.bounds.extent) if layer.bounds else None,
77+
"frameId_to_bbox": layer.get_ground_frame_mapping(),
78+
"fmv_video_url": presigned_url
79+
}
80+
return Response(data)
81+
82+
@action(
83+
detail=True,
84+
methods=["get"],
85+
url_path=r'tiles/(?P<z>\d+)/(?P<x>\d+)/(?P<y>\d+)',
86+
url_name='fmv_tiles',
87+
)
88+
def get_vector_tile(self, request, pk=None, x=None, y=None, z=None):
89+
with connection.cursor() as cursor:
90+
cursor.execute(
91+
FMV_TILE_SQL,
92+
{
93+
'z': z,
94+
'x': x,
95+
'y': y,
96+
'map_layer_id': pk,
97+
},
98+
)
99+
row = cursor.fetchone()
100+
101+
tile = row[0]
102+
return HttpResponse(
103+
tile,
104+
content_type='application/octet-stream',
105+
status=200 if tile else 204,
106+
)

uvdat/core/rest/map_layers.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
RasterMapLayer,
2222
VectorFeature,
2323
VectorMapLayer,
24+
FMVLayer,
25+
FMVVectorFeature,
2426
)
2527
from uvdat.core.rest.serializers import (
2628
AbstractMapLayerSerializer,
2729
NetCDFLayerSerializer,
2830
RasterMapLayerSerializer,
2931
VectorMapLayerDetailSerializer,
3032
VectorMapLayerSerializer,
33+
FMVLayerSerializer,
3134
)
3235

3336
from .permissions import DefaultPermission
@@ -466,7 +469,8 @@ def create(self, request, *args, **kwargs):
466469
raster_layer = RasterMapLayer.objects.filter(id=layer_id).first()
467470
vector_layer = VectorMapLayer.objects.filter(id=layer_id).first()
468471
netcdf_layer = NetCDFLayer.objects.filter(id=layer_id).first()
469-
map_layer = raster_layer or vector_layer or netcdf_layer
472+
fmv_layer = FMVLayer.objects.filter(id=layer_id).first()
473+
map_layer = raster_layer or vector_layer or netcdf_layer or fmv_layer
470474

471475
if map_layer is None:
472476
continue # Skip if no layer is found for the provided ID
@@ -478,6 +482,8 @@ def create(self, request, *args, **kwargs):
478482
serializer = VectorMapLayerSerializer(map_layer)
479483
elif isinstance(map_layer, NetCDFLayer):
480484
serializer = NetCDFLayerSerializer(map_layer)
485+
elif isinstance(map_layer, FMVLayer):
486+
serializer = FMVLayerSerializer(map_layer)
481487
# Get the serialized data
482488
layer_response = serializer.data
483489
if raster_layer:
@@ -486,6 +492,8 @@ def create(self, request, *args, **kwargs):
486492
layer_response['type'] = 'vector'
487493
elif netcdf_layer:
488494
layer_response['type'] = 'netcdf'
495+
elif fmv_layer:
496+
layer_response['type'] = 'fmv'
489497

490498
# Check for LayerRepresentation if provided
491499
if layer_representation_id is not None:
@@ -525,12 +533,14 @@ def list_all_map_layers(self, request, *args, **kwargs):
525533
raster_layers = RasterMapLayer.objects.all()
526534
vector_layers = VectorMapLayer.objects.all()
527535
netcdf_layers = NetCDFLayer.objects.all()
536+
fmv_layers = FMVLayer.objects.all()
528537

529538
# Serialize layers
530539
for map_layer, _serializer, layer_type in [
531540
(raster_layers, RasterMapLayerSerializer, 'raster'),
532541
(vector_layers, VectorMapLayerSerializer, 'vector'),
533542
(netcdf_layers, NetCDFLayerSerializer, 'netcdf'),
543+
(fmv_layers, FMVLayer, 'fmv'),
534544
]:
535545
for layer in map_layer:
536546
serializer = AbstractMapLayerSerializer(layer)
@@ -577,6 +587,9 @@ def list(self, request, *args, **kwargs):
577587
if not map_layer and 'netcdf' == layer_type:
578588
map_layer = NetCDFLayer.objects.filter(id=layer_id).first()
579589
serializer_class = NetCDFLayerSerializer
590+
if not map_layer and 'fmv' == layer_type:
591+
map_layer = FMVLayer.objects.filter(id=layer_id).first()
592+
serializer_class = FMVLayerSerializer
580593

581594
if not map_layer:
582595
continue # Skip if no matching layer is found
@@ -619,6 +632,7 @@ def map_layer_bbox(self, request, *args, **kwargs):
619632
raster_map_layer_ids = request.query_params.getlist('rasterMapLayerIds')
620633
vector_map_layer_ids = request.query_params.getlist('vectorMapLayerIds')
621634
netcdf_map_layer_ids = request.query_params.getlist('netCDFMapLayerIds')
635+
fmv_map_layer_ids = request.query_params.getlist('fmvMapLayerIds')
622636

623637
# Initialize variables to track the overall bounding box
624638
overall_bbox = {
@@ -663,6 +677,17 @@ def map_layer_bbox(self, request, *args, **kwargs):
663677
overall_bbox['ymin'] = min(overall_bbox['ymin'], netcdf_bbox[1])
664678
overall_bbox['xmax'] = max(overall_bbox['xmax'], netcdf_bbox[2])
665679
overall_bbox['ymax'] = max(overall_bbox['ymax'], netcdf_bbox[3])
680+
if fmv_map_layer_ids:
681+
fmv_bboxes = FMVVectorFeature.objects.filter(
682+
map_layer_id__in=fmv_map_layer_ids
683+
).aggregate(extent=Extent('geometry'))['extent']
684+
685+
if vector_bboxes:
686+
overall_bbox['xmin'] = min(overall_bbox['xmin'], vector_bboxes[0])
687+
overall_bbox['ymin'] = min(overall_bbox['ymin'], vector_bboxes[1])
688+
overall_bbox['xmax'] = max(overall_bbox['xmax'], vector_bboxes[2])
689+
overall_bbox['ymax'] = max(overall_bbox['ymax'], vector_bboxes[3])
690+
666691
# Check if the bbox values were updated; if not, return an error message
667692
if overall_bbox['xmin'] == float('inf'):
668693
return JsonResponse(
@@ -697,6 +722,8 @@ def update_name(self, request, *args, **kwargs):
697722
RasterMapLayer.objects.filter(id=layer_id).update(name=new_name)
698723
elif layer_type == 'netcdf':
699724
NetCDFData.objects.filter(id=layer_id).update(name=new_name)
725+
elif layer_type == 'fmv':
726+
FMVLayer.objects.filter(id=layer_id).update(name=new_name)
700727
else:
701728
return Response(
702729
{'error': 'Invalid layer type. Must be "vector" or "raster".'},

uvdat/core/rest/serializers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
VectorFeatureRowData,
2525
VectorFeatureTableData,
2626
VectorMapLayer,
27+
FMVLayer,
28+
FMVVectorFeature,
2729
)
2830

2931

@@ -187,6 +189,11 @@ class Meta:
187189
model = VectorMapLayer
188190
exclude = ['geojson_file']
189191

192+
class FMVLayerSerializer(serializers.ModelSerializer, AbstractMapLayerSerializer):
193+
class Meta:
194+
model = FMVLayer
195+
exclude = ['geojson_file']
196+
190197

191198
class NetCDFLayerSerializer(serializers.ModelSerializer):
192199
bounds = serializers.SerializerMethodField()

uvdat/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
UserViewSet,
2929
VectorFeatureTableDataViewSet,
3030
VectorMapLayerViewSet,
31+
FMVLayerViewSet,
3132
)
3233

3334
router = routers.SimpleRouter()
@@ -59,6 +60,7 @@
5960
router.register(r'layer-collections', LayerCollectionViewSet, basename='layer-collections')
6061
router.register(r'map-layers', MapLayerViewSet, basename='map-layers')
6162
router.register(r'netcdf', NetCDFDataView, basename='netcdf')
63+
router.register(r'fmv-layer', FMVLayerViewSet, basename='fmv-layer')
6264
router.register(r'processing-tasks', ProcessingTaskView, basename='processing-tasks')
6365
router.register(r'users', UserViewSet, basename='users')
6466
router.register(r'tasks', TasksAPIView, basename='tasks')

0 commit comments

Comments
 (0)