import operator from collections import Counter 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 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) counter = Counter(from_space for from_space, to_space in edges.keys()) 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 and (space_ids or counter[from_space] > 1) ] @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]) result = results[0] if results else None if GraphEdge.objects.filter(from_node__space=result.space).exclude(to_node__space=result.space).count() < 2: return None return result 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}