From 2cdef6e18811b9468c8caa4e892da403c226af19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 7 Aug 2017 14:22:32 +0200 Subject: [PATCH] move obstacle altitude code etc. from scad render to compilealtitudes --- src/c3nav/mapdata/fields.py | 9 +- src/c3nav/mapdata/models/geometry/level.py | 104 +++++++++++++++++++-- src/c3nav/mapdata/models/level.py | 69 +------------- 3 files changed, 105 insertions(+), 77 deletions(-) diff --git a/src/c3nav/mapdata/fields.py b/src/c3nav/mapdata/fields.py index cd4f5064..59a6b025 100644 --- a/src/c3nav/mapdata/fields.py +++ b/src/c3nav/mapdata/fields.py @@ -6,7 +6,7 @@ from django.core.validators import RegexValidator from django.db import models from django.utils.translation import ugettext_lazy as _ from shapely import validation -from shapely.geometry import LineString, Point, Polygon, mapping, shape +from shapely.geometry import LineString, MultiPolygon, Point, Polygon, mapping, shape from shapely.geometry.base import BaseGeometry from c3nav.mapdata.utils.geometry import clean_geometry @@ -30,8 +30,9 @@ class GeometryField(models.TextField): def __init__(self, geomtype=None, default=None): if geomtype == 'polyline': geomtype = 'linestring' - if geomtype not in (None, 'polygon', 'linestring', 'point'): - raise ValueError('GeometryField.geomtype has to be None, "polygon", "linestring", "point"') + if geomtype not in (None, 'polygon', 'multipolygon', 'linestring', 'point'): + raise ValueError('GeometryField.geomtype has to be ' + 'None, "polygon", "multipolygon", "linestring" or "point"') self.geomtype = geomtype super().__init__(default=default) @@ -64,6 +65,8 @@ class GeometryField(models.TextField): def _validate_geomtype(self, value, exception: typing.Type[Exception]=ValidationError): if self.geomtype == 'polygon' and not isinstance(value, Polygon): raise exception('Expected Polygon instance, got %s instead.' % repr(value)) + if self.geomtype == 'multipolygon' and not isinstance(value, (Polygon, MultiPolygon)): + raise exception('Expected Polygon or MultiPolygon instance, got %s instead.' % repr(value)) elif self.geomtype == 'linestring' and not isinstance(value, LineString): raise exception('Expected LineString instance, got %s instead.' % repr(value)) elif self.geomtype == 'point' and not isinstance(value, Point): diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index 3554259b..99b82ecc 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -1,9 +1,11 @@ +import itertools from operator import attrgetter, itemgetter from django.db import models from django.db.models import F from django.utils.translation import ugettext_lazy as _ -from shapely.geometry import JOIN_STYLE +from shapely.affinity import scale +from shapely.geometry import JOIN_STYLE, LineString from shapely.ops import cascaded_union from c3nav.mapdata.fields import GeometryField @@ -79,7 +81,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model): """ An altitude area """ - geometry = GeometryField('polygon') + geometry = GeometryField('multipolygon') altitude = models.DecimalField(_('altitude'), null=False, max_digits=6, decimal_places=2) class Meta: @@ -93,15 +95,17 @@ class AltitudeArea(LevelGeometryMixin, models.Model): all_areas = [] space_areas = {} spaces = {} - for level in Level.objects.prefetch_related('buildings', 'doors', 'spaces', 'spaces__columns', - 'spaces__obstacles', 'spaces__lineobstacles', 'spaces__holes', - 'spaces__stairs', 'spaces__altitudemarkers'): + levels = Level.objects.prefetch_related('buildings', 'doors', 'spaces', 'spaces__columns', + 'spaces__obstacles', 'spaces__lineobstacles', 'spaces__holes', + 'spaces__stairs', 'spaces__altitudemarkers') + for level in levels: areas = [] stairs = [] # collect all accessible areas on this level buildings_geom = cascaded_union(tuple(building.geometry for building in level.buildings.all())) for space in level.spaces.all(): + space.orig_geometry = space.geometry if space.outside: space.geometry = space.geometry.difference(buildings_geom) spaces[space.pk] = space @@ -308,15 +312,95 @@ class AltitudeArea(LevelGeometryMixin, models.Model): area = areas[tmpid] area.altitude = area.level.base_altitude + print(areas) + + level_areas = {} + for area in areas: + level_areas.setdefault(area.level, set()).add(area.tmpid) + + # + # now fill in the obstacles and so on + # + for level in levels: + for space in level.spaces.all(): + space.geometry = space.orig_geometry + + buildings_geom = cascaded_union(tuple(b.geometry for b in level.buildings.all())) + doors_geom = cascaded_union(tuple(d.geometry for d in level.doors.all())) + space_geom = cascaded_union(tuple((s.geometry if not s.outside else s.geometry.difference(buildings_geom)) + for s in level.spaces.all())) + accessible_area = cascaded_union((doors_geom, space_geom)) + for space in level.spaces.all(): + accessible_area = accessible_area.difference(space.geometry.intersection( + cascaded_union(tuple(h.geometry for h in space.holes.all())) + )) + + areas_by_altitude = {} + for tmpid in level_areas.get(level, []): + area = areas[tmpid] + areas_by_altitude.setdefault(area.altitude, []).append(area.geometry.buffer(0.01)) + areas_by_altitude = {altitude: [cascaded_union(alt_areas)] + for altitude, alt_areas in areas_by_altitude.items()} + + accessible_area = accessible_area.difference( + cascaded_union(tuple(itertools.chain(*areas_by_altitude.values()))) + ) + + stairs = [] + for space in level.spaces.all(): + geom = space.geometry + if space.outside: + geom = space_geom.difference(buildings_geom) + remaining_space = geom.intersection(accessible_area) + if remaining_space.is_empty: + continue + + max_len = ((geom.bounds[0] - geom.bounds[2]) ** 2 + (geom.bounds[1] - geom.bounds[3]) ** 2) ** 0.5 + stairs = [] + for stair in space.stairs.all(): + for substair in assert_multilinestring(stair.geometry): + for coord1, coord2 in zip(tuple(substair.coords)[:-1], tuple(substair.coords)[1:]): + line = LineString([coord1, coord2]) + fact = (max_len * 3) / line.length + scaled = scale(line, xfact=fact, yfact=fact) + stairs.append(scaled.buffer(0.0001, JOIN_STYLE.mitre).intersection(geom.buffer(0.0001))) + if stairs: + stairs = cascaded_union(stairs) + remaining_space = remaining_space.difference(stairs) + + for polygon in assert_multipolygon(remaining_space.buffer(0)): + center = polygon.centroid + buffered = polygon.buffer(0.001, JOIN_STYLE.mitre) + touches = tuple((altitude, buffered.intersection(alt_areas[0]).area) + for altitude, alt_areas in areas_by_altitude.items() + if buffered.intersects(alt_areas[0])) + if touches: + max_intersection = max(touches, key=itemgetter(1))[1] + altitude = max(altitude for altitude, area in touches if area > max_intersection / 2) + else: + altitude = min(areas_by_altitude.items(), key=lambda a: a[1][0].distance(center))[0] + areas_by_altitude[altitude].append(polygon.buffer(0.001, JOIN_STYLE.mitre)) + + # plot_geometry(remaining_space, title=space.title) + + areas_by_altitude = {altitude: cascaded_union(alt_areas) + for altitude, alt_areas in areas_by_altitude.items()} + print(areas_by_altitude) + + level_areas[level] = [AltitudeArea(level=level, geometry=geometry, altitude=altitude) + for altitude, geometry in areas_by_altitude.items()] + + areas = tuple(itertools.chain(*(a for a in level_areas.values()))) + for i, area in enumerate(areas): + area.tmpid = i + for level in levels: + level_areas[level] = set(area.tmpid for area in level_areas.get(level, [])) + # save to database from c3nav.mapdata.models import MapUpdate with MapUpdate.lock(): areas_to_save = set(range(len(areas))) - level_areas = {} - for area in areas: - level_areas.setdefault(area.level, set()).add(area.tmpid) - all_candidates = AltitudeArea.objects.select_related('level') for candidate in all_candidates: candidate.area = candidate.geometry.area @@ -335,7 +419,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model): break if new_area is None: - potential_areas = [(tmpid, areas[tmpid].geometry.intersection(candidate.geometry).area) + potential_areas = [(tmpid, areas[tmpid].geometry.intersection(candidate.geometry.buffer(0)).area) for tmpid in level_areas.get(candidate.level, set())] potential_areas = [(tmpid, size) for tmpid, size in potential_areas if candidate.area and size/candidate.area > 0.9] diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 0768182d..50453a7c 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -1,18 +1,16 @@ import os from decimal import Decimal from itertools import chain -from operator import itemgetter from django.conf import settings from django.db import models from django.db.models import Prefetch from django.utils.translation import ugettext_lazy as _ -from shapely.affinity import scale -from shapely.geometry import JOIN_STYLE, LineString +from shapely.geometry import JOIN_STYLE from shapely.ops import cascaded_union from c3nav.mapdata.models.locations import SpecificLocation -from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon +from c3nav.mapdata.utils.geometry import assert_multipolygon from c3nav.mapdata.utils.scad import polygon_scad from c3nav.mapdata.utils.svg import SVGImage @@ -193,67 +191,10 @@ class Level(SpecificLocation, models.Model): 'stairs', 'obstacles', 'lineobstacles' ) - buildings_geom = cascaded_union(tuple(b.geometry for b in self.buildings.all())) - doors_geom = cascaded_union(tuple(d.geometry for d in self.doors.all())) - space_geom = cascaded_union(tuple((s.geometry if not s.outside else s.geometry.difference(buildings_geom)) - for s in self.spaces.all())) - accessible_area = cascaded_union((doors_geom, space_geom)) - for space in spaces: - accessible_area = accessible_area.difference(space.geometry.intersection( - cascaded_union(tuple(h.geometry for h in space.holes.all())) - )) - - areas_by_altitude = {} for area in self.altitudeareas.all(): - areas_by_altitude.setdefault(area.altitude, []).append(area.geometry.buffer(0.01)) - areas_by_altitude = {altitude: [cascaded_union(areas)] for altitude, areas in areas_by_altitude.items()} - - accessible_area = accessible_area.difference(cascaded_union(tuple(chain(*areas_by_altitude.values())))) - - stairs = [] - for space in spaces: - geom = space.geometry - if space.outside: - geom = space_geom.difference(buildings_geom) - remaining_space = geom.intersection(accessible_area) - if remaining_space.is_empty: - continue - - max_len = ((geom.bounds[0]-geom.bounds[2])**2 + (geom.bounds[1]-geom.bounds[3])**2)**0.5 - stairs = [] - for stair in space.stairs.all(): - for substair in assert_multilinestring(stair.geometry): - for coord1, coord2 in zip(tuple(substair.coords)[:-1], tuple(substair.coords)[1:]): - line = LineString([coord1, coord2]) - fact = (max_len*3) / line.length - scaled = scale(line, xfact=fact, yfact=fact) - stairs.append(scaled.buffer(0.0001, JOIN_STYLE.mitre).intersection(geom.buffer(0.0001))) - if stairs: - stairs = cascaded_union(stairs) - remaining_space = remaining_space.difference(stairs) - - for polygon in assert_multipolygon(remaining_space.buffer(0)): - center = polygon.centroid - buffered = polygon.buffer(0.001, JOIN_STYLE.mitre) - touches = tuple((altitude, buffered.intersection(areas[0]).area) - for altitude, areas in areas_by_altitude.items() - if buffered.intersects(areas[0])) - if touches: - max_intersection = max(touches, key=itemgetter(1))[1] - altitude = max(altitude for altitude, area in touches if area > max_intersection/2) - else: - altitude = min(areas_by_altitude.items(), key=lambda a: a[1][0].distance(center))[0] - areas_by_altitude[altitude].append(polygon.buffer(0.001, JOIN_STYLE.mitre)) - - # plot_geometry(remaining_space, title=space.title) - - areas_by_altitude = {altitude: [cascaded_union(areas)] for altitude, areas in areas_by_altitude.items()} - - for altitude, areas in areas_by_altitude.items(): - for area in areas: - f.write('translate([0, 0, %.2f]) ' % (altitude-Decimal('0.5'))) - f.write('linear_extrude(height=0.5, center=false, convexity=20) ') - f.write(polygon_scad(area) + ';\n') + f.write('translate([0, 0, %.2f]) ' % (area.altitude-Decimal('0.5'))) + f.write('linear_extrude(height=0.5, center=false, convexity=20) ') + f.write(polygon_scad(area.geometry) + ';\n') @classmethod def render_scad_all(cls, request=None):