From e28a322db38916e7cc6e893b98e0e0bc07860f39 Mon Sep 17 00:00:00 2001 From: Jacob Nesbitt Date: Tue, 14 Nov 2023 17:17:10 -0500 Subject: [PATCH] [WIP] Retrieve map layers from dataset detail endpoint --- uvdat/core/models/dataset.py | 63 ++++++++++++++----- uvdat/core/rest/dataset.py | 17 +++++ uvdat/core/rest/map_layers.py | 15 ++++- uvdat/core/rest/serializers.py | 77 +++++++++++++---------- web/src/components/MainDrawerContents.vue | 21 +++++-- web/src/components/map/ActiveLayers.vue | 2 +- web/src/layers.ts | 48 ++++++++------ web/src/types.ts | 12 ++-- 8 files changed, 171 insertions(+), 84 deletions(-) diff --git a/uvdat/core/models/dataset.py b/uvdat/core/models/dataset.py index c4a26b47..8cdf1239 100644 --- a/uvdat/core/models/dataset.py +++ b/uvdat/core/models/dataset.py @@ -80,23 +80,52 @@ def get_network_gcc(self, exclude_nodes): return get_dataset_network_gcc(self, exclude_nodes) def get_map_layers(self): + """Returns a queryset of either RasterMapLayer, or VectorMapLayer.""" from uvdat.core.models import RasterMapLayer, VectorMapLayer - ret = [] - for vector_map_layer in VectorMapLayer.objects.filter(file_item__dataset=self): - ret.append( - { - 'id': vector_map_layer.id, - 'index': vector_map_layer.index, - 'type': 'vector', - } - ) - for raster_map_layer in RasterMapLayer.objects.filter(file_item__dataset=self): - ret.append( - { - 'id': raster_map_layer.id, - 'index': raster_map_layer.index, - 'type': 'raster', - } + if self.dataset_type == self.DatasetType.RASTER: + return RasterMapLayer.objects.filter(file_item__dataset=self) + if self.dataset_type == self.DatasetType.VECTOR: + return VectorMapLayer.objects.filter(file_item__dataset=self) + + raise NotImplementedError(f"Dataset Type {self.dataset_type}") + + def get_map_layer_tile_extents(self): + """ + Return the extents of all vector map layers of this dataset. + + Returns `None` if the dataset is not a vector dataset. + """ + if self.dataset_type != self.DatasetType.VECTOR: + return None + + from uvdat.core.models import VectorMapLayer, VectorTile + + # Retrieve all layers + layer_ids = VectorMapLayer.objects.filter(file_item__dataset=self).values_list( + 'id', flat=True + ) + + # Return x/y extents by layer id and z depth + vals = ( + VectorTile.objects.filter(map_layer_id__in=layer_ids) + .values('map_layer_id', 'z') + .annotate( + min_x=models.Min('x'), + min_y=models.Min('y'), + max_x=models.Max('x'), + max_y=models.Max('y'), ) - return ret + .order_by('map_layer_id') + ) + + # Deconstruct query into response format + layers = {} + for entry in vals: + map_layer_id = entry.pop('map_layer_id') + if map_layer_id not in layers: + layers[map_layer_id] = {} + + layers[map_layer_id][entry.pop('z')] = entry + + return layers diff --git a/uvdat/core/rest/dataset.py b/uvdat/core/rest/dataset.py index 85e654c4..40f24262 100644 --- a/uvdat/core/rest/dataset.py +++ b/uvdat/core/rest/dataset.py @@ -2,6 +2,7 @@ from django.http import HttpResponse from rest_framework.decorators import action +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from uvdat.core.models import Dataset @@ -19,6 +20,22 @@ def get_queryset(self): else: return Dataset.objects.all() + def retrieve(self, request, *args, **kwargs): + dataset: Dataset = self.get_object() + + # Get map layers + dataset.map_layers = list(dataset.get_map_layers().select_related('file_item__dataset')) + + # Set vector tile extents if vector dataset + if dataset.dataset_type == Dataset.DatasetType.VECTOR: + extents = dataset.get_map_layer_tile_extents() + for layer in dataset.map_layers: + layer.tile_extents = extents.pop(layer.id) + + # Serialize and return + serializer = uvdat_serializers.DatasetDetailSerializer(dataset) + return Response(serializer.data, status=200) + @action(detail=True, methods=['get']) def convert(self, request, **kwargs): dataset = self.get_object() diff --git a/uvdat/core/rest/map_layers.py b/uvdat/core/rest/map_layers.py index 46b877a7..9eb27961 100644 --- a/uvdat/core/rest/map_layers.py +++ b/uvdat/core/rest/map_layers.py @@ -8,11 +8,15 @@ from uvdat.core.models import RasterMapLayer, VectorMapLayer from uvdat.core.models.map_layers import VectorTile -from uvdat.core.rest.serializers import RasterMapLayerSerializer, VectorMapLayerSerializer +from uvdat.core.rest.serializers import ( + RasterMapLayerSerializer, + VectorMapLayerDetailSerializer, + VectorMapLayerSerializer, +) class RasterMapLayerViewSet(ModelViewSet, LargeImageFileDetailMixin): - queryset = RasterMapLayer.objects.all() + queryset = RasterMapLayer.objects.select_related('file_item__dataset').all() serializer_class = RasterMapLayerSerializer FILE_FIELD_NAME = 'cloud_optimized_geotiff' @@ -29,9 +33,14 @@ def get_raster_data(self, request, resolution: str = '1', **kwargs): class VectorMapLayerViewSet(ModelViewSet): - queryset = VectorMapLayer.objects.all() + queryset = VectorMapLayer.objects.select_related('file_item__dataset').all() serializer_class = VectorMapLayerSerializer + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = VectorMapLayerDetailSerializer(instance) + return Response(serializer.data) + @action( detail=True, methods=['get'], diff --git a/uvdat/core/rest/serializers.py b/uvdat/core/rest/serializers.py index 2fb1f65a..9c69ceaa 100644 --- a/uvdat/core/rest/serializers.py +++ b/uvdat/core/rest/serializers.py @@ -32,10 +32,30 @@ class Meta: class DatasetSerializer(serializers.ModelSerializer): - map_layers = serializers.SerializerMethodField('get_map_layers') + class Meta: + model = Dataset + fields = '__all__' - def get_map_layers(self, obj): - return obj.get_map_layers() + +class DatasetDetailSerializer(serializers.ModelSerializer): + map_layers = serializers.SerializerMethodField() + + def get_map_layers(self, obj: Dataset): + map_layers = getattr(obj, 'map_layers', None) + if map_layers is None: + raise Exception(f'"map_layer" attribute not found on Dataset {obj.id}') + + # Set serializer class + sub_serializer_cls = None + if obj.dataset_type == Dataset.DatasetType.VECTOR: + sub_serializer_cls = DatasetVectorMapLayerSerializer + if obj.dataset_type == Dataset.DatasetType.RASTER: + sub_serializer_cls = RasterMapLayerSerializer + if sub_serializer_cls is None: + raise NotImplementedError(f'Dataset Type {obj.dataset_type}') + + serializer = sub_serializer_cls(map_layers, many=True) + return serializer.data class Meta: model = Dataset @@ -54,26 +74,28 @@ class Meta: fields = '__all__' -class RasterMapLayerSerializer(serializers.ModelSerializer): +class AbstractMapLayerSerializer(serializers.Serializer): name = serializers.SerializerMethodField('get_name') type = serializers.SerializerMethodField('get_type') dataset_id = serializers.SerializerMethodField('get_dataset_id') file_item = serializers.SerializerMethodField('get_file_item') - def get_name(self, obj): + def get_name(self, obj: VectorMapLayer | RasterMapLayer): if obj.file_item is None: return None return obj.file_item.name - def get_type(self, obj): + def get_type(self, obj: VectorMapLayer | RasterMapLayer): + if isinstance(obj, VectorMapLayer): + return 'vector' return 'raster' - def get_dataset_id(self, obj): + def get_dataset_id(self, obj: VectorMapLayer | RasterMapLayer): if obj.file_item and obj.file_item.dataset: return obj.file_item.dataset.id return None - def get_file_item(self, obj): + def get_file_item(self, obj: VectorMapLayer | RasterMapLayer): if obj.file_item is None: return None return { @@ -81,39 +103,30 @@ def get_file_item(self, obj): 'name': obj.file_item.name, } + +class RasterMapLayerSerializer(serializers.ModelSerializer, AbstractMapLayerSerializer): class Meta: model = RasterMapLayer fields = '__all__' -class VectorMapLayerSerializer(serializers.ModelSerializer): - name = serializers.SerializerMethodField('get_name') - type = serializers.SerializerMethodField('get_type') - dataset_id = serializers.SerializerMethodField('get_dataset_id') - file_item = serializers.SerializerMethodField('get_file_item') - derived_region_id = serializers.SerializerMethodField('get_derived_region_id') - tile_extents = serializers.SerializerMethodField('get_tile_extents') +class DatasetVectorMapLayerSerializer(serializers.ModelSerializer, AbstractMapLayerSerializer): + tile_extents = serializers.JSONField() - def get_name(self, obj): - if obj.file_item is None: - return None - return obj.file_item.name + class Meta: + model = VectorMapLayer + exclude = ['geojson_file'] - def get_type(self, obj): - return 'vector' - def get_dataset_id(self, obj): - if obj.file_item and obj.file_item.dataset: - return obj.file_item.dataset.id - return None +class VectorMapLayerSerializer(serializers.ModelSerializer, AbstractMapLayerSerializer): + class Meta: + model = VectorMapLayer + exclude = ['geojson_file'] - def get_file_item(self, obj): - if obj.file_item is None: - return None - return { - 'id': obj.file_item.id, - 'name': obj.file_item.name, - } + +class VectorMapLayerDetailSerializer(serializers.ModelSerializer, AbstractMapLayerSerializer): + derived_region_id = serializers.SerializerMethodField('get_derived_region_id') + tile_extents = serializers.SerializerMethodField('get_tile_extents') def get_derived_region_id(self, obj): dr = obj.derivedregion_set.first() diff --git a/web/src/components/MainDrawerContents.vue b/web/src/components/MainDrawerContents.vue index 1cb576f0..9e495140 100644 --- a/web/src/components/MainDrawerContents.vue +++ b/web/src/components/MainDrawerContents.vue @@ -17,10 +17,12 @@ import { loadDerivedRegions, } from "../storeFunctions"; import { Dataset, DerivedRegion } from "@/types"; +import { getDataset } from "@/api/rest"; import { getMapLayerForDataObject, isMapLayerVisible, toggleMapLayer, + createOpenLayer, } from "@/layers"; export default { @@ -72,11 +74,20 @@ export default { ) { currentDataset.value = undefined; } - // Find related MapLayer at current index - const mapLayer = await getMapLayerForDataObject( - dataset, - dataset.current_layer_index - ); + + // Ensure layer index is set + dataset.current_layer_index = dataset.current_layer_index || 0; + if (dataset.map_layers === undefined) { + dataset.map_layers = (await getDataset(dataset.id)).map_layers; + } + + // Use non null assertions since we know from above that both are set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mapLayer = dataset.map_layers![dataset.current_layer_index!]; + if (mapLayer.openlayer === undefined) { + mapLayer.openlayer = createOpenLayer(mapLayer); + } + toggleMapLayer(mapLayer); } diff --git a/web/src/components/map/ActiveLayers.vue b/web/src/components/map/ActiveLayers.vue index 899a5843..fbbf2c3b 100644 --- a/web/src/components/map/ActiveLayers.vue +++ b/web/src/components/map/ActiveLayers.vue @@ -23,7 +23,7 @@ export default { getDataObjectForMapLayer(mapLayer); if (dataObject) { let ret = dataObject.name; - if (dataObject.map_layers.length > 1) { + if (dataObject.map_layers && dataObject.map_layers.length > 1) { ret += ` (Layer ${mapLayer.index})`; } return ret; diff --git a/web/src/layers.ts b/web/src/layers.ts index 8051fc6e..adc435d8 100644 --- a/web/src/layers.ts +++ b/web/src/layers.ts @@ -35,6 +35,32 @@ import CircleStyle from "ol/style/Circle"; const _mapLayerManager = ref<(VectorMapLayer | RasterMapLayer)[]>([]); +export function createOpenLayer(mapLayer: AbstractMapLayer) { + let openLayer: VectorTileLayer | TileLayer; + + if (isVectorMapLayer(mapLayer)) { + openLayer = createVectorOpenLayer(mapLayer); + } else if (isRasterMapLayer(mapLayer)) { + openLayer = createRasterOpenLayer(mapLayer); + styleRasterOpenLayer(mapLayer.openlayer, {}); + cacheRasterData(mapLayer.id); + } else { + throw new Error("Unsupported map layer type."); + } + + openLayer.setZIndex(selectedMapLayers.value.length); + + // Check for existing layer + const existingLayer = _mapLayerManager.value.find( + (l) => l.id === mapLayer.id && l.type === mapLayer.type + ); + if (!existingLayer) { + _mapLayerManager.value.push(mapLayer); + } + + return openLayer; +} + export async function getOrCreateLayerFromID( mapLayerId: number | undefined, mapLayerType: string | undefined @@ -43,28 +69,14 @@ export async function getOrCreateLayerFromID( throw new Error(`Could not get or create openLayer for undefined layer`); } - let cachedMapLayer = _mapLayerManager.value.find((l) => { + const cachedMapLayer = _mapLayerManager.value.find((l) => { return l.id === mapLayerId && l.type === mapLayerType; }); if (cachedMapLayer) return cachedMapLayer; const mapLayer = await getMapLayer(mapLayerId, mapLayerType); - if (isVectorMapLayer(mapLayer)) { - mapLayer.openlayer = createVectorOpenLayer(mapLayer); - } else if (isRasterMapLayer(mapLayer)) { - mapLayer.openlayer = createRasterOpenLayer(mapLayer); - styleRasterOpenLayer(mapLayer.openlayer, {}); - cacheRasterData(mapLayerId); - } - mapLayer.openlayer.setZIndex(selectedMapLayers.value.length); + mapLayer.openlayer = createOpenLayer(mapLayer); - // since this is an async context, check again for existing layer before pushing. - cachedMapLayer = _mapLayerManager.value.find((l) => { - return l.id === mapLayerId && l.type === mapLayerType; - }); - if (!cachedMapLayer) { - _mapLayerManager.value.push(mapLayer); - } return mapLayer; } @@ -292,7 +304,7 @@ export function getDataObjectForMapLayer( ); if (dataset) { dataset.current_layer_index = - dataset?.map_layers.find(({ id }) => id === mapLayer.id)?.index || 0; + dataset?.map_layers?.find(({ id }) => id === mapLayer.id)?.index || 0; } return dataset; } @@ -308,7 +320,7 @@ export async function getMapLayerForDataObject( if (dataObject === undefined) { throw new Error(`Could not get map layer for undefined data object`); } - const mapLayer = dataObject.map_layers.find( + const mapLayer = dataObject.map_layers?.find( ({ index }) => index === layerIndex ); if (!mapLayer) { diff --git a/web/src/types.ts b/web/src/types.ts index a0c7d9a2..45f3b9f9 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -7,13 +7,9 @@ export interface Dataset { modified: string; processing: boolean; metadata: object; - dataset_type: string; - map_layers: { - id: number; - index: number; - type: string; - }[]; - current_layer_index: number; + dataset_type: "vector" | "raster"; + map_layers?: AbstractMapLayer[]; + current_layer_index?: number; classification: "Network" | "Region" | "Other"; network: { nodes: NetworkNode[]; @@ -97,7 +93,7 @@ export interface AbstractMapLayer { }; default_style?: object; index: number; - type: string; + type: "vector" | "raster"; dataset_id?: number; derived_region_id?: number;