team-3/src/c3nav/mapdata/models/geometry/level.py

159 lines
6.4 KiB
Python
Raw Normal View History

from django.db import models
from django.utils.translation import ugettext_lazy as _
2017-08-05 14:17:24 +02:00
from shapely.ops import cascaded_union
from c3nav.mapdata.fields import GeometryField
2017-08-05 12:13:15 +02:00
from c3nav.mapdata.models import Level
2017-07-13 23:49:00 +02:00
from c3nav.mapdata.models.access import AccessRestrictionMixin
2017-05-08 16:40:22 +02:00
from c3nav.mapdata.models.geometry.base import GeometryMixin
2017-05-10 18:03:57 +02:00
from c3nav.mapdata.models.locations import SpecificLocation
2017-08-05 14:17:24 +02:00
from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon
2017-06-11 14:43:14 +02:00
class LevelGeometryMixin(GeometryMixin):
level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level'))
class Meta:
abstract = True
def get_geojson_properties(self, *args, instance=None, **kwargs) -> dict:
result = super().get_geojson_properties(*args, **kwargs)
2017-06-11 14:43:14 +02:00
result['level'] = self.level_id
if hasattr(self, 'get_color'):
color = self.get_color(instance=instance)
if color:
result['color'] = color
if hasattr(self, 'opacity'):
result['opacity'] = self.opacity
2017-05-11 19:36:49 +02:00
return result
2017-06-11 14:43:14 +02:00
def _serialize(self, level=True, **kwargs):
2017-05-11 19:36:49 +02:00
result = super()._serialize(**kwargs)
2017-06-11 14:43:14 +02:00
if level:
result['level'] = self.level_id
return result
2017-06-11 14:43:14 +02:00
class Building(LevelGeometryMixin, models.Model):
"""
The outline of a building on a specific level
"""
geometry = GeometryField('polygon')
class Meta:
verbose_name = _('Building')
verbose_name_plural = _('Buildings')
default_related_name = 'buildings'
2017-06-11 14:43:14 +02:00
class Space(SpecificLocation, LevelGeometryMixin, models.Model):
"""
2017-06-11 14:43:14 +02:00
An accessible space. Shouldn't overlap with spaces on the same level.
"""
2017-05-09 09:36:08 +02:00
geometry = GeometryField('polygon')
outside = models.BooleanField(default=False, verbose_name=_('only outside of building'))
class Meta:
2017-05-09 13:48:55 +02:00
verbose_name = _('Space')
verbose_name_plural = _('Spaces')
default_related_name = 'spaces'
2017-07-13 23:49:00 +02:00
class Door(AccessRestrictionMixin, LevelGeometryMixin, models.Model):
"""
2017-05-09 13:48:55 +02:00
A connection between two spaces
"""
geometry = GeometryField('polygon')
class Meta:
verbose_name = _('Door')
verbose_name_plural = _('Doors')
default_related_name = 'doors'
2017-08-05 11:56:29 +02:00
class AltitudeArea(LevelGeometryMixin, models.Model):
"""
An altitude area
"""
geometry = GeometryField('polygon')
altitude = models.DecimalField(_('altitude'), null=False, max_digits=6, decimal_places=2)
class Meta:
verbose_name = _('Altitude Area')
verbose_name_plural = _('Altitude Areas')
default_related_name = 'altitudeareas'
2017-08-05 14:17:24 +02:00
@classmethod
def recalculate(cls):
# collect location areas
all_areas = []
for level in Level.objects.prefetch_related('buildings', 'doors', 'spaces', 'spaces__columns',
'spaces__obstacles', 'spaces__lineobstacles', 'spaces__holes',
'spaces__stairs', 'spaces__altitudemarkers'):
2017-08-05 14:17:24 +02:00
areas = []
stairs = []
spaces = {}
# collect all accessible areas on this level
2017-08-05 14:17:24 +02:00
buildings_geom = cascaded_union(tuple(building.geometry for building in level.buildings.all()))
for space in level.spaces.all():
if space.outside:
space.geometry = space.geometry.difference(buildings_geom)
spaces[space.pk] = space
area = space.geometry
buffered = space.geometry.buffer(0.0001)
remove = cascaded_union(tuple(c.geometry for c in space.columns.all()) +
tuple(o.geometry for o in space.obstacles.all()) +
tuple(o.buffered_geometry for o in space.lineobstacles.all()) +
tuple(h.geometry for h in space.holes.all()))
areas.extend(assert_multipolygon(space.geometry.difference(remove)))
for stair in space.stairs.all():
substairs = tuple(assert_multilinestring(stair.geometry.intersection(buffered).difference(remove)))
for substair in substairs:
substair.space = space.pk
stairs.extend(substairs)
2017-08-05 14:17:24 +02:00
areas = assert_multipolygon(cascaded_union(areas+list(door.geometry for door in level.doors.all())))
areas = [AltitudeArea(geometry=area, level=level) for area in areas]
space_areas = {space.pk: [] for space in level.spaces.all()}
# assign spaces to areas
2017-08-05 14:17:24 +02:00
for area in areas:
area.spaces = set()
area.connected_to = []
for space in level.spaces.all():
if area.geometry.intersects(space.geometry):
area.spaces.add(space.pk)
space_areas[space.pk].append(area)
2017-08-05 14:17:24 +02:00
# divide areas using stairs
2017-08-05 14:17:24 +02:00
for stair in stairs:
for area in space_areas[stair.space]:
if stair.space not in area.spaces or not stair.intersects(area.geometry):
2017-08-05 14:17:24 +02:00
continue
divided = assert_multipolygon(area.geometry.difference(stair.buffer(0.0001)))
if len(divided) > 2:
raise ValueError
area.geometry = divided[0]
if len(divided) == 2:
new_area = AltitudeArea(geometry=divided[1], level=level)
new_area.spaces = []
2017-08-05 14:17:24 +02:00
new_area.connected_to = [area]
area.connected_to.append(new_area)
areas.append(new_area)
for subarea in (area, new_area):
if len(subarea.spaces) > 1:
spaces_before = subarea.spaces
subarea.spaces = set(space for space in area.spaces
if spaces[space].geometry.intersects(subarea.geometry))
for space in spaces_before-subarea.spaces:
space_areas[space].remove(subarea)
for space in subarea.spaces-spaces_before:
space_areas[space].append(subarea)
2017-08-05 14:17:24 +02:00
break
all_areas.extend(areas)