diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 6c75b96b..189bb4d7 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -24,8 +24,9 @@ from c3nav.mapdata.models.geometry.space import (POI, Area, Column, LineObstacle from c3nav.mapdata.models.level import Level from c3nav.mapdata.models.locations import (Location, LocationGroupCategory, LocationRedirect, LocationSlug, SpecificLocation) -from c3nav.mapdata.utils.locations import (get_location_by_slug_for_request, locations_for_request, - searchable_locations_for_request, visible_locations_for_request) +from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, + locations_for_request, searchable_locations_for_request, + visible_locations_for_request) from c3nav.mapdata.utils.models import get_submodels @@ -252,6 +253,7 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet): /{id}/ add ?show_redirect=1 to suppress redirects and show them as JSON. """ queryset = LocationSlug.objects.all() + lookup_value_regex = r'[^/]+' @api_etag() def list(self, request, *args, **kwargs): @@ -282,10 +284,7 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet): detailed = 'detailed' in request.GET geometry = 'geometry' in request.GET - if not pk.isdigit(): - raise NotFound - - location = locations_for_request(request).get(int(pk)) + location = get_location_by_id_for_request(pk, request) if location is None: raise NotFound @@ -321,6 +320,7 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet): class LocationBySlugViewSet(RetrieveModelMixin, GenericViewSet): queryset = LocationSlug.objects.all() lookup_field = 'slug' + lookup_value_regex = r'[^/]+' @api_etag() def retrieve(self, request, slug=None, *args, **kwargs): diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 3b6adedf..4441671e 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -1,4 +1,7 @@ +import math import operator +import re +from collections import OrderedDict from functools import reduce from itertools import chain from typing import List, Mapping, Optional @@ -6,6 +9,7 @@ from typing import List, Mapping, Optional from django.apps import apps from django.core.cache import cache from django.db.models import Prefetch, Q +from django.utils.translation import ugettext_lazy as _ from shapely.ops import cascaded_union from c3nav.mapdata.models import Level, Location, LocationGroup, MapUpdate @@ -154,13 +158,26 @@ def locations_by_slug_for_request(request) -> Mapping[str, LocationSlug]: return locations +def get_location_by_id_for_request(pk, request): + if isinstance(pk, str): + if pk.isdigit(): + pk = int(pk) + else: + return get_custom_location_for_request(pk, request) + return locations_for_request(request).get(pk) + + def get_location_by_slug_for_request(slug: str, request) -> Optional[LocationSlug]: cache_key = 'mapdata:location:by_slug:%s:%s' % (AccessPermission.cache_key_for_request(request), slug) location = cache.get(cache_key, None) if location is not None: return location - if ':' in slug: + if slug.startswith('c:'): + location = get_custom_location_for_request(slug, request) + if location is None: + return None + elif ':' in slug: code, pk = slug.split(':', 1) model_name = LocationSlug.LOCATION_TYPE_BY_CODE.get(code) if model_name is None or not pk.isdigit(): @@ -180,3 +197,45 @@ def get_location_by_slug_for_request(slug: str, request) -> Optional[LocationSlu cache.set(cache_key, location, 300) return location + + +def get_custom_location_for_request(slug: str, request): + match = re.match(r'^c:(?P[a-z0-9-_]+):(?P-?\d+(\.\d\d?)?):(?P-?\d+(\.\d\d?)?)$', slug) + if match is None: + return None + level = locations_by_slug_for_request(request).get(match.group('level')) + if not isinstance(level, Level): + return None + return CustomLocation(level, float(match.group('x')), float(match.group('y'))) + + +class CustomLocation: + can_search = True + + def __init__(self, level, x, y): + self.pk = 'c:%s:%s:%s' % (level.short_label, x, y) + self.level = level + self.x = x + self.y = y + self.title = str(_('Coordinates')) + self.subtitle = str(_('%(level)s, x=%(x)s, y=%(y)s') % {'level': self.level.title, + 'x': self.x, + 'y': self.y}) + + def serialize(self, simple_geometry=False, geometry=True, **kwargs): + result = OrderedDict(( + ('id', self.pk), + ('slug', self.pk), + ('title', self.title), + ('subtitle', self.subtitle), + )) + if simple_geometry: + result['point'] = (self.level.pk, self.x, self.y) + result['bounds'] = ((int(math.floor(self.x)), int(math.floor(self.y))), + (int(math.ceil(self.x)), int(math.ceil(self.y)))) + if geometry: + result['geometry'] = { + 'type': 'Point', + 'coordinates': (self.x, self.y) + } + return result diff --git a/src/c3nav/site/urls.py b/src/c3nav/site/urls.py index fd03202c..4902260e 100644 --- a/src/c3nav/site/urls.py +++ b/src/c3nav/site/urls.py @@ -6,9 +6,9 @@ details = r'(?P
details/)?' pos = r'(@(?P[a-z0-9-_:]+),(?P-?\d+(\.\d+)?),(?P-?\d+(\.\d+)?),(?P-?\d+(\.\d+)?))?' urlpatterns = [ - url(r'^(?P[l])/(?P[a-z0-9-_:]+)/%s%s$' % (details, pos), map_index, name='site.index'), - url(r'^(?P[od])/(?P[a-z0-9-_:]+)/%s$' % pos, map_index, name='site.index'), - url(r'^r/(?P[a-z0-9-_:]+)/(?P[a-z0-9-_:]+)/%s%s$' % (details, pos), map_index, name='site.index'), + url(r'^(?P[l])/(?P[a-z0-9-_.:]+)/%s%s$' % (details, pos), map_index, name='site.index'), + url(r'^(?P[od])/(?P[a-z0-9-_.:]+)/%s$' % pos, map_index, name='site.index'), + url(r'^r/(?P[a-z0-9-_.:]+)/(?P[a-z0-9-_.:]+)/%s%s$' % (details, pos), map_index, name='site.index'), url(r'^(?Pr)/%s$' % pos, map_index, name='site.index'), url(r'^%s$' % pos, map_index, name='site.index') ]