diff --git a/ohmg/api/filters.py b/ohmg/api/filters.py index 8042a7ba..36727b1a 100644 --- a/ohmg/api/filters.py +++ b/ohmg/api/filters.py @@ -17,6 +17,13 @@ class FilterSessionSchema(FilterSchema): type: Optional[str] +class FilterLayerSchema(FilterSchema): + username: Optional[str] = Field(q=["created_by__username", "last_updated_by__username"]) + map: Optional[str] = Field(q=["region__document__map_id"]) + category: Optional[str] = Field(q=["layerset2__category_id"]) + transformation: Optional[str] = Field(q="transformation") + + class FilterJobSchema(FilterSchema): operation: Optional[str] = Field(q="operation") stage: Optional[str] = Field(q="stage") diff --git a/ohmg/api/paginators.py b/ohmg/api/paginators.py index 781d8336..eef84acd 100644 --- a/ohmg/api/paginators.py +++ b/ohmg/api/paginators.py @@ -7,11 +7,13 @@ from ohmg.accounts.models import User from ohmg.core.models import ( + LayerSetCategory, Map, ) from ohmg.georeference.models import ( SESSION_TYPES, ) +from ohmg.georeference.utils.gcps import TRANSFORMATION_LOOKUP from ohmg.places.models import Place @@ -66,6 +68,56 @@ def paginate_queryset(self, queryset, pagination: Input, **params): } +class LayerPagination(PaginationBase): + class Input(Schema): + offset: int = 0 + limit: int = 10 + + class Output(Schema): + items: List[Any] + count: int + filter_items: dict + + def paginate_queryset(self, queryset, pagination: Input, **params): + category_items = [] + for c in LayerSetCategory.objects.all(): + if queryset.filter(layerset2__category=c).exists(): + category_items.append({"id": c.pk, "label": c.display_name}) + + transformation_items = [] + for t in TRANSFORMATION_LOOKUP.keys(): + if queryset.filter(transformation=t).exists(): + transformation_items.append({"id": t, "label": t}) + + user_ids = queryset.values_list("last_updated_by", flat=True) + users = get_user_model().objects.filter(pk__in=user_ids) + user_items = natsorted( + [{"id": i.username, "label": i.username} for i in users], + key=lambda k: k["id"], + ) + + map_ids = queryset.values_list("region__document__map", flat=True) + maps = Map.objects.filter(pk__in=map_ids) + map_items = natsorted( + [{"id": i[0], "label": i[1]} for i in maps.values_list("identifier", "title")], + key=lambda k: k["label"], + ) + + filter_items = { + "users": user_items, + "maps": map_items, + "categories": category_items, + "transformations": transformation_items, + } + + offset = pagination.offset + return { + "items": queryset[offset : offset + pagination.limit], + "count": queryset.count(), + "filter_items": filter_items, + } + + class MapPagination(PaginationBase): class Input(Schema): offset: int = 0 diff --git a/ohmg/api/routes.py b/ohmg/api/routes.py index c40a43de..c44a7fe2 100644 --- a/ohmg/api/routes.py +++ b/ohmg/api/routes.py @@ -31,10 +31,12 @@ FilterAllDocumentsSchema, FilterDocumentSchema, FilterJobSchema, + FilterLayerSchema, FilterRegionSchema, FilterSessionSchema, ) from .paginators import ( + LayerPagination, MapPagination, ProfilePagination, SessionPagination, @@ -304,6 +306,24 @@ def layer(request, map: str): return Layer.objects.filter(region__document__map_id=map) +@beta2.get("layers2/", response=List[LayerSchema], url_name="api_layers_list") +@paginate(LayerPagination) +def list_layers(request, filters: FilterLayerSchema = Query(...)): + sort_param = request.GET.get("sortby", "") + sort_dir = request.GET.get("sort", "") + queryset = Layer.objects.all() + if sort_param: + if sort_param == "map": + sort_param = "region__document__map__title" + sort_arg = sort_param if sort_dir == "asc" else f"-{sort_param}" + queryset = queryset.order_by(sort_arg).select_related("region") + else: + queryset = queryset.order_by("-pk").select_related("region") + + queryset = filters.filter(queryset) + return queryset + + ## SESSION LOCKS @beta2.get("session-locks/", response=List[SessionLockSchema], url_name="session_locks") def session_locks(request, map: str = None): diff --git a/ohmg/api/schemas.py b/ohmg/api/schemas.py index 8a10676a..0f09ca51 100644 --- a/ohmg/api/schemas.py +++ b/ohmg/api/schemas.py @@ -334,6 +334,15 @@ class LayerSchema(Schema): gcps_geojson: Optional[dict] urls: dict extent: Optional[list] + rmse: Optional[float] + skew: Optional[float] + skew_norm: Optional[float] + anisotropy: Optional[float] + anisotropy_norm: Optional[float] + gcp_count: Optional[int] + transformation: Optional[str] + map: MapSchemaLite + category: Optional[str] @staticmethod def resolve_urls(obj: Layer): @@ -372,6 +381,14 @@ def resolve_created_by(obj: Layer): def resolve_last_updated_by(obj: Layer): return obj.last_updated_by.username if obj.last_updated_by else "" + @staticmethod + def resolve_map(obj: Layer): + return obj.region.document.map + + @staticmethod + def resolve_category(obj: Layer): + return obj.layerset2.category.display_name if obj.layerset2 else None + class LayerFullSchema(Schema): id: int diff --git a/ohmg/core/migrations/0016_layer_anisotropy_layer_gcp_count_layer_rmse_and_more.py b/ohmg/core/migrations/0016_layer_anisotropy_layer_anisotropy_norm_and_more.py similarity index 73% rename from ohmg/core/migrations/0016_layer_anisotropy_layer_gcp_count_layer_rmse_and_more.py rename to ohmg/core/migrations/0016_layer_anisotropy_layer_anisotropy_norm_and_more.py index cb13c468..1e14774b 100644 --- a/ohmg/core/migrations/0016_layer_anisotropy_layer_gcp_count_layer_rmse_and_more.py +++ b/ohmg/core/migrations/0016_layer_anisotropy_layer_anisotropy_norm_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-09-24 14:57 +# Generated by Django 4.2.27 on 2026-09-28 21:43 from django.db import migrations, models @@ -15,6 +15,11 @@ class Migration(migrations.Migration): name='anisotropy', field=models.FloatField(blank=True, null=True), ), + migrations.AddField( + model_name='layer', + name='anisotropy_norm', + field=models.FloatField(blank=True, null=True), + ), migrations.AddField( model_name='layer', name='gcp_count', @@ -30,6 +35,11 @@ class Migration(migrations.Migration): name='skew', field=models.FloatField(blank=True, null=True), ), + migrations.AddField( + model_name='layer', + name='skew_norm', + field=models.FloatField(blank=True, null=True), + ), migrations.AddField( model_name='layer', name='transformation', diff --git a/ohmg/core/models/layer.py b/ohmg/core/models/layer.py index c7ef4f26..d2c58e2f 100644 --- a/ohmg/core/models/layer.py +++ b/ohmg/core/models/layer.py @@ -86,7 +86,9 @@ class Meta: transformation = models.CharField(null=True, blank=True) rmse = models.FloatField("RMSE", null=True, blank=True) skew = models.FloatField(null=True, blank=True) + skew_norm = models.FloatField(null=True, blank=True) anisotropy = models.FloatField(null=True, blank=True) + anisotropy_norm = models.FloatField(null=True, blank=True) def __str__(self): return self.title @@ -137,13 +139,26 @@ def update_georeferencing_measures(self): if gcp_group: rmse, _, _, skew, aniso = gcp_group.get_georeferencer().get_measures() self.rmse = rmse - self.skew = skew - self.anisotropy = aniso + if skew is not None: + self.skew = skew + self.skew_norm = abs(skew) + if aniso is not None: + self.anisotropy = aniso + self.anisotropy_norm = aniso if aniso >= 1 or aniso == 0 else round(1 / aniso, 3) self.transformation = gcp_group.transformation self.gcp_count = gcp_group.gcps.count() self.save( skip_map_lookup_update=True, - update_fields=["rmse", "skew", "anisotropy", "transformation", "gcp_count"], + set_extent=False, + update_fields=[ + "rmse", + "skew", + "skew_norm", + "anisotropy", + "anisotropy_norm", + "transformation", + "gcp_count", + ], ) def create_xyz_url(self) -> Union[str, None]: diff --git a/ohmg/core/urls.py b/ohmg/core/urls.py index 127d894c..5742012d 100644 --- a/ohmg/core/urls.py +++ b/ohmg/core/urls.py @@ -2,6 +2,7 @@ from .views import ( DocumentView, + LayerListView, LayersetDerivativeView, LayerSetView, LayerView, @@ -38,6 +39,7 @@ kwargs={"resource": "layer"}, name="layer_derivative", ), + path("layers/", LayerListView.as_view(), name="layer_list"), path("layerset/", LayerSetView.as_view(), name="layerset_view"), path("layerset/", LayerSetView.as_view(), name="layerset_view"), ] diff --git a/ohmg/core/views.py b/ohmg/core/views.py index fe6fee49..f6f17136 100644 --- a/ohmg/core/views.py +++ b/ohmg/core/views.py @@ -76,6 +76,18 @@ def get(self, request): return render(request, "core/maps.html", context=context_dict) +class LayerListView(View): + def get(self, request): + if not request.user.is_staff: + raise Http404 + context_dict = { + "LAYERS_PARAMS": { + "CONTEXT": generate_ohmg_context(request), + } + } + return render(request, "core/layers.html", context=context_dict) + + class MapView(View): @time_this_function def get(self, request, identifier): diff --git a/ohmg/frontend/svelte_components/rollup.config.js b/ohmg/frontend/svelte_components/rollup.config.js index 85378337..073aa443 100644 --- a/ohmg/frontend/svelte_components/rollup.config.js +++ b/ohmg/frontend/svelte_components/rollup.config.js @@ -75,6 +75,7 @@ export default (cliArgs) => { 'Map', 'Sessions', 'Maps', + 'Layers', 'Jobs', 'Profiles', 'LatestBlogPosts', diff --git a/ohmg/frontend/svelte_components/src/components/interfaces/Georeferencer.svelte b/ohmg/frontend/svelte_components/src/components/interfaces/Georeferencer.svelte index 49a850fd..ff038729 100644 --- a/ohmg/frontend/svelte_components/src/components/interfaces/Georeferencer.svelte +++ b/ohmg/frontend/svelte_components/src/components/interfaces/Georeferencer.svelte @@ -1168,28 +1168,31 @@