fix more bugs caused by updates, especially using unwrap_geom

This commit is contained in:
Laura Klünder 2023-07-24 11:51:25 +02:00
parent 1837c49ab8
commit 9618d7304f
9 changed files with 59 additions and 38 deletions

View file

@ -22,6 +22,7 @@ from c3nav.editor.views.base import etag_func
from c3nav.mapdata.api import api_etag from c3nav.mapdata.api import api_etag
from c3nav.mapdata.models import Area, MapUpdate, Source from c3nav.mapdata.models import Area, MapUpdate, Source
from c3nav.mapdata.models.geometry.space import POI from c3nav.mapdata.models.geometry.space import POI
from c3nav.mapdata.utils.geometry import unwrap_geom
from c3nav.mapdata.utils.user import can_access_editor from c3nav.mapdata.utils.user import can_access_editor
@ -69,7 +70,7 @@ class EditorViewSet(EditorViewSetMixin, ViewSet):
@staticmethod @staticmethod
def _get_level_geometries(level): def _get_level_geometries(level):
buildings = level.buildings.all() buildings = level.buildings.all()
buildings_geom = unary_union([building.geometry.wrapped_geom for building in buildings]) buildings_geom = unary_union([unwrap_geom(building.geometry) for building in buildings])
spaces = {space.pk: space for space in level.spaces.all()} spaces = {space.pk: space for space in level.spaces.all()}
holes_geom = [] holes_geom = []
for space in spaces.values(): for space in spaces.values():
@ -77,9 +78,9 @@ class EditorViewSet(EditorViewSetMixin, ViewSet):
space.geometry = space.geometry.difference(buildings_geom) space.geometry = space.geometry.difference(buildings_geom)
columns = [column.geometry for column in space.columns.all()] columns = [column.geometry for column in space.columns.all()]
if columns: if columns:
columns_geom = unary_union([column.geometry.wrapped_geom for column in space.columns.all()]) columns_geom = unary_union([unwrap_geom(column.geometry) for column in space.columns.all()])
space.geometry = space.geometry.difference(columns_geom) space.geometry = space.geometry.difference(columns_geom)
holes = [hole.geometry.wrapped_geom for hole in space.holes.all()] holes = [unwrap_geom(hole.geometry) for hole in space.holes.all()]
if holes: if holes:
space_holes_geom = unary_union(holes) space_holes_geom = unary_union(holes)
holes_geom.append(space_holes_geom.intersection(space.geometry)) holes_geom.append(space_holes_geom.intersection(space.geometry))
@ -221,7 +222,10 @@ class EditorViewSet(EditorViewSetMixin, ViewSet):
if request.user_permissions.can_access_base_mapdata: if request.user_permissions.can_access_base_mapdata:
doors = [door for door in level.doors.filter(Door.q_for_request(request)).all() doors = [door for door in level.doors.filter(Door.q_for_request(request)).all()
if door.geometry.intersects(space.geometry)] if door.geometry.intersects(space.geometry)]
doors_space_geom = unary_union([door.geometry for door in doors]+[space.geometry]) doors_space_geom = unary_union(
[unwrap_geom(door.geometry) for door in doors] +
[unwrap_geom(space.geometry)]
)
levels, levels_on_top, levels_under = self._get_levels_pk(request, level.primary_level) levels, levels_on_top, levels_under = self._get_levels_pk(request, level.primary_level)
if level.on_top_of_id is not None: if level.on_top_of_id is not None:

View file

@ -41,7 +41,7 @@ class EditorFormBase(I18nModelFormMixin, ModelForm):
# hide geometry widget # hide geometry widget
self.fields['geometry'].widget = HiddenInput() self.fields['geometry'].widget = HiddenInput()
if not creating: if not creating:
self.initial['geometry'] = json.dumps(mapping(self.instance.geometry), separators=(',', ':')) self.initial['geometry'] = mapping(self.instance.geometry)
if self._meta.model.__name__ == 'Source' and self.request.user.is_superuser: if self._meta.model.__name__ == 'Source' and self.request.user.is_superuser:
Source = self.request.changeset.wrap_model('Source') Source = self.request.changeset.wrap_model('Source')

View file

