team-3/src/c3nav/mapdata/models/geometry/base.py
2017-11-16 19:55:23 +01:00

133 lines
5.3 KiB
Python

import math
from collections import OrderedDict
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from shapely.geometry import LineString, Point, mapping
from shapely.ops import unary_union
from c3nav.mapdata.models.base import SerializableMixin
from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon
from c3nav.mapdata.utils.json import format_geojson
class GeometryManager(models.Manager):
def within(self, minx, miny, maxx, maxy):
return self.get_queryset().filter(minx__lte=maxx, maxx__gte=minx, miny__lte=maxy, maxy__gte=miny)
class GeometryMixin(SerializableMixin):
"""
A map feature with a geometry
"""
geometry = None
minx = models.DecimalField(_('min x coordinate'), max_digits=6, decimal_places=2, db_index=True)
miny = models.DecimalField(_('min y coordinate'), max_digits=6, decimal_places=2, db_index=True)
maxx = models.DecimalField(_('max x coordinate'), max_digits=6, decimal_places=2, db_index=True)
maxy = models.DecimalField(_('max y coordinate'), max_digits=6, decimal_places=2, db_index=True)
objects = GeometryManager()
class Meta:
abstract = True
base_manager_name = 'objects'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.orig_geometry = None if 'geometry' in self.get_deferred_fields() else self.geometry
def get_geojson_properties(self, *args, **kwargs) -> dict:
result = OrderedDict((
('type', self.__class__.__name__.lower()),
('id', self.pk),
))
if getattr(self, 'bounds', False):
result['bounds'] = True
return result
def to_geojson(self, instance=None) -> dict:
result = OrderedDict((
('type', 'Feature'),
('properties', self.get_geojson_properties(instance=instance)),
('geometry', format_geojson(mapping(self.geometry), round=False)),
))
original_geometry = getattr(self, 'original_geometry', None)
if original_geometry:
result['original_geometry'] = format_geojson(mapping(original_geometry), round=False)
return result
@classmethod
def serialize_type(cls, geomtype=True, **kwargs):
result = super().serialize_type()
if geomtype:
result['geomtype'] = cls._meta.get_field('geometry').geomtype
return result
@cached_property
def point(self):
c = self.geometry.centroid
x1, y1, x2, y2 = self.geometry.bounds
lines = (tuple(assert_multilinestring(LineString(((x1, c.y), (x2, c.y))).intersection(self.geometry))) +
tuple(assert_multilinestring(LineString(((c.x, y1), (c.x, y2))).intersection(self.geometry))))
return min(lines, key=lambda line: (line.distance(c), line.length),
default=self.geometry.representative_point).centroid
def serialize(self, **kwargs):
result = super().serialize(**kwargs)
if 'geometry' in result:
result.move_to_end('geometry')
return result
def _serialize(self, geometry=True, simple_geometry=False, **kwargs):
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
if geometry:
result['geometry'] = format_geojson(mapping(self.geometry), round=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):
result['bounds'] = ((int(math.floor(self.minx)), int(math.floor(self.miny))),
(int(math.ceil(self.maxx)), int(math.ceil(self.maxy))))
return result
def details_display(self):
result = super().details_display()
result['geometry'] = format_geojson(mapping(self.geometry), round=False)
return result
def get_shadow_geojson(self):
pass
def contains(self, x, y) -> bool:
return self.geometry.contains(Point(x, y))
def recalculate_bounds(self):
self.minx, self.miny, self.maxx, self.maxy = self.geometry.bounds
@property
def geometry_changed(self):
if self.orig_geometry is None:
return True
if self.geometry is self.orig_geometry:
return False
if not self.geometry.almost_equals(self.orig_geometry, 1):
return True
field = self._meta.get_field('geometry')
rounded = field.to_python(field.get_prep_value(self.geometry))
if not rounded.almost_equals(self.orig_geometry, 2):
return True
return False
def get_changed_geometry(self):
field = self._meta.get_field('geometry')
new_geometry = self.geometry
new_geometry = field.to_python(field.get_prep_value(new_geometry))
if self.orig_geometry is None:
return new_geometry
difference = new_geometry.symmetric_difference(self.orig_geometry)
if self._meta.get_field('geometry').geomtype in ('polygon', 'multipolygon'):
difference = unary_union(assert_multipolygon(difference))
return difference
def save(self, *args, **kwargs):
self.recalculate_bounds()
super().save(*args, **kwargs)