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

329 lines
16 KiB
Python
Raw Normal View History

2017-08-06 22:07:34 +02:00
import os
from decimal import Decimal
2017-06-11 00:00:25 +02:00
from itertools import chain
2017-08-08 13:29:24 +02:00
from operator import attrgetter, itemgetter
2017-06-11 00:00:25 +02:00
2017-05-12 23:37:03 +02:00
from django.conf import settings
2016-08-29 18:49:24 +02:00
from django.db import models
from django.db.models import Prefetch
2017-08-07 14:55:08 +02:00
from django.utils.functional import cached_property
2016-08-29 18:49:24 +02:00
from django.utils.translation import ugettext_lazy as _
2017-08-07 14:55:08 +02:00
from shapely.geometry import JOIN_STYLE, box
2016-11-26 13:48:44 +01:00
from shapely.ops import cascaded_union
2016-08-29 18:49:24 +02:00
2017-05-10 18:03:57 +02:00
from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.utils.geometry import assert_multipolygon
2017-08-07 21:20:50 +02:00
from c3nav.mapdata.utils.scad import add_indent, polygon_scad
2017-05-21 23:41:42 +02:00
from c3nav.mapdata.utils.svg import SVGImage
2016-08-29 18:49:24 +02:00
class Level(SpecificLocation, models.Model):
2016-08-29 18:49:24 +02:00
"""
2017-06-11 14:43:14 +02:00
A map level
2016-08-29 18:49:24 +02:00
"""
2017-08-05 16:49:10 +02:00
base_altitude = models.DecimalField(_('base altitude'), null=False, unique=True, max_digits=6, decimal_places=2)
2017-08-07 15:29:52 +02:00
default_height = models.DecimalField(_('default space height'), max_digits=6, decimal_places=2, default=3.0)
2017-06-11 14:43:14 +02:00
on_top_of = models.ForeignKey('mapdata.Level', null=True, on_delete=models.CASCADE,
related_name='levels_on_top', verbose_name=_('on top of'))
2016-08-29 18:49:24 +02:00
class Meta:
2017-06-11 14:43:14 +02:00
verbose_name = _('Level')
verbose_name_plural = _('Levels')
default_related_name = 'levels'
2017-08-05 16:49:10 +02:00
ordering = ['base_altitude']
2016-09-24 14:09:52 +02:00
2016-11-26 13:48:44 +01:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
2016-12-04 01:49:49 +01:00
2017-06-12 23:33:59 +02:00
def lower(self, level_model=None):
2017-06-11 15:37:25 +02:00
if self.on_top_of_id is not None:
raise TypeError
if level_model is None:
level_model = Level
2017-08-05 16:49:10 +02:00
return level_model.objects.filter(base_altitude__lt=self.base_altitude,
on_top_of__isnull=True).order_by('-base_altitude')
2016-12-04 01:49:49 +01:00
2017-06-12 23:33:59 +02:00
def higher(self, level_model=None):
2017-06-11 15:37:25 +02:00
if self.on_top_of_id is not None:
raise TypeError
if level_model is None:
level_model = Level
2017-08-05 16:49:10 +02:00
return level_model.objects.filter(base_altitude__gt=self.base_altitude,
on_top_of__isnull=True).order_by('base_altitude')
2016-12-04 01:49:49 +01:00
2017-06-11 00:00:25 +02:00
@property
2017-06-11 14:43:14 +02:00
def sublevels(self):
2017-06-11 00:00:25 +02:00
if self.on_top_of is not None:
raise TypeError
2017-06-11 14:43:14 +02:00
return chain((self, ), self.levels_on_top.all())
2017-06-11 00:00:25 +02:00
@property
2017-06-11 14:43:14 +02:00
def sublevel_title(self):
2017-06-11 00:00:25 +02:00
return '-' if self.on_top_of_id is None else self.title
@property
2017-06-11 14:43:14 +02:00
def primary_level(self):
2017-06-11 00:00:25 +02:00
return self if self.on_top_of_id is None else self.on_top_of
2017-06-10 23:04:28 +02:00
@property
2017-06-11 14:43:14 +02:00
def primary_level_pk(self):
2017-06-10 23:04:28 +02:00
return self.pk if self.on_top_of_id is None else self.on_top_of_id
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)
result['on_top_of'] = self.on_top_of_id
2017-08-05 16:49:10 +02:00
result['base_altitude'] = float(str(self.base_altitude))
2017-08-07 15:29:52 +02:00
result['default_height'] = float(str(self.default_heights))
2017-05-11 19:36:49 +02:00
return result
2016-12-01 12:25:02 +01:00
2017-05-13 21:25:57 +02:00
def _render_space_ground(self, svg, space):
areas_by_color = {}
for area in space.areas.all():
areas_by_color.setdefault(area.get_color(), []).append(area)
areas_by_color.pop(None, None)
areas_by_color.pop('', None)
for i, (color, color_areas) in enumerate(areas_by_color.items()):
geometries = cascaded_union(tuple(area.geometry for area in color_areas)).intersection(space.geometry)
svg.add_geometry(geometries, fill_color=color)
stair_geometries = tuple(stair.geometry for stair in space.stairs.all())
svg.add_geometry(cascaded_union(stair_geometries).intersection(space.geometry),
stroke_width=0.06, stroke_color='#000000', opacity=0.15)
for i in range(2):
svg.add_geometry(cascaded_union(tuple(g.parallel_offset(0.06+0.04*i, 'right', join_style=JOIN_STYLE.mitre)
for g in stair_geometries)).intersection(space.geometry),
stroke_width=0.04, stroke_color='#000000', opacity=0.07-0.05*i)
def _render_space_inventory(self, svg, space):
obstacle_geometries = cascaded_union(
tuple(obstacle.geometry for obstacle in space.obstacles.all()) +
tuple(obstacle.buffered_geometry for obstacle in space.lineobstacles.all())
).intersection(space.geometry)
svg.add_geometry(obstacle_geometries, fill_color='#999999')
def render_svg(self, request, effects=True, draw_spaces=None):
2017-08-05 12:13:15 +02:00
from c3nav.mapdata.models import Source, Area, Door, Space
bounds = Source.max_bounds()
svg = SVGImage(bounds=bounds, scale=settings.RENDER_SCALE)
2017-05-12 23:37:03 +02:00
building_geometries = cascaded_union(tuple(b.geometry for b in self.buildings.all()))
spaces = self.spaces.filter(Space.q_for_request(request)).prefetch_related(
Prefetch('areas', Area.qs_for_request(request)),
'groups', 'columns', 'holes', 'areas__groups',
'stairs', 'obstacles', 'lineobstacles'
)
2017-05-12 23:37:03 +02:00
for space in spaces:
2017-06-08 16:21:32 +02:00
if space.outside:
space.geometry = space.geometry.difference(building_geometries)
2017-06-11 13:51:05 +02:00
columns_geom = cascaded_union(tuple(column.geometry for column in space.columns.all()))
holes_geom = cascaded_union(tuple(hole.geometry for hole in space.holes.all()))
space.geometry = space.geometry.difference(columns_geom)
space.hole_geometries = holes_geom.intersection(space.geometry)
2017-06-08 16:21:32 +02:00
2017-06-11 13:51:05 +02:00
space_geometries = cascaded_union(tuple(space.geometry for space in spaces))
hole_geometries = cascaded_union(tuple(space.hole_geometries for space in spaces))
2017-05-13 21:25:57 +02:00
2017-05-13 16:39:01 +02:00
# draw space background
doors = self.doors.filter(Door.q_for_request(request))
door_geometries = cascaded_union(tuple(d.geometry for d in doors))
2017-06-11 14:43:14 +02:00
level_geometry = cascaded_union((space_geometries, building_geometries, door_geometries))
level_geometry = level_geometry.difference(hole_geometries)
level_clip = svg.register_geometry(level_geometry, defid='level', as_clip_path=True)
svg.add_geometry(fill_color='#d1d1d1', clip_path=level_clip)
2017-05-13 16:39:01 +02:00
# color in spaces
spaces_by_color = {}
2017-06-11 13:51:05 +02:00
for space in spaces:
2017-05-13 16:39:01 +02:00
spaces_by_color.setdefault(space.get_color(), []).append(space)
spaces_by_color.pop(None, None)
2017-05-13 20:48:48 +02:00
spaces_by_color.pop('', None)
2017-05-13 16:39:01 +02:00
for i, (color, color_spaces) in enumerate(spaces_by_color.items()):
geometries = cascaded_union(tuple(space.geometry for space in color_spaces))
2017-08-04 18:50:54 +02:00
svg.add_geometry(geometries.intersection(level_geometry), fill_color=color)
2017-05-13 16:39:01 +02:00
2017-06-11 13:51:05 +02:00
for space in spaces:
2017-05-13 21:25:57 +02:00
self._render_space_ground(svg, space)
2017-05-13 16:39:01 +02:00
# calculate walls
2017-06-11 13:51:05 +02:00
wall_geometry = building_geometries.difference(space_geometries).difference(door_geometries)
2017-05-12 23:37:03 +02:00
2017-05-13 16:39:01 +02:00
# draw wall shadow
if effects:
2017-08-03 21:47:56 +02:00
wall_dilated_geometry = wall_geometry.buffer(0.5, join_style=JOIN_STYLE.mitre)
svg.add_geometry(wall_dilated_geometry, fill_color='#000000', opacity=0.1, filter='wallblur',
2017-06-11 14:43:14 +02:00
clip_path=level_clip)
2017-06-11 13:51:05 +02:00
for space in spaces:
2017-05-13 21:25:57 +02:00
self._render_space_inventory(svg, space)
2017-05-13 16:39:01 +02:00
# draw walls
2017-08-03 21:47:56 +02:00
svg.add_geometry(wall_geometry, fill_color='#929292', stroke_color='#333333', stroke_width=0.05)
2017-05-12 23:37:03 +02:00
2017-05-13 16:39:01 +02:00
# draw doors
door_geometries = cascaded_union(tuple(d.geometry for d in doors))
2017-06-11 13:51:05 +02:00
door_geometries = door_geometries.difference(space_geometries)
2017-08-03 21:47:56 +02:00
svg.add_geometry(door_geometries, fill_color='#ffffff', stroke_color='#929292', stroke_width=0.05)
2017-05-12 23:37:03 +02:00
return svg.get_xml()
2017-08-06 22:07:34 +02:00
@staticmethod
def _give_height_to_areas_with_one_neighbor(accessible_area, areas_by_altitude):
# give height to all obstacles that touch only one altitude
remaining_polygons = []
for polygon in assert_multipolygon(accessible_area):
buffered = polygon.buffer(0.001)
found_altitude = None
for altitude, area in areas_by_altitude.items():
if buffered.intersects(area[0]):
if found_altitude is not None:
found_altitude = None
break
found_altitude = altitude
if found_altitude is None:
remaining_polygons.append(polygon)
else:
areas_by_altitude[found_altitude].append(polygon)
return cascaded_union(remaining_polygons)
2017-08-07 14:55:08 +02:00
@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 cascaded_union(tuple(item.geometry.buffer(0)
for item in chain(self.altitudeareas.all(), self.buildings.all()))).bounds
2017-08-07 16:13:35 +02:00
def _render_scad_polygon(self, f, geometry, altitude, height=Decimal('0.0'), low_clip=()):
for low_altitude, low_area in low_clip:
intersection = geometry.intersection(low_area)
if not intersection.is_empty:
geometry = geometry.difference(intersection)
low_height = max(altitude - low_altitude, 0)
2017-08-07 21:20:50 +02:00
total_height = low_height+height
if total_height:
2017-08-07 16:13:35 +02:00
f.write(' ')
f.write('translate([0, 0, %.2f]) ' % (altitude - low_height))
2017-08-07 21:20:50 +02:00
f.write(add_indent(polygon_scad(intersection, total_height))[4:])
2017-08-07 16:13:35 +02:00
if not geometry.is_empty:
f.write(' ')
f.write('translate([0, 0, %.2f]) ' % (altitude - Decimal('0.5')))
2017-08-07 21:20:50 +02:00
f.write(add_indent(polygon_scad(geometry, height+Decimal('0.5')))[4:])
2017-08-07 16:13:35 +02:00
def _render_scad(self, f, low_clip=(), spaces=None, request=None):
2017-08-07 21:20:50 +02:00
f.write(' // '+self.title+'\n')
2017-08-06 22:07:34 +02:00
if spaces is None:
from c3nav.mapdata.models import Area, Space
spaces = self.spaces.filter(Space.q_for_request(request, allow_none=True)).prefetch_related(
Prefetch('areas', Area.qs_for_request(request, allow_none=True)),
'groups', 'columns', 'holes', 'areas__groups',
'stairs', 'obstacles', 'lineobstacles'
)
2017-08-07 21:20:50 +02:00
f.write('')
2017-08-06 22:07:34 +02:00
for area in self.altitudeareas.all():
2017-08-07 16:13:35 +02:00
area.geometry = area.geometry.buffer(0)
self._render_scad_polygon(f, area.geometry, area.altitude, low_clip=low_clip)
draw_obstacles = {}
2017-08-08 13:29:24 +02:00
height_spaces = {}
2017-08-07 16:13:35 +02:00
for space in spaces:
2017-08-08 13:29:24 +02:00
columns = cascaded_union(tuple(columns.geometry for columns in space.columns.all()))
space.geometry = space.geometry.difference(columns)
if self.on_top_of_id is None and not space.outside:
height = space.height or self.default_height
height_spaces.setdefault(height, []).append(space.geometry)
holes = cascaded_union(tuple(hole.geometry for hole in space.holes.all()))
2017-08-07 16:13:35 +02:00
for lineobstacle in space.lineobstacles.all():
lineobstacle.geometry = lineobstacle.buffered_geometry
for obstacle in chain(space.obstacles.all(), space.lineobstacles.all()):
2017-08-08 13:29:24 +02:00
geometry = obstacle.geometry.intersection(space.geometry).difference(holes)
2017-08-07 16:13:35 +02:00
for altitudearea in self.altitudeareas.all():
intersection = geometry.intersection(altitudearea.geometry)
if not intersection.is_empty:
geometry = geometry.difference(intersection.buffer(0.001, join_style=JOIN_STYLE.mitre))
draw_obstacles.setdefault((altitudearea.altitude, obstacle.height), []).append(intersection)
if not geometry.is_empty:
for polygon in assert_multipolygon(geometry):
center = polygon.centroid
altitude = min(self.altitudeareas.all(), key=lambda a: a.geometry.distance(center)).altitude
draw_obstacles.setdefault((altitude, obstacle.height), []).append(polygon)
for (altitude, height), polygons in draw_obstacles.items():
self._render_scad_polygon(f, cascaded_union(polygons), altitude, height, low_clip=low_clip)
2017-08-06 22:07:34 +02:00
2017-08-08 13:29:24 +02:00
spaces_geom = cascaded_union(tuple(space.geometry for space in self.spaces.all() if not space.outside))
buildings_geom = cascaded_union(tuple(building.geometry for building in self.buildings.all()))
doors_geom = cascaded_union(tuple(door.geometry for door in self.doors.all()))
walls_geom = buildings_geom.difference(doors_geom).difference(spaces_geom)
drawn_walls = {}
for height, polygons in sorted(height_spaces.items(), key=itemgetter(0)):
polygons = cascaded_union(polygons)
for area in self.altitudeareas.all():
intersection = area.geometry.intersection(polygons)
if not intersection.is_empty:
walls = intersection.buffer(0.5, join_style=JOIN_STYLE.mitre).intersection(walls_geom)
walls = walls.buffer(0.001, join_style=JOIN_STYLE.mitre)
self._render_scad_polygon(f, walls, area.altitude+height, low_clip=low_clip)
drawn_walls.setdefault(area.altitude+height, []).append(walls)
remaining_walls_geom = walls_geom.difference(cascaded_union(tuple(chain(*drawn_walls.values()))))
drawn_walls = {altitude: cascaded_union(walls) for altitude, walls in drawn_walls.items()}
drawn_walls_sorted = sorted(drawn_walls.items(), key=itemgetter(0))
for wall in assert_multipolygon(remaining_walls_geom):
buffered = wall.buffer(0.001, join_style=JOIN_STYLE.mitre)
try:
altitude = next(iter(altitude for altitude, geom in drawn_walls_sorted if geom.intersects(buffered)))
except StopIteration:
altitude = min(drawn_walls_sorted, key=lambda a: buffered.distance(a[1]))[0]
self._render_scad_polygon(f, buffered, altitude, low_clip=low_clip)
2017-08-08 12:31:55 +02:00
@classmethod
def _render_scad_levels(cls, levels, filename, level_spaces):
bounds = cascaded_union(tuple(box(*level.bounds) for level in levels)).bounds
center = tuple(box(*bounds).centroid.coords[0])
min_altitude = min((level.min_altitude for level in levels), default=0)
filename = os.path.join(settings.RENDER_ROOT, filename)
with open(filename, 'w') as f:
f.write('translate([%.2f, %.2f, %.2f]) {\n' % (0-center[0], 0-center[1], 0-min_altitude+Decimal('0.5')))
first = True
for level in levels:
low_clip = []
if first:
low_clip = [(level.min_altitude-Decimal('0.5'), box(*bounds))]
first = False
level._render_scad(f, spaces=level_spaces.get(level.pk, []), low_clip=low_clip)
f.write('}\n')
2017-08-06 22:07:34 +02:00
@classmethod
2017-08-07 14:55:08 +02:00
def render_scad_all(cls, levels=None, request=None):
2017-08-06 22:07:34 +02:00
from c3nav.mapdata.models import Level, Area, Space
spaces = Space.objects.filter(Space.q_for_request(request, allow_none=True)).prefetch_related(
Prefetch('areas', Area.qs_for_request(request, allow_none=True)),
'groups', 'columns', 'holes', 'areas__groups',
'stairs', 'obstacles', 'lineobstacles'
)
level_spaces = {}
for space in spaces:
level_spaces.setdefault(space.level_id, []).append(space)
2017-08-07 14:55:08 +02:00
if levels is None:
levels = Level.objects
levels = levels.prefetch_related('buildings', 'doors', 'altitudeareas').order_by('base_altitude')
2017-08-07 14:55:08 +02:00
2017-08-08 12:31:55 +02:00
cls._render_scad_levels(levels, 'all.levels.scad', level_spaces)
2017-08-07 14:55:08 +02:00
2017-08-08 12:31:55 +02:00
for level in levels:
if level.on_top_of_id is not None:
continue
sublevels = tuple(sublevel for sublevel in levels if sublevel.on_top_of_id == level.pk)
cls._render_scad_levels((level, )+sublevels, level.get_slug()+'.scad', level_spaces)