@ -55,6 +55,9 @@ class GeometryField(models.JSONField):
def to_python(self, value): def to_python(self, value):
if value is None or value == '': if value is None or value == '':
return None return None
if isinstance(value, str):
# todo: this is all too complex, why do we need this?
value = json.loads(value)
try: try:
geometry = shape(value) geometry = shape(value)
except Exception: except Exception:

View file

@ -9,7 +9,7 @@ from shapely.geometry.base import BaseGeometry
from shapely.ops import unary_union from shapely.ops import unary_union
from c3nav.mapdata.models.base import SerializableMixin from c3nav.mapdata.models.base import SerializableMixin
from c3nav.mapdata.utils.geometry import assert_multipolygon, good_representative_point, smart_mapping from c3nav.mapdata.utils.geometry import assert_multipolygon, good_representative_point, smart_mapping, unwrap_geom
from c3nav.mapdata.utils.json import format_geojson from c3nav.mapdata.utils.json import format_geojson
geometry_affecting_fields = ('height', 'width', 'access_restriction') geometry_affecting_fields = ('height', 'width', 'access_restriction')
@ -119,11 +119,11 @@ class GeometryMixin(SerializableMixin):
return True return True
if self.geometry is self.orig_geometry: if self.geometry is self.orig_geometry:
return False return False
if not self.geometry.equals_exact(self.orig_geometry, 0.05): if not self.geometry.equals_exact(unwrap_geom(self.orig_geometry), 0.05):
return True return True
field = self._meta.get_field('geometry') field = self._meta.get_field('geometry')
rounded = field.to_python(field.get_prep_value(self.geometry)) rounded = field.to_python(field.get_prep_value(self.geometry))
if not rounded.equals_exact(self.orig_geometry, 0.005): if not rounded.equals_exact(unwrap_geom(self.orig_geometry), 0.005):
return True return True
return False return False
@ -132,7 +132,7 @@ class GeometryMixin(SerializableMixin):
new_geometry = field.get_final_value(self.geometry) new_geometry = field.get_final_value(self.geometry)
if self.orig_geometry is None: if self.orig_geometry is None:
return new_geometry return new_geometry
difference = new_geometry.symmetric_difference(self.orig_geometry) difference = new_geometry.symmetric_difference(unwrap_geom(self.orig_geometry))
if self._meta.get_field('geometry').geomtype in ('polygon', 'multipolygon'): if self._meta.get_field('geometry').geomtype in ('polygon', 'multipolygon'):
difference = unary_union(assert_multipolygon(difference)) difference = unary_union(assert_multipolygon(difference))
return difference return difference

View file

