From 4d113da65364d1e4ec2dd0800edfcfd0e94b555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 21 Dec 2019 12:17:16 +0100 Subject: [PATCH] cleangeometries & round_coordinates shouldn't return duplicate coordinates --- src/c3nav/mapdata/fields.py | 2 +- .../management/commands/cleangeometries.py | 19 ++++++ src/c3nav/mapdata/models/geometry/base.py | 10 ++-- src/c3nav/mapdata/utils/json.py | 59 ++++++++++++++++--- 4 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/c3nav/mapdata/management/commands/cleangeometries.py diff --git a/src/c3nav/mapdata/fields.py b/src/c3nav/mapdata/fields.py index 7edf1169..a6171cf7 100644 --- a/src/c3nav/mapdata/fields.py +++ b/src/c3nav/mapdata/fields.py @@ -100,7 +100,7 @@ class GeometryField(models.TextField): else: logging.debug('Fixing rounded geometry failed, saving it to the database without rounding.') - return format_geojson(mapping(value), round=False) if as_json else value + return format_geojson(mapping(value), rounded=False) if as_json else value def get_prep_value(self, value): if value is None: diff --git a/src/c3nav/mapdata/management/commands/cleangeometries.py b/src/c3nav/mapdata/management/commands/cleangeometries.py new file mode 100644 index 00000000..a11ce41e --- /dev/null +++ b/src/c3nav/mapdata/management/commands/cleangeometries.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand +from django.db import transaction + +from c3nav.mapdata.models.geometry.base import GeometryMixin +from c3nav.mapdata.utils.models import get_submodels + + +class Command(BaseCommand): + help = 'clean-up/fix all geometries in the database' + + def handle(self, *args, **options): + with transaction.atomic(): + for model in get_submodels(GeometryMixin): + for instance in model.objects.all(): + old_geom = instance.geometry.wrapped_geojson + instance.save() + instance.refresh_from_db() + if instance.geometry.wrapped_geojson != old_geom: + print('Fixed %s' % instance) diff --git a/src/c3nav/mapdata/models/geometry/base.py b/src/c3nav/mapdata/models/geometry/base.py index 351228c7..5a046bf0 100644 --- a/src/c3nav/mapdata/models/geometry/base.py +++ b/src/c3nav/mapdata/models/geometry/base.py @@ -59,11 +59,11 @@ class GeometryMixin(SerializableMixin): result = { 'type': 'Feature', 'properties': self.get_geojson_properties(instance=instance), - 'geometry': format_geojson(smart_mapping(self.geometry), round=False), + 'geometry': format_geojson(smart_mapping(self.geometry), rounded=False), } original_geometry = getattr(self, 'original_geometry', None) if original_geometry: - result['original_geometry'] = format_geojson(smart_mapping(original_geometry), round=False) + result['original_geometry'] = format_geojson(smart_mapping(original_geometry), rounded=False) return result @classmethod @@ -80,7 +80,7 @@ class GeometryMixin(SerializableMixin): def _serialize(self, geometry=True, simple_geometry=False, **kwargs): result = super()._serialize(simple_geometry=simple_geometry, **kwargs) if geometry: - result['geometry'] = format_geojson(smart_mapping(self.geometry), round=False) + result['geometry'] = format_geojson(smart_mapping(self.geometry), rounded=False) if simple_geometry: result['point'] = (self.level_id, ) + tuple(round(i, 2) for i in self.point.coords[0]) if not isinstance(self.geometry, Point): @@ -96,8 +96,8 @@ class GeometryMixin(SerializableMixin): def get_geometry(self, detailed_geometry=True): if detailed_geometry: - return format_geojson(smart_mapping(self.geometry), round=False) - return format_geojson(smart_mapping(box(*self.geometry.bounds)), round=False) + return format_geojson(smart_mapping(self.geometry), rounded=False) + return format_geojson(smart_mapping(box(*self.geometry.bounds)), rounded=False) def get_shadow_geojson(self): pass diff --git a/src/c3nav/mapdata/utils/json.py b/src/c3nav/mapdata/utils/json.py index 07e24267..b40d8bf7 100644 --- a/src/c3nav/mapdata/utils/json.py +++ b/src/c3nav/mapdata/utils/json.py @@ -35,21 +35,64 @@ def json_encoder_reindent(method, data, *args, **kwargs): return result.replace(b'"'+magic_marker, b'').replace(magic_marker+b'"', b'') -def format_geojson(data, round=True): +def format_geojson(data, rounded=True): coordinates = data.get('coordinates', None) if coordinates is not None: + if data['type'] == 'Point': + coordinates = tuple(round(i, 2) for i in coordinates) + elif data['type'] == 'LineString': + coordinates = round_coordinates(coordinates) + elif data['type'] == 'MultiLineString': + coordinates = tuple(round_coordinates(linestring) for linestring in coordinates) + elif data['type'] == 'Polygon': + coordinates = round_polygon(coordinates) + if not coordinates: + data['type'] = 'MultiPolygon' + elif data['type'] == 'MultiPolygon': + coordinates = round_multipolygon(coordinates) + else: + raise ValueError('Unknown geojson type: %s' % data['type']) return { 'type': data['type'], - 'coordinates': round_coordinates(data['coordinates']) if round else data['coordinates'], + 'coordinates': coordinates, } return { 'type': data['type'], - 'geometries': [format_geojson(geometry, round=round) for geometry in data['geometries']], + 'geometries': [format_geojson(geometry, rounded=rounded) for geometry in data['geometries']], } -def round_coordinates(data): - if isinstance(data, (list, tuple)): - return tuple(round_coordinates(item) for item in data) - else: - return round(data, 2) +def round_multipolygon(coordinates): + # round every polygon on its own, then remove empty polygons + coordinates = tuple(round_polygon(polygon) for polygon in coordinates) + return tuple(polygon for polygon in coordinates if polygon) + + +def check_ring(coordinates): + # check if this is a valid ring + # that measn it has at least 3 points (or 4 if the first and last one are identical) + return len(coordinates) >= (4 if coordinates[0] == coordinates[-1] else 3) + + +def round_polygon(coordinates): + # round each ring on it's own and remove rings that are invalid + # if the exterior ring is invalid, return and empty polygon + coordinates = tuple(round_coordinates(ring) for ring in coordinates) + exterior, *interiors = coordinates + if not check_ring(exterior): + return () + return (exterior, *(interior for interior in interiors if check_ring(interior))) + + +def round_coordinates(coordinates): + # round coordinates, as in a list of x,y tuples + # filter out consecutive identical points + result = [] + last_point = None + for x, y in coordinates: + point = (round(x, 2), round(y, 2)) + if point == last_point: + continue + result.append(point) + last_point = point + return result