space "identifyable" property including quest

This commit is contained in:
Laura Klünder 2024-12-26 03:44:05 +01:00
parent e574b10a68
commit 8373ee50de
7 changed files with 126 additions and 31 deletions

View file

@ -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',

View file

@ -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'),
),
]

View file

@ -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')

View file

@ -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

View file

@ -1 +1,2 @@
import c3nav.mapdata.quests.simple # noqa
import c3nav.mapdata.quests.positioning # noqa
import c3nav.mapdata.quests.route_descriptions # noqa

View file

@ -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)

View file

@ -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(