From 8373ee50de18e0b9cba682d72073287e6fc2db18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 26 Dec 2024 03:44:05 +0100 Subject: [PATCH] space "identifyable" property including quest --- src/c3nav/editor/forms.py | 2 +- .../migrations/0128_space_identifyable.py | 18 +++++ src/c3nav/mapdata/models/geometry/level.py | 13 ++++ src/c3nav/mapdata/models/geometry/space.py | 2 +- src/c3nav/mapdata/quests/__init__.py | 3 +- src/c3nav/mapdata/quests/positioning.py | 54 +++++++++++++++ .../{simple.py => route_descriptions.py} | 65 +++++++++++-------- 7 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0128_space_identifyable.py create mode 100644 src/c3nav/mapdata/quests/positioning.py rename src/c3nav/mapdata/quests/{simple.py => route_descriptions.py} (79%) diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 98df6300..8bf3d432 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -380,7 +380,7 @@ def create_editor_form(editor_model): 'up_separate', 'bssid', 'main_point', 'external_url', 'external_url_label', 'hub_import_type', 'walk', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', - 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label', + "identifyable", 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssid', 'bluetooth_address', "group", 'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', diff --git a/src/c3nav/mapdata/migrations/0128_space_identifyable.py b/src/c3nav/mapdata/migrations/0128_space_identifyable.py new file mode 100644 index 00000000..7ec09cae --- /dev/null +++ b/src/c3nav/mapdata/migrations/0128_space_identifyable.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-12-26 02:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0127_alter_beaconmeasurement_data_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='space', + name='identifyable', + field=models.BooleanField(default=None, help_text='if unknown, this will be a quest. if yes, quests for enter, leave or cross descriptions to this room will be generated.', null=True, verbose_name='easily identifyable/findable'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index ad817002..a3ebdef9 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -6,6 +6,7 @@ from operator import attrgetter, itemgetter from typing import Sequence import numpy as np +from django.core.exceptions import ObjectDoesNotExist from django.core.validators import MinValueValidator from django.db import models from django.db.models import CheckConstraint, Q @@ -95,6 +96,13 @@ class LevelGeometryMixin(GeometryMixin): def pre_save_changed_geometries(self): self.register_change() + @cached_property + def main_level_id(self): + try: + return self.level.on_top_of_id or self.level_id + except ObjectDoesNotExist: + return None + def save(self, *args, **kwargs): self.pre_save_changed_geometries() super().save(*args, **kwargs) @@ -127,6 +135,11 @@ class Space(LevelGeometryMixin, SpecificLocation, models.Model): load_group_contribute = models.ForeignKey(LoadGroup, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('contribute to load group')) + identifyable = models.BooleanField(null=True, default=None, + verbose_name=_('easily identifyable/findable'), + help_text=_('if unknown, this will be a quest. if yes, quests for enter, ' + 'leave or cross descriptions to this room will be generated.')) + class Meta: verbose_name = _('Space') verbose_name_plural = _('Spaces') diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index 4d468aad..24cbeb33 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -44,7 +44,7 @@ class SpaceGeometryMixin(GeometryMixin): @cached_property def main_level_id(self): try: - return self.space.level.on_top_of_id or self.space.level_id + return self.space.main_level_id except ObjectDoesNotExist: return None diff --git a/src/c3nav/mapdata/quests/__init__.py b/src/c3nav/mapdata/quests/__init__.py index b1e35486..d46b4f01 100644 --- a/src/c3nav/mapdata/quests/__init__.py +++ b/src/c3nav/mapdata/quests/__init__.py @@ -1 +1,2 @@ -import c3nav.mapdata.quests.simple # noqa \ No newline at end of file +import c3nav.mapdata.quests.positioning # noqa +import c3nav.mapdata.quests.route_descriptions # noqa \ No newline at end of file diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py new file mode 100644 index 00000000..bb3ab872 --- /dev/null +++ b/src/c3nav/mapdata/quests/positioning.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from shapely import Point +from shapely.geometry import mapping + +from c3nav.mapdata.models.geometry.space import RangingBeacon +from c3nav.mapdata.quests.base import ChangeSetModelForm, register_quest, Quest + + +class RangingBeaconAltitudeQuestForm(ChangeSetModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["altitude"].label = ( + _('How many meters above ground is the access point “%s” mounted?') % self.instance.comment + ) + + def clean_altitude(self): + data = self.cleaned_data["altitude"] + if not data: + raise ValidationError(_("The AP should not be 0m above ground.")) + return data + + class Meta: + model = RangingBeacon + fields = ("altitude", ) + + def save(self, *args, **kwargs): + self.instance.altitude_quest = False + return super().save(*args, **kwargs) + + @property + def changeset_title(self): + return f'Altitude Quest: {self.instance.title}' + + +@register_quest +@dataclass +class RangingBeaconAltitudeQuest(Quest): + quest_type = "ranging_beacon_altitude" + quest_type_label = _('Ranging Beacon Altitude') + quest_type_icon = "router" + form_class = RangingBeaconAltitudeQuestForm + obj: RangingBeacon + + @property + def point(self) -> Point: + return mapping(self.obj.geometry) + + @classmethod + def _qs_for_request(cls, request): + return RangingBeacon.qs_for_request(request).select_related('space', + 'space__level').filter(altitude_quest=True) diff --git a/src/c3nav/mapdata/quests/simple.py b/src/c3nav/mapdata/quests/route_descriptions.py similarity index 79% rename from src/c3nav/mapdata/quests/simple.py rename to src/c3nav/mapdata/quests/route_descriptions.py index 0993a849..14779743 100644 --- a/src/c3nav/mapdata/quests/simple.py +++ b/src/c3nav/mapdata/quests/route_descriptions.py @@ -1,37 +1,31 @@ import operator from dataclasses import dataclass from functools import reduce -from typing import ClassVar, Optional +from typing import Optional, ClassVar -from django.core.exceptions import ValidationError -from django.db.models import F, Q +from django.db.models import Q, F from django.utils.translation import gettext_lazy as _ +from ninja.errors import ValidationError from shapely import Point, LineString from shapely.geometry import mapping from c3nav.mapdata.forms import I18nModelFormMixin -from c3nav.mapdata.models import GraphEdge, Space -from c3nav.mapdata.models.geometry.space import RangingBeacon, LeaveDescription, CrossDescription -from c3nav.mapdata.quests.base import register_quest, Quest, ChangeSetModelForm +from c3nav.mapdata.models import Space, GraphEdge +from c3nav.mapdata.models.geometry.space import LeaveDescription, CrossDescription +from c3nav.mapdata.quests.base import ChangeSetModelForm, register_quest, Quest from c3nav.mapdata.utils.geometry import unwrap_geom -class RangingBeaconAltitudeQuestForm(ChangeSetModelForm): +class SpaceIdentifyableQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["altitude"].label = ( - _('How many meters above ground is the access point “%s” mounted?') % self.instance.comment - ) - - def clean_altitude(self): - data = self.cleaned_data["altitude"] - if not data: - raise ValidationError(_("The AP should not be 0m above ground.")) - return data + self.fields["identifyable"].label = _("Does this space qualify as “easily identifyable/findable”?") + self.fields["identifyable"].help_text = "" + self.fields["identifyable"].required = True class Meta: - model = RangingBeacon - fields = ("altitude", ) + model = Space + fields = ("identifyable", ) def save(self, *args, **kwargs): self.instance.altitude_quest = False @@ -44,21 +38,34 @@ class RangingBeaconAltitudeQuestForm(ChangeSetModelForm): @register_quest @dataclass -class RangingBeaconAltitudeQuest(Quest): - quest_type = "ranging_beacon_altitude" - quest_type_label = _('Ranging Beacon Altitude') - quest_type_icon = "router" - form_class = RangingBeaconAltitudeQuestForm - obj: RangingBeacon +class SpaceIdentifyableQuest(Quest): + quest_type = "space_identifyable" + quest_type_label = _('Space identifyability') + quest_type_icon = "beenhere" + form_class = SpaceIdentifyableQuestForm + obj: Space + + @property + def quest_description(self) -> list[str]: + return [ + _("If you are standing in any adjacent space to this one and you know that this space is named “%s”, " + "will it be very straightforward to find it?") % self.obj.title, + _("This applies mainly to rooms that are connected to a corridor where, if you pass their door, you will, " + "thanks to a sign or other some other labeling, immediately able to tell that this is the door you want " + "to go through."), + _("Also, obviously, if this is a side room to another room, like a bathroom in a wardrobe."), + _("If finding this space from adjacent spaces is not obvious, the answer is no."), + _("Even if it's a bathroom or similar facility, the answer is no unless it is very obvious and easy to see" + " where it is."), + ] @property def point(self) -> Point: - return mapping(self.obj.geometry) + return mapping(self.obj.point) @classmethod def _qs_for_request(cls, request): - return RangingBeacon.qs_for_request(request).select_related('space', - 'space__level').filter(altitude_quest=True) + return Space.qs_for_request(request).select_related('level').filter(identifyable=None) def get_door_edges_for_request(request, space_ids: Optional[list[int]] = None): @@ -68,7 +75,6 @@ def get_door_edges_for_request(request, space_ids: Optional[list[int]] = None): spaces = {space.pk: space for space in qs.select_related("level")} existing = set(tuple(item) for item in LeaveDescription.objects.values_list("space_id", "target_space_id")) - qs = GraphEdge.objects.filter( from_node__space__in=spaces, to_node__space__in=spaces, @@ -143,6 +149,7 @@ class LeaveDescriptionQuest(Quest): the_point=unwrap_geom(from_point), ) for (from_space, to_space), (from_point, to_point) in edges.items() + if spaces[to_space].identifyable is False ] @classmethod @@ -220,6 +227,8 @@ class CrossDescriptionQuest(Quest): results = [] for (origin_space, space), (first_point, origin_point) in edges.items(): for target_space, target_point in from_space_conns.get(space, ()): + if not (spaces[target_space].identifyable is False): + continue line = LineString([origin_point, target_point]) the_point = line.interpolate(0.33, normalized=True) if line.length < 3 else line.interpolate(1) results.append(cls(