@ -26,7 +26,7 @@ from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.locations import SpecificLocation from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.cache.changes import changed_geometries
from c3nav.mapdata.utils.geometry import (assert_multilinestring, assert_multipolygon, clean_cut_polygon, from c3nav.mapdata.utils.geometry import (assert_multilinestring, assert_multipolygon, clean_cut_polygon,
cut_polygon_with_line) cut_polygon_with_line, unwrap_geom)
class LevelGeometryMixin(GeometryMixin): class LevelGeometryMixin(GeometryMixin):
@ -222,7 +222,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
stairs = [] stairs = []
# collect all accessible areas on this level # collect all accessible areas on this level
buildings_geom = unary_union(tuple(building.geometry.wrapped_geom for building in level.buildings.all())) buildings_geom = unary_union(tuple(unwrap_geom(building.geometry) for building in level.buildings.all()))
for space in level.spaces.all(): for space in level.spaces.all():
spaces[space.pk] = space spaces[space.pk] = space
space.orig_geometry = space.geometry space.orig_geometry = space.geometry
@ -271,9 +271,9 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
space_areas.update({space.pk: [] for space in level.spaces.all()}) space_areas.update({space.pk: [] for space in level.spaces.all()})
for area in areas: for area in areas:
area.spaces = set() area.spaces = set()
area.geometry_prep = prepared.prep(area.geometry) area.geometry_prep = prepared.prep(unwrap_geom(area.geometry))
for space in level.spaces.all(): for space in level.spaces.all():
if area.geometry_prep.intersects(space.geometry): if area.geometry_prep.intersects(unwrap_geom(space.geometry)):
area.spaces.add(space.pk) area.spaces.add(space.pk)
space_areas[space.pk].append(area) space_areas[space.pk].append(area)
@ -470,9 +470,11 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
for space in level.spaces.all(): for space in level.spaces.all():
space.geometry = space.orig_geometry space.geometry = space.orig_geometry
buildings_geom = unary_union(tuple(b.geometry.wrapped_geom for b in level.buildings.all())) buildings_geom = unary_union(tuple(unwrap_geom(b.geometry) for b in level.buildings.all()))
doors_geom = unary_union(tuple(d.geometry for d in level.doors.all())) doors_geom = unary_union(tuple(d.geometry for d in level.doors.all()))
space_geom = unary_union(tuple((s.geometry if not s.outside else s.geometry.difference(buildings_geom)) space_geom = unary_union(tuple((unwrap_geom(s.geometry)
if not s.outside
else s.geometry.difference(buildings_geom))
for s in level.spaces.all())) for s in level.spaces.all()))
# accessible area on this level is doors + spaces - holes # accessible area on this level is doors + spaces - holes
@ -494,7 +496,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
space_geom = space.geometry space_geom = space.geometry
if space.outside: if space.outside:
space_geom = space_geom.difference(buildings_geom) space_geom = space_geom.difference(buildings_geom)
space_geom_prep = prepared.prep(space_geom) space_geom_prep = prepared.prep(unwrap_geom(space_geom))
holes_geom = unary_union(tuple(h.geometry for h in space.holes.all())) holes_geom = unary_union(tuple(h.geometry for h in space.holes.all()))
# remaining_space means remaining space (=obstacles) that still needs to be added to altitude areas # remaining_space means remaining space (=obstacles) that still needs to be added to altitude areas
@ -572,7 +574,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
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
candidate.geometry_prep = prepared.prep(candidate.geometry) candidate.geometry_prep = prepared.prep(unwrap_geom(candidate.geometry))
all_candidates = sorted(all_candidates, key=attrgetter('area'), reverse=True) all_candidates = sorted(all_candidates, key=attrgetter('area'), reverse=True)
num_modified = 0 num_modified = 0
@ -606,7 +608,7 @@ class AltitudeArea(LevelGeometryMixin, models.Model):
num_deleted += 1 num_deleted += 1
continue continue
if not field.get_final_value(new_area.geometry).equals_exact(candidate.geometry, 0.00001): if not field.get_final_value(new_area.geometry).equals_exact(unwrap_geom(candidate.geometry), 0.00001):
num_modified += 1 num_modified += 1
candidate.geometry = new_area.geometry candidate.geometry = new_area.geometry

View file

