import operator from dataclasses import dataclass from functools import reduce from typing import Optional, ClassVar 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 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 SpaceIdentifyableQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) 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 = Space fields = ("identifyable", ) 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 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.point) @classmethod def _qs_for_request(cls, request): 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): qs = Space.qs_for_request(request) if space_ids: qs = qs.filter(pk__in=space_ids) 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, waytype=None, ) if space_ids: qs = qs.filter(reduce(operator.or_, (Q(from_node__space_id=space_ids[i], to_node__space_id=space_ids[i+1]) for i in range(len(space_ids) - 1)))) return spaces, { (from_space, to_space): (from_point, to_point) for from_space, to_space, from_point, to_point in qs.exclude( from_node__space=F("to_node__space") ).values_list("from_node__space_id", "to_node__space_id", "from_node__geometry", "to_node__geometry") if (from_space, to_space) not in existing } class LeaveDescriptionQuestForm(I18nModelFormMixin, ChangeSetModelForm): class Meta: model = LeaveDescription fields = ("description", ) @property def changeset_title(self): return f'LeaveDesscription Quest: {self.instance.space.title} → {self.instance.target_space.title}' @register_quest @dataclass class LeaveDescriptionQuest(Quest): quest_type = "leave_description" quest_type_label = _('Leave Description') quest_type_icon = "logout" form_class = LeaveDescriptionQuestForm obj: ClassVar space: Space target_space: Space the_point: Point @property def quest_description(self) -> list[str]: return [ _("Please provide a description to be used when leaving “%(from_space)s” towards “%(to_space)s”.") % { "from_space": self.space.title, "to_space": self.target_space.title, }, _("This will be used all for all connections that lead from this space to the other, not just the " "highlighted one! So, if there is more than one connection between these two rooms, please be generic."), _("The description should make it possible to find the room exit no matter where in the room you are."), _("Examples: “Walk through the red door.”, „Walk through the doors with the Sign “Hall 3” above it.”"), ] @property def point(self) -> dict: return mapping(self.the_point) @property def level_id(self) -> int: return self.space.level.on_top_of_id or self.space.level_id @property def identifier(self) -> str: return f"{self.space.pk}-{self.target_space.pk}" @classmethod def get_all_for_request(cls, request, space_ids: tuple[int, int] = ()): spaces, edges = get_door_edges_for_request(request, space_ids) return [ cls( space=spaces[from_space], target_space=spaces[to_space], 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 def get_for_request(cls, request, identifier: str): space_ids = identifier.split('-') if len(space_ids) != 2 or not all(i.isdigit() for i in space_ids): return None results = cls.get_all_for_request(request, space_ids=[int(i) for i in space_ids]) return results[0] if results else None def get_form_kwargs(self, request): instance = LeaveDescription() instance.space = self.space instance.target_space = self.target_space return {"instance": instance} class CrossDescriptionQuestForm(I18nModelFormMixin, ChangeSetModelForm): class Meta: model = CrossDescription fields = ("description", ) @property def changeset_title(self): return f'CrossDesscription Quest: {self.instance.origin_space.title} → {self.instance.space.title} → {self.instance.target_space.title}' @register_quest @dataclass class CrossDescriptionQuest(Quest): quest_type = "cross_description" quest_type_label = _('Cross Description') quest_type_icon = "roundabout_right" form_class = CrossDescriptionQuestForm obj: ClassVar space: Space origin_space: Space target_space: Space the_point: Point @property def quest_description(self) -> list[str]: return [ _("Please provide a description to be used when coming from “%(from_space)s” into “%(space)s” and exiting towardss “%(to_space)s”.") % { "from_space": self.origin_space.title, "space": self.space.title, "to_space": self.target_space.title, }, _("This will be used combination of space connections that match this description, not just the " "highlighted ones! So, if there is more than connection between these two rooms, please be generic."), _("This description will replace the entire route descripting when passing through the room this way."), _("Examples: “Go straight ahead into the room right across.” “Turn right and go through the big doors.”"), ] @property def point(self) -> dict: return mapping(self.the_point) @property def level_id(self) -> int: return self.space.level.on_top_of_id or self.space.level_id @property def identifier(self) -> str: return f"{self.origin_space.pk}-{self.space.pk}-{self.target_space.pk}" @classmethod def get_all_for_request(cls, request, space_ids: Optional[list[int]] = None): spaces, edges = get_door_edges_for_request(request, space_ids) from_space_conns = {} for (from_space, to_space), (from_point, to_point) in edges.items(): from_space_conns.setdefault(from_space, []).append((to_space, from_point)) 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( space=spaces[space], origin_space=spaces[origin_space], target_space=spaces[target_space], the_point=the_point, )) return results @classmethod def get_for_request(cls, request, identifier: str): space_ids = identifier.split('-') if len(space_ids) != 3 or not (space_ids[0].isdigit() and space_ids[1].isdigit()): return None results = cls.get_all_for_request(request, space_ids=tuple(int(i) for i in space_ids)) return results[0] if results else None def get_form_kwargs(self, request): instance = LeaveDescription() instance.space = self.space instance.origin_space = self.origin_space instance.target_space = self.target_space return {"instance": instance}