2017-05-08 16:40:22 +02:00
|
|
|
from collections import OrderedDict
|
2017-05-08 21:55:45 +02:00
|
|
|
|
|
|
|
import numpy as np
|
2016-12-19 16:54:11 +01:00
|
|
|
from django.core.cache import cache
|
2016-12-16 11:03:40 +01:00
|
|
|
from django.db import models
|
|
|
|
from django.utils.functional import cached_property
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2016-12-22 03:33:53 +01:00
|
|
|
from django.utils.translation import ungettext_lazy
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-05-08 21:55:45 +02:00
|
|
|
from c3nav.mapdata.fields import GeometryField, JSONField, validate_bssid_lines
|
2016-12-19 16:54:11 +01:00
|
|
|
from c3nav.mapdata.lastupdate import get_last_mapdata_update
|
2017-05-08 16:40:22 +02:00
|
|
|
from c3nav.mapdata.models.base import EditorFormMixin
|
2016-12-16 11:03:40 +01:00
|
|
|
|
|
|
|
|
2017-05-10 18:03:57 +02:00
|
|
|
class LocationSlug(models.Model):
|
|
|
|
slug = models.SlugField(_('name'), unique=True, null=True, max_length=50)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Slug for Location')
|
|
|
|
verbose_name_plural = _('Slugs für Locations')
|
|
|
|
default_related_name = 'locationslugs'
|
|
|
|
|
|
|
|
|
|
|
|
class LocationModelMixin:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Location(LocationSlug, models.Model):
|
|
|
|
titles = JSONField(default={})
|
|
|
|
can_search = models.BooleanField(default=True, verbose_name=_('can be searched'))
|
|
|
|
can_describe = models.BooleanField(default=True, verbose_name=_('can be used to describe a position'))
|
|
|
|
color = models.CharField(null=True, blank=True, max_length=16, verbose_name=_('background color'),
|
|
|
|
help_text=_('if set, has to be a valid color for svg images'))
|
|
|
|
public = models.BooleanField(verbose_name=_('public'), default=True)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
def get_geojson_properties(self):
|
|
|
|
result = super().get_geojson_properties()
|
|
|
|
result['slug'] = self.slug_ptr.slug
|
|
|
|
result['titles'] = OrderedDict(sorted(self.titles.items()))
|
|
|
|
return result
|
|
|
|
|
|
|
|
@property
|
|
|
|
def subtitle(self):
|
|
|
|
return self._meta.verbose_name
|
|
|
|
|
|
|
|
|
|
|
|
class SpecificLocation(Location, models.Model):
|
|
|
|
groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('Location Groups'), blank=True)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
|
|
|
|
class LegacyLocation:
|
2016-12-16 11:03:40 +01:00
|
|
|
@property
|
|
|
|
def location_id(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@property
|
|
|
|
def subtitle(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def to_location_json(self):
|
|
|
|
return OrderedDict((
|
2017-05-05 16:21:48 +02:00
|
|
|
('id', self.id),
|
|
|
|
('location_id', self.location_id),
|
2016-12-16 11:03:40 +01:00
|
|
|
('title', str(self.title)),
|
|
|
|
('subtitle', str(self.subtitle)),
|
|
|
|
))
|
|
|
|
|
|
|
|
|
2017-05-10 18:03:57 +02:00
|
|
|
class LocationGroup(Location, EditorFormMixin, models.Model):
|
2016-12-24 21:41:57 +01:00
|
|
|
compiled_room = models.BooleanField(default=False, verbose_name=_('is a compiled room'))
|
2016-12-16 11:03:40 +01:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Location Group')
|
|
|
|
verbose_name_plural = _('Location Groups')
|
|
|
|
default_related_name = 'locationgroups'
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def location_id(self):
|
2017-05-05 16:21:48 +02:00
|
|
|
return 'g:'+self.slug
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2016-12-23 22:42:42 +01:00
|
|
|
def get_in_levels(self):
|
|
|
|
last_update = get_last_mapdata_update()
|
|
|
|
if last_update is None:
|
|
|
|
return self._get_in_levels()
|
|
|
|
|
2017-05-05 16:21:48 +02:00
|
|
|
cache_key = 'c3nav__mapdata__locationgroup__in_levels__'+last_update.isoformat()+'__'+str(self.id),
|
2016-12-23 22:42:42 +01:00
|
|
|
in_levels = cache.get(cache_key)
|
|
|
|
if not in_levels:
|
|
|
|
in_levels = self._get_in_levels()
|
|
|
|
cache.set(cache_key, in_levels, 900)
|
|
|
|
|
|
|
|
return in_levels
|
|
|
|
|
|
|
|
def _get_in_levels(self):
|
|
|
|
level_ids = set()
|
|
|
|
in_levels = []
|
|
|
|
for arealocation in self.arealocations.all():
|
|
|
|
for area in arealocation.get_in_areas():
|
|
|
|
if area.location_type == 'level' and area.id not in level_ids:
|
|
|
|
level_ids.add(area.id)
|
|
|
|
in_levels.append(area)
|
|
|
|
|
2017-05-07 12:06:13 +02:00
|
|
|
in_levels = sorted(in_levels, key=lambda area: area.section.altitude)
|
2016-12-23 22:42:42 +01:00
|
|
|
return in_levels
|
|
|
|
|
2016-12-22 03:33:53 +01:00
|
|
|
@property
|
|
|
|
def subtitle(self):
|
2016-12-23 22:42:42 +01:00
|
|
|
if self.compiled_room:
|
|
|
|
return ', '.join(area.title for area in self.get_in_levels())
|
2016-12-22 03:33:53 +01:00
|
|
|
return ungettext_lazy('%d location', '%d locations') % self.arealocations.count()
|
|
|
|
|
2016-12-19 16:54:11 +01:00
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
2016-12-23 22:42:42 +01:00
|
|
|
def get_geojson_properties(self):
|
|
|
|
result = super().get_geojson_properties()
|
|
|
|
return result
|
|
|
|
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-05-08 16:40:22 +02:00
|
|
|
class AreaLocation(models.Model):
|
2016-12-19 16:54:11 +01:00
|
|
|
LOCATION_TYPES = (
|
|
|
|
('level', _('Level')),
|
|
|
|
('area', _('General Area')),
|
|
|
|
('room', _('Room')),
|
|
|
|
('roomsegment', _('Room Segment')),
|
|
|
|
('poi', _('Point of Interest')),
|
|
|
|
)
|
|
|
|
LOCATION_TYPES_ORDER = tuple(name for name, title in LOCATION_TYPES)
|
2016-12-22 03:23:59 +01:00
|
|
|
ROUTING_INCLUSIONS = (
|
2017-05-01 18:10:46 +02:00
|
|
|
('default', _('Default, include it is unlocked')),
|
2016-12-22 03:23:59 +01:00
|
|
|
('allow_avoid', _('Included, but allow excluding')),
|
|
|
|
('allow_include', _('Avoided, but allow including')),
|
2016-12-23 23:34:20 +01:00
|
|
|
('needs_permission', _('Excluded, needs permission to include')),
|
2016-12-22 03:23:59 +01:00
|
|
|
)
|
2016-12-19 16:54:11 +01:00
|
|
|
|
2017-05-08 16:40:22 +02:00
|
|
|
section = models.ForeignKey('mapdata.Section', on_delete=models.CASCADE, verbose_name=_('section'))
|
2017-05-08 16:05:44 +02:00
|
|
|
geometry = GeometryField('polygon')
|
2017-05-05 16:21:48 +02:00
|
|
|
slug = models.SlugField(_('Name'), unique=True, max_length=50)
|
2016-12-19 18:52:10 +01:00
|
|
|
location_type = models.CharField(max_length=20, choices=LOCATION_TYPES, verbose_name=_('Location Type'))
|
2016-12-16 11:03:40 +01:00
|
|
|
titles = JSONField()
|
2016-12-19 18:52:10 +01:00
|
|
|
groups = models.ManyToManyField(LocationGroup, verbose_name=_('Location Groups'), blank=True)
|
2017-05-01 17:35:41 +02:00
|
|
|
public = models.BooleanField(verbose_name=_('public'))
|
2016-12-19 18:52:10 +01:00
|
|
|
|
2016-12-19 16:54:11 +01:00
|
|
|
can_search = models.BooleanField(default=True, verbose_name=_('can be searched'))
|
|
|
|
can_describe = models.BooleanField(default=True, verbose_name=_('can be used to describe a position'))
|
2016-12-24 22:05:59 +01:00
|
|
|
color = models.CharField(null=True, blank=True, max_length=16, verbose_name=_('background color'),
|
|
|
|
help_text=_('if set, has to be a valid color for svg images'))
|
2016-12-22 03:23:59 +01:00
|
|
|
routing_inclusion = models.CharField(max_length=20, choices=ROUTING_INCLUSIONS, default='default',
|
2016-12-19 18:52:10 +01:00
|
|
|
verbose_name=_('Routing Inclusion'))
|
2016-12-27 19:03:54 +01:00
|
|
|
bssids = models.TextField(blank=True, validators=[validate_bssid_lines], verbose_name=_('BSSIDs'))
|
2016-12-16 11:03:40 +01:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Area Location')
|
|
|
|
verbose_name_plural = _('Area Locations')
|
|
|
|
default_related_name = 'arealocations'
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def location_id(self):
|
2017-05-05 16:21:48 +02:00
|
|
|
return self.slug
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2016-12-19 16:54:11 +01:00
|
|
|
def get_in_areas(self):
|
|
|
|
last_update = get_last_mapdata_update()
|
|
|
|
if last_update is None:
|
|
|
|
return self._get_in_areas()
|
|
|
|
|
2017-05-05 16:21:48 +02:00
|
|
|
cache_key = 'c3nav__mapdata__location__in_areas__'+last_update.isoformat()+'__'+str(self.id),
|
2016-12-19 16:54:11 +01:00
|
|
|
in_areas = cache.get(cache_key)
|
|
|
|
if not in_areas:
|
|
|
|
in_areas = self._get_in_areas()
|
|
|
|
cache.set(cache_key, in_areas, 900)
|
|
|
|
|
|
|
|
return in_areas
|
|
|
|
|
|
|
|
def _get_in_areas(self):
|
|
|
|
my_area = self.geometry.area
|
|
|
|
|
|
|
|
in_areas = []
|
|
|
|
area_location_i = self.get_sort_key(self)
|
|
|
|
for location_type in reversed(self.LOCATION_TYPES_ORDER[:area_location_i]):
|
2017-05-07 12:06:13 +02:00
|
|
|
for arealocation in AreaLocation.objects.filter(location_type=location_type, section=self.section):
|
2016-12-19 16:54:11 +01:00
|
|
|
intersection_area = arealocation.geometry.intersection(self.geometry).area
|
2016-12-19 18:07:04 +01:00
|
|
|
if intersection_area and intersection_area / my_area > 0.99:
|
2016-12-19 16:54:11 +01:00
|
|
|
in_areas.append(arealocation)
|
|
|
|
|
|
|
|
return in_areas
|
|
|
|
|
|
|
|
@property
|
|
|
|
def subtitle(self):
|
2016-12-23 23:03:25 +01:00
|
|
|
return self.get_subtitle()
|
2016-12-19 16:54:11 +01:00
|
|
|
|
2016-12-21 00:24:56 +01:00
|
|
|
@property
|
|
|
|
def subtitle_without_type(self):
|
2016-12-23 23:03:25 +01:00
|
|
|
return self.get_subtitle()
|
2016-12-21 00:24:56 +01:00
|
|
|
|
2016-12-23 23:03:25 +01:00
|
|
|
def get_subtitle(self):
|
2016-12-21 00:24:56 +01:00
|
|
|
items = []
|
2016-12-24 21:43:50 +01:00
|
|
|
items += [group.title for group in self.groups.filter(can_describe=True)]
|
2016-12-22 03:31:27 +01:00
|
|
|
items += [area.title for area in self.get_in_areas() if area.can_describe]
|
2016-12-19 16:54:11 +01:00
|
|
|
return ', '.join(items)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_sort_key(cls, arealocation):
|
|
|
|
return cls.LOCATION_TYPES_ORDER.index(arealocation.location_type)
|
|
|
|
|
2016-12-16 11:03:40 +01:00
|
|
|
def get_geojson_properties(self):
|
|
|
|
result = super().get_geojson_properties()
|
|
|
|
return result
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
|
2017-05-10 18:03:57 +02:00
|
|
|
class PointLocation(LegacyLocation):
|
|
|
|
def __init__(self, section: 'Section', x: int, y: int, request):
|
2017-05-07 12:06:13 +02:00
|
|
|
self.section = section
|
2016-12-16 11:03:40 +01:00
|
|
|
self.x = x
|
|
|
|
self.y = y
|
2016-12-24 02:08:30 +01:00
|
|
|
self.request = request
|
2016-12-16 11:03:40 +01:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def location_id(self):
|
2017-05-07 12:06:13 +02:00
|
|
|
return 'c:%d:%d:%d' % (self.section.id, self.x * 100, self.y * 100)
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2016-12-21 01:59:08 +01:00
|
|
|
@cached_property
|
|
|
|
def xy(self):
|
|
|
|
return np.array((self.x, self.y))
|
|
|
|
|
2016-12-21 00:24:56 +01:00
|
|
|
@cached_property
|
|
|
|
def description(self):
|
|
|
|
from c3nav.routing.graph import Graph
|
|
|
|
graph = Graph.load()
|
2017-05-07 12:06:13 +02:00
|
|
|
point = graph.get_nearest_point(self.section, self.x, self.y)
|
2016-12-24 02:08:30 +01:00
|
|
|
|
2016-12-24 02:46:05 +01:00
|
|
|
if point is None or (':nonpublic' in point.arealocations and not self.request.c3nav_full_access and
|
2016-12-24 02:16:35 +01:00
|
|
|
not len(set(self.request.c3nav_access_list) & set(point.arealocations))):
|
2016-12-21 00:24:56 +01:00
|
|
|
return _('Unreachable Coordinates'), ''
|
|
|
|
|
|
|
|
locations = sorted(AreaLocation.objects.filter(name__in=point.arealocations, can_describe=True),
|
|
|
|
key=AreaLocation.get_sort_key, reverse=True)
|
|
|
|
|
|
|
|
if not locations:
|
|
|
|
return _('Coordinates'), ''
|
|
|
|
|
|
|
|
location = locations[0]
|
|
|
|
if location.contains(self.x, self.y):
|
|
|
|
return (_('Coordinates in %(location)s') % {'location': location.title}), location.subtitle_without_type
|
|
|
|
else:
|
|
|
|
return (_('Coordinates near %(location)s') % {'location': location.title}), location.subtitle_without_type
|
|
|
|
|
2016-12-16 11:03:40 +01:00
|
|
|
@property
|
|
|
|
def title(self) -> str:
|
2016-12-21 00:24:56 +01:00
|
|
|
return self.description[0]
|
2016-12-16 11:03:40 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def subtitle(self) -> str:
|
2016-12-21 00:24:56 +01:00
|
|
|
add_subtitle = self.description[1]
|
2017-05-07 12:06:13 +02:00
|
|
|
subtitle = '%s:%d:%d' % (self.section.name, self.x * 100, self.y * 100)
|
2016-12-21 00:24:56 +01:00
|
|
|
if add_subtitle:
|
|
|
|
subtitle += ' - '+add_subtitle
|
|
|
|
return subtitle
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2016-12-27 23:39:14 +01:00
|
|
|
def to_location_json(self):
|
2016-12-16 11:03:40 +01:00
|
|
|
result = super().to_location_json()
|
2017-05-07 12:06:13 +02:00
|
|
|
result['section'] = self.section.id
|
2016-12-16 11:03:40 +01:00
|
|
|
result['x'] = self.x
|
|
|
|
result['y'] = self.y
|
|
|
|
return result
|