Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ohmg/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
52 changes: 52 additions & 0 deletions ohmg/api/paginators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions ohmg/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
FilterAllDocumentsSchema,
FilterDocumentSchema,
FilterJobSchema,
FilterLayerSchema,
FilterRegionSchema,
FilterSessionSchema,
)
from .paginators import (
LayerPagination,
MapPagination,
ProfilePagination,
SessionPagination,
Expand Down Expand Up @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions ohmg/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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',
Expand All @@ -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',
Expand Down
21 changes: 18 additions & 3 deletions ohmg/core/models/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
2 changes: 2 additions & 0 deletions ohmg/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .views import (
DocumentView,
LayerListView,
LayersetDerivativeView,
LayerSetView,
LayerView,
Expand Down Expand Up @@ -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/<int:pk>", LayerSetView.as_view(), name="layerset_view"),
]
12 changes: 12 additions & 0 deletions ohmg/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions ohmg/frontend/svelte_components/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default (cliArgs) => {
'Map',
'Sessions',
'Maps',
'Layers',
'Jobs',
'Profiles',
'LatestBlogPosts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1168,28 +1168,31 @@
<nav style="justify-content: space-between;">
{#if CONTEXT.user.is_staff}
<div class="error-section">
<div class="tooltip">RMSE
<span class="tooltiptext">Root Mean Square Error is the average distance between
<div class="tooltip">
<span class="tooltiptext tooltip-left-anchor">Root Mean Square Error is the average distance between
where you placed a GCP and where the corresponding location on the
old map actually ends up. It should be under 2.35m. Only relevant with 4+ GCPs.</span>
</div>
<span class="tag is-small {rmseClass}">{rmse == null ? "n/a" : `${rmse}m`}</span>
RMSE
</div>
<span class="tag is-small {rmseClass}">{rmse == null ? "n/a" : `${rmse}m`}</span>
<label>
<span class="tooltip">Show error
<span class="tooltiptext">Visualize predicted points and distances to target points. RMSE is the average
of these distances.
</span>
</span>
<input type="checkbox" bind:checked={showOffets} />
</label>
<div class="tooltip">Skew
<span class="tooltiptext">Skew is the degree measure of how off-square the page is. It should be close to 0.</span>
</div>
<span class="tag is-small {skewClass}">{skew == null ? "n/a" : `${skew}°`}</span>
<div class="tooltip">Anisotropy
<span class="tooltiptext">Anisotropy is a measure of how distorted the
scale of the image is, as a ratio of X (horizontal) scale
to Y (vertical) scale. It should be close to 1.</span>
<div class="tooltip">Stretch
<span class="tooltiptext">"Stretch" is a normalized value of <em>anisotropy</em>, measured as the ratio
of the horizontal scale factor to the vertical scale factor. A value of 1 means the X and Y scales
are proportional.</span>
</div>
<span class="tag is-small {anisoClass}">{aniso == null ? "n/a" : aniso}</span>
<label>
<span class="tooltip">Show offsets
<span class="tooltiptext">Display predicted points and offset distances</span>
</span>
<input type="checkbox" bind:checked={showOffets} />
</label>
</div>
{/if}
<label title="Set georeferencing transformation">
Expand Down Expand Up @@ -1283,9 +1286,13 @@
cursor: pointer;
}

.tooltiptext.tooltip-left-anchor {
margin-left: 0;
}

.tooltiptext {
visibility: hidden;
width: 130px;
width: 200px;
background-color: black;
color: #ffffff;
text-align: center;
Expand All @@ -1294,7 +1301,7 @@
position: absolute;
z-index: 1;
bottom: 100%;
left: 65%;
left: 0%;
margin-left: -65px;
}

Expand Down
Loading
Loading