class Topology(AltimetryMixin, TimeStampedModel, NoDeleteMixin): paths = models.ManyToManyField(Path, editable=False, db_column='troncons', through='PathAggregation', verbose_name=_(u"Path")) offset = models.FloatField(default=0.0, db_column='decallage', verbose_name=_(u"Offset")) # in SRID units kind = models.CharField(editable=False, verbose_name=_(u"Kind"), max_length=32) # Override default manager objects = NoDeleteMixin.get_manager_cls(models.GeoManager)() geom = models.GeometryField(editable=False, srid=settings.SRID, null=True, blank=True, spatial_index=False, dim=3) class Meta: db_table = 'e_t_evenement' verbose_name = _(u"Topology") verbose_name_plural = _(u"Topologies") def __init__(self, *args, **kwargs): super(Topology, self).__init__(*args, **kwargs) if not self.pk: self.kind = self.__class__.KIND @classmethod def add_property(cls, name, func): if hasattr(cls, name): raise AttributeError("%s has already an attribute %s" % (cls, name)) setattr(cls, name, property(func)) @classproperty def KIND(cls): return cls._meta.object_name.upper() def __unicode__(self): return u"%s (%s)" % (_(u"Topology"), self.pk) def ispoint(self): if not self.pk and self.geom and self.geom.geom_type == 'Point': return True return all([ a.start_position == a.end_position for a in self.aggregations.all() ]) def geom_as_point(self): geom = self.geom assert geom, 'Topology point is None' if geom.geom_type != 'Point': logger.warning( "Topology has wrong geometry type : %s instead of Point" % geom.geom_type) geom = Point(geom.coords[0], srid=settings.SRID) return geom def add_path(self, path, start=0.0, end=1.0, order=0, reload=True): """ Shortcut function to add paths into this topology. """ from .factories import PathAggregationFactory aggr = PathAggregationFactory.create(topo_object=self, path=path, start_position=start, end_position=end, order=order) # Since a trigger modifies geom, we reload the object if reload: self.reload() return aggr @classmethod def overlapping(cls, topologies): """ Return a Topology queryset overlapping specified topologies. """ return TopologyHelper.overlapping(cls, topologies) def mutate(self, other, delete=True): """ Take alls attributes of the other topology specified and save them into this one. Optionnally deletes the other. """ self.offset = other.offset self.geom = other.geom self.save() PathAggregation.objects.filter(topo_object=self).delete() aggrs = other.aggregations.all() # A point has only one aggregation, except if it is on an intersection. # In this case, the trigger will create them, so ignore them here. if other.ispoint(): aggrs = aggrs[:1] for aggr in aggrs: self.add_path(aggr.path, aggr.start_position, aggr.end_position, aggr.order, reload=False) if delete: other.delete(force=True) # Really delete it from database self.save() return self def reload(self, fromdb=None): """ Reload into instance all computed attributes in triggers. """ if self.pk: # Update computed values fromdb = self.__class__.objects.get(pk=self.pk) self.geom = fromdb.geom self.offset = fromdb.offset # /!\ offset may be set by a trigger OR in # the django code, reload() will override # any unsaved value AltimetryMixin.reload(self, fromdb) TimeStampedModel.reload(self, fromdb) NoDeleteMixin.reload(self, fromdb) return self @debug_pg_notices def save(self, *args, **kwargs): # HACK: these fields are readonly from the Django point of view # but they can be changed at DB level. Since Django write all fields # to DB anyway, it is important to update it before writting if self.pk: tmp = self.__class__.objects.get(pk=self.pk) self.length = tmp.length # In the case of points, the geom can be set by Django. Don't override. if (self.ispoint() and self.geom is None) or \ (not self.ispoint() and tmp.geom is not None): self.geom = tmp.geom if not self.kind: if self.KIND == "TOPOLOGYMIXIN": raise Exception("Cannot save abstract topologies") self.kind = self.__class__.KIND # Static value for Topology offset, if any shortmodelname = self._meta.object_name.lower().replace('edge', '') self.offset = settings.TOPOLOGY_STATIC_OFFSETS.get( shortmodelname, self.offset) # Save into db super(Topology, self).save(*args, **kwargs) self.reload() def serialize(self): return TopologyHelper.serialize(self) @classmethod def deserialize(cls, serialized): return TopologyHelper.deserialize(serialized)
class Topology(AltimetryMixin, TimeStampedModel, NoDeleteMixin): paths = models.ManyToManyField(Path, editable=False, db_column='troncons', through='PathAggregation', verbose_name=_(u"Path")) offset = models.FloatField(default=0.0, db_column='decallage', verbose_name=_(u"Offset")) # in SRID units kind = models.CharField(editable=False, verbose_name=_(u"Kind"), max_length=32) # Override default manager objects = NoDeleteMixin.get_manager_cls(models.GeoManager)() geom = models.GeometryField( editable=(not settings.TREKKING_TOPOLOGY_ENABLED), srid=settings.SRID, null=True, default=None, spatial_index=False) """ Fake srid attribute, that prevents transform() calls when using Django map widgets. """ srid = settings.API_SRID class Meta: db_table = 'e_t_evenement' verbose_name = _(u"Topology") verbose_name_plural = _(u"Topologies") def __init__(self, *args, **kwargs): super(Topology, self).__init__(*args, **kwargs) if not self.pk: self.kind = self.__class__.KIND @classmethod def add_property(cls, name, func): if hasattr(cls, name): raise AttributeError("%s has already an attribute %s" % (cls, name)) setattr(cls, name, property(func)) @classproperty def KIND(cls): return cls._meta.object_name.upper() def __unicode__(self): return u"%s (%s)" % (_(u"Topology"), self.pk) def ispoint(self): if not self.pk and self.geom and self.geom.geom_type == 'Point': return True return all([ a.start_position == a.end_position for a in self.aggregations.all() ]) def add_path(self, path, start=0.0, end=1.0, order=0, reload=True): """ Shortcut function to add paths into this topology. """ from .factories import PathAggregationFactory aggr = PathAggregationFactory.create(topo_object=self, path=path, start_position=start, end_position=end, order=order) if self.deleted: self.deleted = False self.save(update_fields=['deleted']) # Since a trigger modifies geom, we reload the object if reload: self.reload() return aggr @classmethod def overlapping(cls, topologies): """ Return a Topology queryset overlapping specified topologies. """ return TopologyHelper.overlapping(cls, topologies) def mutate(self, other, delete=True): """ Take alls attributes of the other topology specified and save them into this one. Optionnally deletes the other. """ self.offset = other.offset self.save(update_fields=['offset']) PathAggregation.objects.filter(topo_object=self).delete() # The previous operation has put deleted = True (in triggers) # and NULL in geom (see update_geometry_of_evenement:: IF t_count = 0) self.deleted = False self.geom = other.geom self.save(update_fields=['deleted', 'geom']) # Now copy all agregations from other to self aggrs = other.aggregations.all() # A point has only one aggregation, except if it is on an intersection. # In this case, the trigger will create them, so ignore them here. if other.ispoint(): aggrs = aggrs[:1] for aggr in aggrs: self.add_path(aggr.path, aggr.start_position, aggr.end_position, aggr.order, reload=False) self.reload() if delete: other.delete(force=True) # Really delete it from database return self def reload(self, fromdb=None): """ Reload into instance all computed attributes in triggers. """ if self.pk: # Update computed values fromdb = self.__class__.objects.get(pk=self.pk) self.geom = fromdb.geom self.offset = fromdb.offset # /!\ offset may be set by a trigger OR in # the django code, reload() will override # any unsaved value AltimetryMixin.reload(self, fromdb) TimeStampedModel.reload(self, fromdb) NoDeleteMixin.reload(self, fromdb) return self @debug_pg_notices def save(self, *args, **kwargs): # HACK: these fields are readonly from the Django point of view # but they can be changed at DB level. Since Django write all fields # to DB anyway, it is important to update it before writting if self.pk and settings.TREKKING_TOPOLOGY_ENABLED: existing = self.__class__.objects.get(pk=self.pk) self.length = existing.length # In the case of points, the geom can be set by Django. Don't override. point_geom_not_set = self.ispoint() and self.geom is None geom_already_in_db = not self.ispoint( ) and existing.geom is not None if (point_geom_not_set or geom_already_in_db): self.geom = existing.geom else: if not self.deleted and self.geom is None: # We cannot have NULL geometry. So we use an empty one, # it will be computed or overwritten by triggers. self.geom = fromstr('POINT (0 0)') if not self.kind: if self.KIND == "TOPOLOGYMIXIN": raise Exception("Cannot save abstract topologies") self.kind = self.__class__.KIND # Static value for Topology offset, if any shortmodelname = self._meta.object_name.lower().replace('edge', '') self.offset = settings.TOPOLOGY_STATIC_OFFSETS.get( shortmodelname, self.offset) # Save into db super(Topology, self).save(*args, **kwargs) self.reload() def serialize(self, **kwargs): return TopologyHelper.serialize(self, **kwargs) @classmethod def deserialize(cls, serialized): return TopologyHelper.deserialize(serialized)