space "identifyable" property including quest
This commit is contained in:
parent
e574b10a68
commit
8373ee50de
7 changed files with 126 additions and 31 deletions
|
@ -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',
|
||||
|
|
18
src/c3nav/mapdata/migrations/0128_space_identifyable.py
Normal file
18
src/c3nav/mapdata/migrations/0128_space_identifyable.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
import c3nav.mapdata.quests.simple # noqa
|
||||
import c3nav.mapdata.quests.positioning # noqa
|
||||
import c3nav.mapdata.quests.route_descriptions # noqa
|
54
src/c3nav/mapdata/quests/positioning.py
Normal file
54
src/c3nav/mapdata/quests/positioning.py
Normal 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)
|
|
@ -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(
|
Loading…
Add table
Add a link
Reference in a new issue