move obstacle altitude code etc. from scad render to compilealtitudes
This commit is contained in:
parent
ee5dea5337
commit
2cdef6e188
3 changed files with 105 additions and 77 deletions
|
@ -6,7 +6,7 @@ from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from shapely import validation
|
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 shapely.geometry.base import BaseGeometry
|
||||||
|
|
||||||
from c3nav.mapdata.utils.geometry import clean_geometry
|
from c3nav.mapdata.utils.geometry import clean_geometry
|
||||||
|
@ -30,8 +30,9 @@ class GeometryField(models.TextField):
|
||||||
def __init__(self, geomtype=None, default=None):
|
def __init__(self, geomtype=None, default=None):
|
||||||
if geomtype == 'polyline':
|
if geomtype == 'polyline':
|
||||||
geomtype = 'linestring'
|
geomtype = 'linestring'
|
||||||
if geomtype not in (None, 'polygon', 'linestring', 'point'):
|
if geomtype not in (None, 'polygon', 'multipolygon', 'linestring', 'point'):
|
||||||
raise ValueError('GeometryField.geomtype has to be None, "polygon", "linestring", "point"')
|
raise ValueError('GeometryField.geomtype has to be '
|
||||||
|
'None, "polygon", "multipolygon", "linestring" or "point"')
|
||||||
self.geomtype = geomtype
|
self.geomtype = geomtype
|
||||||
super().__init__(default=default)
|
super().__init__(default=default)
|
||||||
|
|
||||||
|
@ -64,6 +65,8 @@ class GeometryField(models.TextField):
|
||||||
def _validate_geomtype(self, value, exception: typing.Type[Exception]=ValidationError):
|
def _validate_geomtype(self, value, exception: typing.Type[Exception]=ValidationError):
|
||||||
if self.geomtype == 'polygon' and not isinstance(value, Polygon):
|
if self.geomtype == 'polygon' and not isinstance(value, Polygon):
|
||||||
raise exception('Expected Polygon instance, got %s instead.' % repr(value))
|
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):
|
elif self.geomtype == 'linestring' and not isinstance(value, LineString):
|
||||||
raise exception('Expected LineString instance, got %s instead.' % repr(value))
|
raise exception('Expected LineString instance, got %s instead.' % repr(value))
|
||||||
elif self.geomtype == 'point' and not isinstance(value, Point):
|
elif self.geomtype == 'point' and not isinstance(value, Point):
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import itertools
|
||||||
from operator import attrgetter, itemgetter
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 shapely.ops import cascaded_union
|
||||||
|
|
||||||
from c3nav.mapdata.fields import GeometryField
|
from c3nav.mapdata.fields import GeometryField
|
||||||
|
@ -79,7 +81,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
An altitude area
|
An altitude area
|
||||||
"""
|
"""
|
||||||
geometry = GeometryField('polygon')
|
geometry = GeometryField('multipolygon')
|
||||||
altitude = models.DecimalField(_('altitude'), null=False, max_digits=6, decimal_places=2)
|
altitude = models.DecimalField(_('altitude'), null=False, max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -93,15 +95,17 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
|
||||||
all_areas = []
|
all_areas = []
|
||||||
space_areas = {}
|
space_areas = {}
|
||||||
spaces = {}
|
spaces = {}
|
||||||
for level in Level.objects.prefetch_related('buildings', 'doors', 'spaces', 'spaces__columns',
|
levels = Level.objects.prefetch_related('buildings', 'doors', 'spaces', 'spaces__columns',
|
||||||
'spaces__obstacles', 'spaces__lineobstacles', 'spaces__holes',
|
'spaces__obstacles', 'spaces__lineobstacles', 'spaces__holes',
|
||||||
'spaces__stairs', 'spaces__altitudemarkers'):
|
'spaces__stairs', 'spaces__altitudemarkers')
|
||||||
|
for level in levels:
|
||||||
areas = []
|
areas = []
|
||||||
stairs = []
|
stairs = []
|
||||||
|
|
||||||
# collect all accessible areas on this level
|
# collect all accessible areas on this level
|
||||||
buildings_geom = cascaded_union(tuple(building.geometry for building in level.buildings.all()))
|
buildings_geom = cascaded_union(tuple(building.geometry for building in level.buildings.all()))
|
||||||
for space in level.spaces.all():
|
for space in level.spaces.all():
|
||||||
|
space.orig_geometry = space.geometry
|
||||||
if space.outside:
|
if space.outside:
|
||||||
space.geometry = space.geometry.difference(buildings_geom)
|
space.geometry = space.geometry.difference(buildings_geom)
|
||||||
spaces[space.pk] = space
|
spaces[space.pk] = space
|
||||||
|
@ -308,15 +312,95 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
|
||||||
area = areas[tmpid]
|
area = areas[tmpid]
|
||||||
area.altitude = area.level.base_altitude
|
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
|
# save to database
|
||||||
from c3nav.mapdata.models import MapUpdate
|
from c3nav.mapdata.models import MapUpdate
|
||||||
with MapUpdate.lock():
|
with MapUpdate.lock():
|
||||||
areas_to_save = set(range(len(areas)))
|
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')
|
all_candidates = AltitudeArea.objects.select_related('level')
|
||||||
for candidate in all_candidates:
|
for candidate in all_candidates:
|
||||||
candidate.area = candidate.geometry.area
|
candidate.area = candidate.geometry.area
|
||||||
|
@ -335,7 +419,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
|
||||||
break
|
break
|
||||||
|
|
||||||
if new_area is None:
|
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())]
|
for tmpid in level_areas.get(candidate.level, set())]
|
||||||
potential_areas = [(tmpid, size) for tmpid, size in potential_areas
|
potential_areas = [(tmpid, size) for tmpid, size in potential_areas
|
||||||
if candidate.area and size/candidate.area > 0.9]
|
if candidate.area and size/candidate.area > 0.9]
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import os
|
import os
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from operator import itemgetter
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from shapely.affinity import scale
|
from shapely.geometry import JOIN_STYLE
|
||||||
from shapely.geometry import JOIN_STYLE, LineString
|
|
||||||
from shapely.ops import cascaded_union
|
from shapely.ops import cascaded_union
|
||||||
|
|
||||||
from c3nav.mapdata.models.locations import SpecificLocation
|
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.scad import polygon_scad
|
||||||
from c3nav.mapdata.utils.svg import SVGImage
|
from c3nav.mapdata.utils.svg import SVGImage
|
||||||
|
|
||||||
|
@ -193,67 +191,10 @@ class Level(SpecificLocation, models.Model):
|
||||||
'stairs', 'obstacles', 'lineobstacles'
|
'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():
|
for area in self.altitudeareas.all():
|
||||||
areas_by_altitude.setdefault(area.altitude, []).append(area.geometry.buffer(0.01))
|
f.write('translate([0, 0, %.2f]) ' % (area.altitude-Decimal('0.5')))
|
||||||
areas_by_altitude = {altitude: [cascaded_union(areas)] for altitude, areas in areas_by_altitude.items()}
|
f.write('linear_extrude(height=0.5, center=false, convexity=20) ')
|
||||||
|
f.write(polygon_scad(area.geometry) + ';\n')
|
||||||
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')
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render_scad_all(cls, request=None):
|
def render_scad_all(cls, request=None):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue