Skip to content

Commit c14fd24

Browse files
authored
Merge pull request #361 from ohmg-dev/316_deprecate_multimask
316 deprecate multimask field on LayerSets
2 parents c79448a + f308656 commit c14fd24

12 files changed

Lines changed: 109 additions & 192 deletions

File tree

‎ohmg/api/schemas.py‎

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ class LayerSchema(Schema):
327327
extent: Optional[list]
328328

329329
@staticmethod
330-
def resolve_urls(obj):
330+
def resolve_urls(obj: Layer):
331331
return {
332332
"resource": f"/layer/{obj.pk}",
333333
"thumbnail": obj.thumbnail.url if obj.thumbnail else "",
@@ -336,21 +336,15 @@ def resolve_urls(obj):
336336
}
337337

338338
@staticmethod
339-
def resolve_mask(obj):
340-
if obj.layerset2 and obj.layerset2.multimask and obj.slug in obj.layerset2.multimask:
341-
return obj.layerset2.multimask[obj.slug]
342-
else:
343-
return None
339+
def resolve_mask(obj: Layer):
340+
return json.loads(obj.mask.geojson) if obj.mask else None
344341

345342
@staticmethod
346-
def resolve_image_url(obj):
347-
if obj.region and obj.region.file:
348-
return obj.region.file.url
349-
else:
350-
return None
343+
def resolve_image_url(obj: Layer):
344+
return obj.region.file.url if obj.region and obj.region.file else None
351345

352346
@staticmethod
353-
def resolve_gcps_geojson(obj):
347+
def resolve_gcps_geojson(obj: Layer):
354348
if not obj.region:
355349
logger.warning(f"[WARNING] Layer {obj.pk} has no associated region")
356350
return None
@@ -362,18 +356,12 @@ def resolve_gcps_geojson(obj):
362356
return obj.region.gcpgroup.as_geojson
363357

364358
@staticmethod
365-
def resolve_created_by(obj):
366-
if obj.created_by:
367-
return obj.created_by.username
368-
else:
369-
return ""
359+
def resolve_created_by(obj: Layer):
360+
return obj.created_by.username if obj.created_by else ""
370361

371362
@staticmethod
372-
def resolve_last_updated_by(obj):
373-
if obj.last_updated_by:
374-
return obj.last_updated_by.username
375-
else:
376-
return ""
363+
def resolve_last_updated_by(obj: Layer):
364+
return obj.last_updated_by.username if obj.last_updated_by else ""
377365

378366

379367
class LayerFullSchema(Schema):
@@ -398,11 +386,8 @@ def resolve_urls(obj):
398386
}
399387

400388
@staticmethod
401-
def resolve_mask(obj):
402-
if obj.layerset2 and obj.layerset2.multimask and obj.slug in obj.layerset2.multimask:
403-
return obj.layerset2.multimask[obj.slug]
404-
else:
405-
return None
389+
def resolve_mask(obj: Layer):
390+
return obj.mask.geojson if obj.mask else None
406391

407392
@staticmethod
408393
def resolve_image_url(obj):

‎ohmg/core/management/commands/layerset.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ def handle(self, *args, **options):
3939
)
4040

4141
if options.operation == "inspect":
42-
print(ls.multimask_extent)
42+
print(ls, len(ls.get_layers()))

‎ohmg/core/management/commands/oneoffs.py‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -411,22 +411,22 @@ def old_ds_to_new(file_set):
411411
print("deleted")
412412

413413
## Mar 11th, 2026
414+
## Pushing all multimask entries directly onto Layers, now that masks are
415+
## stored on Layers, not as a collected set of GeoJSON in LayerSet.multimask
414416
elif operation == "add-masks-to-layers":
415417
from django.contrib.gis.geos import GEOSGeometry
416418

417419
from ohmg.core.models import Layer, LayerSet
418420

