from decimal import Decimal from itertools import chain from operator import attrgetter from django.core.validators import MinValueValidator from django.db import models from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from shapely.ops import unary_union from c3nav.mapdata.models.locations import SpecificLocation class Level(SpecificLocation, models.Model): """ A map level """ base_altitude = models.DecimalField(_('base altitude'), null=False, unique=True, max_digits=6, decimal_places=2) default_height = models.DecimalField(_('default space height'), max_digits=6, decimal_places=2, default=3.0, validators=[MinValueValidator(Decimal('0'))]) door_height = models.DecimalField(_('door height'), max_digits=6, decimal_places=2, default=2.0, validators=[MinValueValidator(Decimal('0'))]) on_top_of = models.ForeignKey('mapdata.Level', null=True, on_delete=models.CASCADE, related_name='levels_on_top', verbose_name=_('on top of')) short_label = models.SlugField(max_length=20, verbose_name=_('short label'), unique=True) class Meta: verbose_name = _('Level') verbose_name_plural = _('Levels') default_related_name = 'levels' ordering = ['base_altitude'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def lower(self, level_model=None): if self.on_top_of_id is not None: raise TypeError if level_model is None: level_model = Level return level_model.objects.filter(base_altitude__lt=self.base_altitude, on_top_of__isnull=True).order_by('-base_altitude') def higher(self, level_model=None): if self.on_top_of_id is not None: raise TypeError if level_model is None: level_model = Level return level_model.objects.filter(base_altitude__gt=self.base_altitude, on_top_of__isnull=True).order_by('base_altitude') @property def sublevels(self): if self.on_top_of is not None: raise TypeError return chain((self, ), self.levels_on_top.all()) @property def sublevel_title(self): return '-' if self.on_top_of_id is None else self.title @property def primary_level(self): return self if self.on_top_of_id is None else self.on_top_of @property def primary_level_pk(self): return self.pk if self.on_top_of_id is None else self.on_top_of_id def _serialize(self, level=True, **kwargs): result = super()._serialize(**kwargs) result['short_label'] = self.short_label result['on_top_of'] = self.on_top_of_id result['base_altitude'] = float(str(self.base_altitude)) result['default_height'] = float(str(self.default_height)) result['door_height'] = float(str(self.door_height)) return result def details_display(self, editor_url=True, **kwargs): result = super().details_display(**kwargs) result['display'].insert(3, (_('short label'), self.short_label)) result['display'].extend([ (_('outside only'), self.base_altitude), (_('default height'), self.default_height), ]) if editor_url: result['editor_url'] = reverse('editor.levels.detail', kwargs={'pk': self.pk}) return result @cached_property def min_altitude(self): return min(self.altitudeareas.all(), key=attrgetter('altitude'), default=self.base_altitude).altitude @cached_property def bounds(self): return unary_union(tuple(item.geometry.buffer(0) for item in chain(self.altitudeareas.all(), self.buildings.all()))).bounds def get_icon(self): return super().get_icon() or 'layers'