@ -13,7 +13,7 @@ from c3nav.mapdata.render.geometry.altitudearea import AltitudeAreaGeometries
from c3nav.mapdata.render.geometry.hybrid import HybridGeometry from c3nav.mapdata.render.geometry.hybrid import HybridGeometry
from c3nav.mapdata.render.geometry.mesh import Mesh from c3nav.mapdata.render.geometry.mesh import Mesh
from c3nav.mapdata.utils.cache import AccessRestrictionAffected from c3nav.mapdata.utils.cache import AccessRestrictionAffected
from c3nav.mapdata.utils.geometry import get_rings from c3nav.mapdata.utils.geometry import get_rings, unwrap_geom
from c3nav.mapdata.utils.mesh import triangulate_rings from c3nav.mapdata.utils.mesh import triangulate_rings
empty_geometry_collection = GeometryCollection() empty_geometry_collection = GeometryCollection()
@ -65,7 +65,7 @@ class LevelGeometries:
@classmethod @classmethod
def build_for_level(cls, level, altitudeareas_above): def build_for_level(cls, level, altitudeareas_above):
geoms = LevelGeometries() geoms = LevelGeometries()
buildings_geom = unary_union([b.geometry.wrapped_geom for b in level.buildings.all()]) buildings_geom = unary_union([unwrap_geom(b.geometry) for b in level.buildings.all()])
geoms.buildings = buildings_geom geoms.buildings = buildings_geom
buildings_geom_prep = prepared.prep(buildings_geom) buildings_geom_prep = prepared.prep(buildings_geom)
@ -89,10 +89,10 @@ class LevelGeometries:
space.holes_geom = empty_geometry_collection space.holes_geom = empty_geometry_collection
space.walkable_geom = space.geometry space.walkable_geom = space.geometry
spaces_geom = unary_union([s.geometry for s in level.spaces.all()]) spaces_geom = unary_union([unwrap_geom(s.geometry) for s in level.spaces.all()])
doors_geom = unary_union([d.geometry for d in level.doors.all()]) doors_geom = unary_union([unwrap_geom(d.geometry) for d in level.doors.all()])
doors_geom = doors_geom.intersection(buildings_geom) doors_geom = doors_geom.intersection(buildings_geom)
walkable_spaces_geom = unary_union([s.walkable_geom for s in level.spaces.all()]) walkable_spaces_geom = unary_union([unwrap_geom(s.walkable_geom) for s in level.spaces.all()])
geoms.doors = doors_geom.difference(walkable_spaces_geom) geoms.doors = doors_geom.difference(walkable_spaces_geom)
if level.on_top_of_id is None: if level.on_top_of_id is None:
geoms.holes = unary_union([s.holes_geom for s in level.spaces.all()]) geoms.holes = unary_union([s.holes_geom for s in level.spaces.all()])
@ -127,7 +127,9 @@ class LevelGeometries:
buffered.difference(buildings_geom) buffered.difference(buildings_geom)
) )
colors.setdefault(space.get_color_sorted(), {}).setdefault(access_restriction, []).append(space.geometry) colors.setdefault(space.get_color_sorted(), {}).setdefault(access_restriction, []).append(
unwrap_geom(space.geometry)
)
for area in space.areas.all(): for area in space.areas.all():
access_restriction = area.access_restriction_id or space.access_restriction_id access_restriction = area.access_restriction_id or space.access_restriction_id
@ -166,7 +168,9 @@ class LevelGeometries:
geoms.ramps.extend(ramp.geometry for ramp in space.ramps.all()) geoms.ramps.extend(ramp.geometry for ramp in space.ramps.all())
heightareas.setdefault(int((space.height or level.default_height)*1000), []).append(space.geometry) heightareas.setdefault(int((space.height or level.default_height)*1000), []).append(
unwrap_geom(space.geometry)
)
colors.pop(None, None) colors.pop(None, None)
# merge ground colors # merge ground colors
@ -178,8 +182,8 @@ class LevelGeometries:
# add altitudegroup geometries and split ground colors into them # add altitudegroup geometries and split ground colors into them
for altitudearea in level.altitudeareas.all(): for altitudearea in level.altitudeareas.all():
altitudearea_prep = prepared.prep(altitudearea.geometry) altitudearea_prep = prepared.prep(unwrap_geom(altitudearea.geometry))
altitudearea_colors = {color: {access_restriction: area.intersection(altitudearea.geometry) altitudearea_colors = {color: {access_restriction: area.intersection(unwrap_geom(altitudearea.geometry))
for access_restriction, area in areas.items() for access_restriction, area in areas.items()
if altitudearea_prep.intersects(area)} if altitudearea_prep.intersects(area)}
for color, areas in colors.items()} for color, areas in colors.items()}

View file

@ -16,7 +16,7 @@ from c3nav.mapdata.models import Level, MapUpdate, Source
from c3nav.mapdata.render.geometry import AltitudeAreaGeometries, LevelGeometries from c3nav.mapdata.render.geometry import AltitudeAreaGeometries, LevelGeometries
from c3nav.mapdata.utils.cache import AccessRestrictionAffected, MapHistory from c3nav.mapdata.utils.cache import AccessRestrictionAffected, MapHistory
from c3nav.mapdata.utils.cache.package import CachePackage from c3nav.mapdata.utils.cache.package import CachePackage
from c3nav.mapdata.utils.geometry import get_rings from c3nav.mapdata.utils.geometry import get_rings, unwrap_geom
empty_geometry_collection = GeometryCollection() empty_geometry_collection = GeometryCollection()
@ -24,7 +24,7 @@ empty_geometry_collection = GeometryCollection()
class Cropper: class Cropper:
def __init__(self, geometry=None): def __init__(self, geometry=None):
self.geometry = geometry self.geometry = geometry
self.geometry_prep = None if geometry is None else prepared.prep(geometry) self.geometry_prep = None if geometry is None else prepared.prep(unwrap_geom(geometry))
def intersection(self, other): def intersection(self, other):
if self.geometry is None: if self.geometry is None:
@ -179,7 +179,7 @@ class LevelRenderData:
) if not geom.is_empty) ) if not geom.is_empty)
for altitudearea in old_geoms.altitudeareas: for altitudearea in old_geoms.altitudeareas:
new_geometry = crop_to.intersection(altitudearea.geometry) new_geometry = crop_to.intersection(unwrap_geom(altitudearea.geometry))
if new_geometry.is_empty: if new_geometry.is_empty:
continue continue
new_geometry_prep = prepared.prep(new_geometry) new_geometry_prep = prepared.prep(new_geometry)

