team-3/src/c3nav/mapdata/models/geometry/base.py

143 lines
5.5 KiB
Python
Raw Normal View History

2017-10-28 21:36:52 +02:00
import math
2018-12-24 03:58:16 +01:00
from contextlib import contextmanager
2017-05-08 21:55:45 +02:00
2018-12-20 16:12:00 +01:00
from django.db import models
2017-10-28 21:36:52 +02:00
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from shapely.geometry import Point, box
from shapely.geometry.base import BaseGeometry
2017-11-16 19:24:49 +01:00
from shapely.ops import unary_union
from c3nav.mapdata.models.base import SerializableMixin
from c3nav.mapdata.utils.geometry import assert_multipolygon, good_representative_point, smart_mapping, unwrap_geom
from c3nav.mapdata.utils.json import format_geojson
geometry_affecting_fields = ('height', 'width', 'access_restriction')
class GeometryMixin(SerializableMixin):
2018-12-24 03:58:16 +01:00
no_orig = False
"""
A map feature with a geometry
"""
geometry: BaseGeometry
level_id: int
subtitle: str
2018-12-20 16:12:00 +01:00
import_tag = models.CharField(_('import tag'), null=True, blank=True, max_length=32)
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
2018-12-24 03:58:16 +01:00
if not self.no_orig:
self.orig_geometry = None if 'geometry' in self.get_deferred_fields() else self.geometry
self._orig = {field.attname: (None if field.attname in self.get_deferred_fields()
else getattr(self, field.attname))
for field in self._meta.get_fields()
if field.name in geometry_affecting_fields}
@classmethod
@contextmanager
def dont_keep_originals(cls):
2019-12-23 15:02:52 +01:00
# todo: invert this and to no_orig being True by default
2018-12-24 03:58:16 +01:00
cls.no_orig = True
yield
cls.no_orig = False
def get_geojson_properties(self, *args, **kwargs) -> dict:
result = {
'type': self.__class__.__name__.lower(),
'id': self.id
}
2017-05-27 16:19:49 +02:00
if getattr(self, 'bounds', False):
result['bounds'] = True
return result
def get_geojson_key(self):
return (self.__class__.__name__.lower(), self.id)
def to_geojson(self, instance=None) -> dict:
result = {
'type': 'Feature',
'properties': self.get_geojson_properties(instance=instance),
'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), rounded=False)
return result
2017-05-11 21:30:29 +02:00
@classmethod
def serialize_type(cls, geomtype=True, **kwargs):
result = super().serialize_type()
if geomtype:
result['geomtype'] = cls._meta.get_field('geometry').geomtype
return result
2017-10-28 21:36:52 +02:00
@cached_property
def point(self):
if "geometry" in self.get_deferred_fields():
raise ValueError
2017-11-28 22:02:24 +01:00
return good_representative_point(self.geometry)
2017-10-28 21:36:52 +02:00
def _serialize(self, geometry=True, simple_geometry=False, **kwargs):
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
if geometry and "geometry" not in self.get_deferred_fields():
result['geometry'] = format_geojson(smart_mapping(self.geometry), rounded=False)
if simple_geometry and "geometry" not in self.get_deferred_fields():
result['point'] = (self.level_id, ) + tuple(round(i, 2) for i in self.point.coords[0])
2017-10-28 21:36:52 +02:00
if not isinstance(self.geometry, Point):
self.geometry: BaseGeometry
2017-11-22 22:15:33 +01:00
minx, miny, maxx, maxy = self.geometry.bounds
result['bounds'] = ((int(math.floor(minx)), int(math.floor(miny))),
(int(math.ceil(maxx)), int(math.ceil(maxy))))
2017-05-11 19:36:49 +02:00
return result
2017-11-02 13:35:58 +01:00
2018-09-19 19:08:47 +02:00
def details_display(self, detailed_geometry=True, **kwargs):
result = super().details_display(**kwargs)
2018-12-25 17:39:40 +01:00
result['geometry'] = self.get_geometry(detailed_geometry=detailed_geometry)
2017-11-02 13:35:58 +01:00
return result
2017-05-11 19:36:49 +02:00
2018-12-25 17:39:40 +01:00
def get_geometry(self, detailed_geometry=True):
if "geometry" in self.get_deferred_fields():
return None
2018-12-25 17:39:40 +01:00
if detailed_geometry:
return format_geojson(smart_mapping(self.geometry), rounded=False)
return format_geojson(smart_mapping(box(*self.geometry.bounds)), rounded=False)
2018-12-25 17:39:40 +01:00
def get_shadow_geojson(self):
2017-05-09 09:36:08 +02:00
pass
2017-05-09 13:16:36 +02:00
def contains(self, x, y) -> bool:
return self.geometry.contains(Point(x, y))
@property
def all_geometry_changed(self):
return any(getattr(self, attname) != value for attname, value in self._orig.items())
@property
def geometry_changed(self):
2017-10-24 00:11:02 +02:00
if self.orig_geometry is None:
return True
if self.geometry is self.orig_geometry:
return False
if not self.geometry.equals_exact(unwrap_geom(self.orig_geometry), 0.05):
2017-10-24 00:11:02 +02:00
return True
field = self._meta.get_field('geometry')
rounded = field.to_python(field.get_prep_value(self.geometry))
if not rounded.equals_exact(unwrap_geom(self.orig_geometry), 0.005):
2017-10-24 00:11:02 +02:00
return True
return False
def get_changed_geometry(self):
field = self._meta.get_field('geometry')
new_geometry = field.get_final_value(self.geometry)
2017-11-16 19:24:49 +01:00
if self.orig_geometry is None:
return new_geometry
difference = new_geometry.symmetric_difference(unwrap_geom(self.orig_geometry))
2017-11-16 19:33:19 +01:00
if self._meta.get_field('geometry').geomtype in ('polygon', 'multipolygon'):
2017-11-16 19:24:49 +01:00
difference = unary_union(assert_multipolygon(difference))
return difference