diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index 0b787f9d53..32176673c1 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -5,7 +5,9 @@ from ctypes import byref, c_int, c_uint from django.contrib.gis.geos import prototypes as capi -from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.geometry import ( + GEOSGeometry, ProjectInterpolateMixin, +) from django.contrib.gis.geos.libgeos import get_pointer_arr from django.contrib.gis.geos.linestring import LinearRing, LineString from django.contrib.gis.geos.point import Point @@ -99,7 +101,7 @@ class MultiPoint(GeometryCollection): _typeid = 4 -class MultiLineString(GeometryCollection): +class MultiLineString(ProjectInterpolateMixin, GeometryCollection): _allowed = (LineString, LinearRing) _typeid = 5 diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 42cecc59c8..4cbd561e3c 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -14,6 +14,7 @@ from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.error import GEOSException, GEOSIndexError from django.contrib.gis.geos.libgeos import GEOM_PTR from django.contrib.gis.geos.mutable_list import ListMixin +from django.contrib.gis.geos.prepared import PreparedGeometry from django.contrib.gis.geos.prototypes.io import ( ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w, ) @@ -27,8 +28,10 @@ class GEOSGeometry(GEOSBase, ListMixin): # Raise GEOSIndexError instead of plain IndexError # (see ticket #4740 and GEOSIndexError docstring) _IndexError = GEOSIndexError + _GEOS_CLASSES = None ptr_type = GEOM_PTR + has_cs = False # Only Point, LineString, LinearRing have coordinate sequences def __init__(self, geo_input, srid=None): """ @@ -92,7 +95,24 @@ class GEOSGeometry(GEOSBase, ListMixin): self.srid = srid # Setting the class type (e.g., Point, Polygon, etc.) - self.__class__ = GEOS_CLASSES[self.geom_typeid] + if GEOSGeometry._GEOS_CLASSES is None: + # Lazy-loaded variable to avoid import conflicts with GEOSGeometry. + from .linestring import LineString, LinearRing + from .point import Point + from .polygon import Polygon + from .collections import ( + GeometryCollection, MultiPoint, MultiLineString, MultiPolygon) + GEOSGeometry._GEOS_CLASSES = { + 0: Point, + 1: LineString, + 2: LinearRing, + 3: Polygon, + 4: MultiPoint, + 5: MultiLineString, + 6: MultiPolygon, + 7: GeometryCollection, + } + self.__class__ = GEOSGeometry._GEOS_CLASSES[self.geom_typeid] # Setting the coordinate sequence for the geometry (will be None on # geometries that do not have coordinate sequences) @@ -185,15 +205,6 @@ class GEOSGeometry(GEOSBase, ListMixin): return self.sym_difference(other) # #### Coordinate Sequence Routines #### - @property - def has_cs(self): - "Returns True if this Geometry has a coordinate sequence, False if not." - # Only these geometries are allowed to have coordinate sequences. - if isinstance(self, (Point, LineString, LinearRing)): - return True - else: - return False - def _set_cs(self): "Sets the coordinate sequence for this Geometry." if self.has_cs: @@ -560,16 +571,6 @@ class GEOSGeometry(GEOSBase, ListMixin): "Return the envelope for this geometry (a polygon)." return self._topology(capi.geos_envelope(self.ptr)) - def interpolate(self, distance): - if not isinstance(self, (LineString, MultiLineString)): - raise TypeError('interpolate only works on LineString and MultiLineString geometries') - return self._topology(capi.geos_interpolate(self.ptr, distance)) - - def interpolate_normalized(self, distance): - if not isinstance(self, (LineString, MultiLineString)): - raise TypeError('interpolate only works on LineString and MultiLineString geometries') - return self._topology(capi.geos_interpolate_normalized(self.ptr, distance)) - def intersection(self, other): "Returns a Geometry representing the points shared by this Geometry and other." return self._topology(capi.geos_intersection(self.ptr, other.ptr)) @@ -579,20 +580,6 @@ class GEOSGeometry(GEOSBase, ListMixin): "Computes an interior point of this Geometry." return self._topology(capi.geos_pointonsurface(self.ptr)) - def project(self, point): - if not isinstance(point, Point): - raise TypeError('locate_point argument must be a Point') - if not isinstance(self, (LineString, MultiLineString)): - raise TypeError('locate_point only works on LineString and MultiLineString geometries') - return capi.geos_project(self.ptr, point.ptr) - - def project_normalized(self, point): - if not isinstance(point, Point): - raise TypeError('locate_point argument must be a Point') - if not isinstance(self, (LineString, MultiLineString)): - raise TypeError('locate_point only works on LineString and MultiLineString geometries') - return capi.geos_project_normalized(self.ptr, point.ptr) - def relate(self, other): "Returns the DE-9IM intersection matrix for this Geometry and the other." return capi.geos_relate(self.ptr, other.ptr).decode() @@ -647,6 +634,7 @@ class GEOSGeometry(GEOSBase, ListMixin): Returns the extent of this geometry as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). """ + from .point import Point env = self.envelope if isinstance(env, Point): xmin, ymin = env.tuple @@ -668,21 +656,25 @@ class GEOSGeometry(GEOSBase, ListMixin): "Clones this Geometry." return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid) -# Class mapping dictionary. Has to be at the end to avoid import -# conflicts with GEOSGeometry. -from django.contrib.gis.geos.linestring import LineString, LinearRing # isort:skip -from django.contrib.gis.geos.point import Point # isort:skip -from django.contrib.gis.geos.polygon import Polygon # isort:skip -from django.contrib.gis.geos.collections import ( # isort:skip - GeometryCollection, MultiPoint, MultiLineString, MultiPolygon) -from django.contrib.gis.geos.prepared import PreparedGeometry # isort:skip -GEOS_CLASSES = { - 0: Point, - 1: LineString, - 2: LinearRing, - 3: Polygon, - 4: MultiPoint, - 5: MultiLineString, - 6: MultiPolygon, - 7: GeometryCollection, -} + +class ProjectInterpolateMixin(object): + """ + Used for LineString and MultiLineString. + """ + def interpolate(self, distance): + return self._topology(capi.geos_interpolate(self.ptr, distance)) + + def interpolate_normalized(self, distance): + return self._topology(capi.geos_interpolate_normalized(self.ptr, distance)) + + def project(self, point): + from .point import Point + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + return capi.geos_project(self.ptr, point.ptr) + + def project_normalized(self, point): + from .point import Point + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + return capi.geos_project_normalized(self.ptr, point.ptr) diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 1a74819da8..500825a700 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -135,6 +135,9 @@ lgeos = SimpleLazyObject(load_geos) class GEOSFuncFactory(object): + """ + Lazy loading of GEOS functions. + """ argtypes = None restype = None errcheck = None diff --git a/django/contrib/gis/geos/linestring.py b/django/contrib/gis/geos/linestring.py index 82ae9a2a4e..8e191c4b04 100644 --- a/django/contrib/gis/geos/linestring.py +++ b/django/contrib/gis/geos/linestring.py @@ -1,15 +1,18 @@ from django.contrib.gis.geos import prototypes as capi from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.error import GEOSException -from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.geometry import ( + GEOSGeometry, ProjectInterpolateMixin, +) from django.contrib.gis.geos.point import Point from django.contrib.gis.shortcuts import numpy from django.utils.six.moves import range -class LineString(GEOSGeometry): +class LineString(ProjectInterpolateMixin, GEOSGeometry): _init_func = capi.create_linestring _minlength = 2 + has_cs = True def __init__(self, *args, **kwargs): """ diff --git a/django/contrib/gis/geos/point.py b/django/contrib/gis/geos/point.py index e53a4569bf..65cf1344c9 100644 --- a/django/contrib/gis/geos/point.py +++ b/django/contrib/gis/geos/point.py @@ -10,6 +10,7 @@ from django.utils.six.moves import range class Point(GEOSGeometry): _minlength = 2 _maxlength = 3 + has_cs = True def __init__(self, x, y=None, z=None, srid=None): """ diff --git a/django/contrib/gis/geos/prepared.py b/django/contrib/gis/geos/prepared.py index 98ab1554ea..994082b296 100644 --- a/django/contrib/gis/geos/prepared.py +++ b/django/contrib/gis/geos/prepared.py @@ -1,6 +1,5 @@ from .base import GEOSBase from .error import GEOSException -from .geometry import GEOSGeometry from .libgeos import geos_version_info from .prototypes import prepared as capi @@ -18,6 +17,7 @@ class PreparedGeometry(GEOSBase): # from being garbage collected which could then crash the prepared one # See #21662 self._base_geom = geom + from .geometry import GEOSGeometry if not isinstance(geom, GEOSGeometry): raise TypeError self.ptr = capi.geos_prepare(geom.ptr)