View file

@ -40,10 +40,18 @@ class WrappedGeometry():
@cached_property @cached_property
def wrapped_geom(self): def wrapped_geom(self):
if not self.wrapped_geojson['coordinates']: if not self.wrapped_geojson or not self.wrapped_geojson['coordinates']:
return GeometryCollection() return GeometryCollection()
return shapely_shape(self.wrapped_geojson) return shapely_shape(self.wrapped_geojson)
def __getstate__(self):
self.picklable = True
# make sure geometry is cached
if self.wrapped_geojson:
# noinspection PyStatementEffect
self.wrapped_geom
super().__getstate__()
def __getattr__(self, name): def __getattr__(self, name):
if name in ('__getstate__'): if name in ('__getstate__'):
self.picklable = True self.picklable = True
@ -64,8 +72,8 @@ class WrappedGeometry():
return result return result
def unwrap_geometry(geometry): def unwrap_geom(geometry):
return getattr(geometry, 'geom', geometry) return geometry.wrapped_geom if isinstance(geometry, WrappedGeometry) else geometry
def smart_mapping(geometry): def smart_mapping(geometry):

View file

@ -20,7 +20,7 @@ from shapely.ops import unary_union
from c3nav.mapdata.models import AltitudeArea, Area, GraphEdge, Level, LocationGroup, MapUpdate, Space, WayType from c3nav.mapdata.models import AltitudeArea, Area, GraphEdge, Level, LocationGroup, MapUpdate, Space, WayType
from c3nav.mapdata.models.geometry.space import POI, CrossDescription, LeaveDescription from c3nav.mapdata.models.geometry.space import POI, CrossDescription, LeaveDescription
from c3nav.mapdata.models.locations import CustomLocationProxyMixin from c3nav.mapdata.models.locations import CustomLocationProxyMixin
from c3nav.mapdata.utils.geometry import assert_multipolygon, get_rings, good_representative_point from c3nav.mapdata.utils.geometry import assert_multipolygon, get_rings, good_representative_point, unwrap_geom
from c3nav.mapdata.utils.locations import CustomLocation from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.routing.exceptions import LocationUnreachable, NoRouteFound, NotYetRoutable from c3nav.routing.exceptions import LocationUnreachable, NoRouteFound, NotYetRoutable
from c3nav.routing.route import Route from c3nav.routing.route import Route
@ -63,7 +63,7 @@ class Router:
restrictions = {} restrictions = {}
nodes = deque() nodes = deque()
for level in levels_query: for level in levels_query:
buildings_geom = unary_union(tuple(building.geometry.wrapped_geom for building in level.buildings.all())) buildings_geom = unary_union(tuple(unwrap_geom(building.geometry) for building in level.buildings.all()))
nodes_before_count = len(nodes) nodes_before_count = len(nodes)
@ -122,9 +122,9 @@ class Router:
space.areas.add(area.pk) space.areas.add(area.pk)
for area in level.altitudeareas.all(): for area in level.altitudeareas.all():
if not space.geometry_prep.intersects(area.geometry): if not space.geometry_prep.intersects(unwrap_geom(area.geometry)):
continue continue
for subgeom in assert_multipolygon(accessible_geom.intersection(area.geometry)): for subgeom in assert_multipolygon(accessible_geom.intersection(unwrap_geom(area.geometry))):
if subgeom.is_empty: if subgeom.is_empty:
continue continue
area_clear_geom = unary_union(tuple(get_rings(subgeom.difference(obstacles_geom)))) area_clear_geom = unary_union(tuple(get_rings(subgeom.difference(obstacles_geom))))
@ -511,7 +511,7 @@ class BaseRouterProxy:
@cached_property @cached_property
def geometry_prep(self): def geometry_prep(self):
return prepared.prep(self.src.geometry) return prepared.prep(unwrap_geom(self.src.geometry))
def __getstate__(self): def __getstate__(self):
result = self.__dict__.copy() result = self.__dict__.copy()