419421
for ls in LayerSet.objects.all().order_by("map__title"):
420422
print(ls)
421-
if ls.multimask:
422-
for k, v in ls.multimask.items():
423-
print(k)
424-
## there SHOULD only be one layer here, but per ticket
425-
## #308 some regions got duplicated, and some of the
426-
## duplicates got georeferenced :(. Can't clean all of
427-
## that up now, so... using filter here
428-
layers = Layer.objects.filter(slug=k, region__document__map=ls.map)
429-
for layer in layers:
430-
layer.mask = GEOSGeometry(json.dumps(v["geometry"]))
431-
layer.save(skip_map_lookup_update=True, set_extent=False)
432-
print("")
423+
for layer in ls.get_layers():
424+
print(f" {layer.slug}")
425+
if ls.multimask and layer.slug in ls.multimask:
426+
print(" -> setting mask from layerset multimask")
427+
mask_geom = ls.multimask[layer.slug]
428+
layer.mask = GEOSGeometry(json.dumps(mask_geom["geometry"]))
429+
else:
430+
print(" -> setting mask to None")
431+
layer.mask = None
432+
layer.save(skip_map_lookup_update=True, set_extent=False)

‎ohmg/core/models/layer.py‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import urllib.parse
34
from pathlib import Path
@@ -103,6 +104,19 @@ def file_url_encoded(self):
103104
no COG exists, return None."""
104105
return urllib.parse.quote(self.file_url, safe="")
105106

107+
@property
108+
def mask_geojson_feature(self) -> Union[dict | None]:
109+
"""Returns a GeoJSON feature representation of the Layer mask, or None if no mask"""
110+
return (
111+
{
112+
"type": "Feature",
113+
"geometry": json.loads(self.mask.geojson),
114+
"properties": {"layer": self.slug},
115+
}
116+
if self.mask
117+
else None
118+
)
119+
106120
def create_xyz_url(self) -> Union[str, None]:
107121
file_url = get_file_url(self)
108122
if file_url:
@@ -145,16 +159,7 @@ def set_layerset(self, layerset):
145159

146160
# make sure to clean up the existing multimask in the current vrs if necessary
147161
existing_obj = LayerSet.objects.get(pk=self.layerset2.pk) if self.layerset2 else None
148-
delete_existing = False
149-
if existing_obj:
150-
if existing_obj.multimask and self.slug in existing_obj.multimask:
151-
del existing_obj.multimask[self.slug]
152-
existing_obj.save(update_fields=["multimask"])
153-
logger.info(
154-
f"Layer {self.pk} removed from existing multimask in LayerSet {existing_obj.pk}"
155-
)
156-
if existing_obj.get_layers().count() == 1:
157-
delete_existing = True
162+
delete_existing = existing_obj and existing_obj.get_layers().count() == 1
158163
self.layerset2 = layerset
159164
self.save(update_fields=["layerset2"])
160165
logger.info(f"Layer {self.pk} added to LayerSet {self.layerset2} ({self.layerset2.pk})")

‎ohmg/core/models/layerset.py‎

Lines changed: 18 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import json
21
import logging
32
import urllib.parse
43
from typing import TYPE_CHECKING, Iterable, Union
54

65
from django.conf import settings
76
from django.contrib.gis.db import models
8-
from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Polygon
7+
from django.contrib.gis.geos import MultiPolygon, Polygon
98
from django.contrib.postgres.fields import ArrayField
109
from django.utils.functional import cached_property
1110
from django.utils.safestring import mark_safe
@@ -112,67 +111,25 @@ def create_xyz_url(self) -> Union[str, None]:
112111

113112
@property
114113
def multimask_extent(self):
115-
"""Calculate an extent based on all layers in this layerset's
116-
multimask. If there is no multimask, return None."""
117-
extent = None
118-
if self.multimask:
119-
feature_polygons = []
120-
for v in self.multimask.values():
121-
poly = Polygon(v["geometry"]["coordinates"][0])
122-
feature_polygons.append(poly)
123-
if len(feature_polygons) > 0:
124-
extent = MultiPolygon(feature_polygons, srid=4326).extent
125-
return extent
114+
"""Calculate an extent based any existing masks of layers in this LayerSet.
115+
If no layer have masks then return None."""
116+
feature_polygons = []
117+
for feat in self.multimask_geojson["features"]:
118+
poly = Polygon(feat["geometry"]["coordinates"][0])
119+
feature_polygons.append(poly)
120+
return (
121+
MultiPolygon(feature_polygons, srid=4326).extent if len(feature_polygons) > 0 else None
122+
)
126123

127124
@property
128-
def multimask_geojson(self):
129-
if self.multimask:
130-
multimask_geojson = {"type": "FeatureCollection", "features": []}
131-
for layer, geojson in self.multimask.items():
132-
geojson["properties"] = {"layer": layer}
133-
multimask_geojson["features"].append(geojson)
134-
return multimask_geojson
135-
else:
136-
return None
137-
138-
def validate_multimask_geojson(self, multimask_geojson):
139-
errors = []
140-
for feature in multimask_geojson["features"]:
141-
lyr = feature["properties"]["layer"]
142-
try:
143-
geom_str = json.dumps(feature["geometry"])
144-
g = GEOSGeometry(geom_str)
145-
if not g.valid:
146-
logger.warning(f"{self} | invalid mask: {lyr} - {g.valid_reason}")
147-
errors.append((lyr, g.valid_reason))
148-
except Exception as e:
149-
logger.warning(f"{self} | improper GeoJSON in multimask")
150-
errors.append((lyr, e))
151-
return errors
152-
153-
def update_multimask_from_geojson(self, multimask_geojson):
154-
from .layer import Layer
155-
156-
errors = self.validate_multimask_geojson(multimask_geojson)
157-
if errors:
158-
return errors
159-
160-
if multimask_geojson["features"]:
161-
self.multimask = {}
162-
for feature in multimask_geojson["features"]:
163-
layer_slug = feature["properties"]["layer"]
164-
self.multimask[feature["properties"]["layer"]] = feature
165-
166-
## future patch: save mask directly to layers
167-
layer = Layer.objects.get(slug=layer_slug, region__document__map=self.map)
168-
new_mask = GEOSGeometry(json.dumps(feature["geometry"]))
169-
if new_mask != layer.mask:
170-
logger.debug(f"updating mask on layer {layer.slug} ({layer.pk})")
171-
layer.mask = new_mask
172-
layer.save(skip_map_lookup_update=True, set_extent=False)
173-
else:
174-
self.multimask = None
175-
self.save(update_fields=["multimask"])
125+
def multimask_geojson(self) -> dict:
126+
"""Collect all masks from layers in this layerset and return as GeoJSON Feature Collection"""
127+
fc = {"type": "FeatureCollection", "features": []}
128+
for layer in self.get_layers():
129+
mask_geojson = layer.mask_geojson_feature
130+
if mask_geojson:
131+
fc["features"].append(mask_geojson)
132+
return fc
176133

177134
def save(self, set_tilejson: bool = False, *args, **kwargs):
178135
if self._state.adding is False:

‎ohmg/core/models/map.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ def stats(self):
232232
# make sure 0/0 appears at the very bottom, then 0/1, 0/2, etc.
233233
mm_percent = main_lyrs_ct * 0.000001
234234
mm_display = f"0/{main_lyrs_ct}"
235-
if main_layerset and main_layerset.multimask is not None:
236-
mm_ct = len(main_layerset.multimask)
235+
if main_layerset:
236+
mm_ct = len(main_layerset.multimask_geojson["features"])
237237
mm_todo = main_lyrs_ct - mm_ct
238238
if mm_ct > 0 and main_lyrs_ct > 0:
239239
mm_display = f"{mm_ct}/{main_lyrs_ct}"
@@ -393,8 +393,8 @@ def update_item_lookup(self):
393393
# make sure 0/0 appears at the very bottom, then 0/1, 0/2, etc.
394394
multimask_rank = main_lyrs_ct * 0.000001
395395

396-
if main_layerset and main_layerset.multimask is not None:
397-
multimask_ct = len(main_layerset.multimask)
396+
if main_layerset:
397+
multimask_ct = len(main_layerset.multimask_geojson["features"])
398398
if multimask_ct > 0 and main_lyrs_ct > 0:
399399
pct = multimask_ct / main_lyrs_ct
400400
multimask_rank += pct * 0.000001

‎ohmg/core/signals.py‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ def handle_layer_deletion(sender, instance, **kwargs):
5252
except Exception as e:
5353
logger.warning(f"instance.region.gcpgroup.delete(): {e}")
5454

55-
# remove layer mask from layerset if present
56-
if instance.layerset2:
57-
if instance.layerset2.multimask:
58-
if instance.slug in instance.layerset2.multimask:
59-
del instance.layerset2.multimask[instance.slug]
60-
instance.layerset2.save()
61-
6255
# remove layerset if this was the last layer attached to it
6356
if instance.layerset2:
6457
if instance.layerset2.get_layers().count() == 0:

‎ohmg/core/views.py‎

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from django.conf import settings
66
from django.contrib.auth.decorators import login_required
7-
from django.contrib.gis.geos import Polygon
7+
from django.contrib.gis.geos import GEOSGeometry, Polygon
88
from django.http import FileResponse, Http404, JsonResponse
99
from django.shortcuts import HttpResponse, get_object_or_404, redirect, render
1010
from django.utils.decorators import method_decorator
@@ -415,11 +415,7 @@ def get(self, request, pk, derivative, resource=""):
415415

416416

417417
class LayerSetView(View):
418-
@method_decorator(
419-
validate_post_request(
420-
operations=["bulk-classify-layers", "check-for-existing-mask", "set-mask"]
421-
)
422-
)
418+
@method_decorator(validate_post_request(operations=["bulk-classify-layers", "set-mask"]))
423419
def post(self, request):
424420
body = json.loads(request.body)
425421
operation = body.get("operation")
@@ -442,31 +438,44 @@ def post(self, request):
442438
else:
443439
return JsonResponseSuccess("Layers classified successfully.")
444440

445-
if operation == "check-for-existing-mask":
446-
r = get_object_or_404(Layer, pk=payload.get("resource-id"))
447-
448-
if r.layerset2:
449-
if not r.layerset2.category.slug == payload.get("category"):
450-
if r.layerset2.multimask and r.slug in r.layerset2.multimask:
451-
return JsonResponseFail(
452-
f"Layer already in {r.layerset2.category} multimask.",
453-
payload=payload,
454-
)
455-
456-
return JsonResponseSuccess(payload=payload)
457-
458441
if operation == "set-mask":
459-
try:
460-
layerset = LayerSet.objects.get(
461-
map_id=payload["map-id"], category__slug=payload["category"]
462-
)
463-
errors = layerset.update_multimask_from_geojson(payload["multimask-geojson"])
464-
if errors:
465-
return JsonResponseFail("; ".join([f"\n-- {i[0]}: {i[1]}" for i in errors]))
442+
layerset = LayerSet.objects.get(
443+
map_id=payload["map-id"], category__slug=payload["category"]
444+
)
445+
446+
## first validate the incoming geojson for each mask
447+
errors = []
448+
geom_lookup = {}
449+
for feature in payload["multimask-geojson"]["features"]:
450+
lyr_slug = feature["properties"]["layer"]
451+
try:
452+
geom_str = json.dumps(feature["geometry"])
453+
g = GEOSGeometry(geom_str)
454+
if not g.valid:
455+
logger.warning(f"{layerset} | invalid mask: {lyr_slug} - {g.valid_reason}")
456+
errors.append((lyr_slug, g.valid_reason))
457+
geom_lookup[lyr_slug] = g
458+
except Exception as e:
459+
logger.warning(f"{self} | improper GeoJSON in mask")
460+
errors.append((lyr_slug, e))
461+
if errors:
462+
return JsonResponseFail("; ".join([f"\n-- {i[0]}: {i[1]}" for i in errors]))
463+
464+
for layer in layerset.get_layers():
465+
# if there is no mask to set and the layer doesn't need a mask removed, skip
466+
if layer.mask is None and layer.slug not in geom_lookup:
467+
continue
468+
# update all layers with provided masks
469+
if layer.slug in geom_lookup:
470+
if layer.mask != geom_lookup[layer.slug]:
471+
layer.mask = geom_lookup[layer.slug]
472+
logger.debug(f"updating mask on layer {layer.slug} ({layer.pk})")
473+
# remove mask from any layers that previously had one but has since been deleted
466474
else:
467-
return JsonResponseSuccess()
468-
except LayerSet.DoesNotExist:
469-
return JsonResponseNotFound()
475+
logger.debug(f"removing mask from layer {layer.slug} ({layer.pk})")
476+
layer.mask = None
477+
layer.save(set_extent=False, skip_map_lookup_update=True)
478+
return JsonResponseSuccess()
470479

471480

472481
class LayersetDerivativeView(View):

0 commit comments

Comments
 (0)