Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11948 -- Added interpolate and project linear referencing methods

Thanks novalis for the report and the initial patch, and Anssi
Kääriäinen and Justin Bronn for the review.
  • Loading branch information...
commit 2f6e00a840176f95c836f25a41cc1a7d31941ba5 1 parent 15d355d
Claude Paroz authored
32  django/contrib/gis/geos/geometry.py
@@ -581,6 +581,20 @@ def envelope(self):
581 581
         "Return the envelope for this geometry (a polygon)."
582 582
         return self._topology(capi.geos_envelope(self.ptr))
583 583
 
  584
+    def interpolate(self, distance):
  585
+        if not isinstance(self, (LineString, MultiLineString)):
  586
+            raise TypeError('interpolate only works on LineString and MultiLineString geometries')
  587
+        if not hasattr(capi, 'geos_interpolate'):
  588
+            raise NotImplementedError('interpolate requires GEOS 3.2+')
  589
+        return self._topology(capi.geos_interpolate(self.ptr, distance))
  590
+
  591
+    def interpolate_normalized(self, distance):
  592
+        if not isinstance(self, (LineString, MultiLineString)):
  593
+            raise TypeError('interpolate only works on LineString and MultiLineString geometries')
  594
+        if not hasattr(capi, 'geos_interpolate_normalized'):
  595
+            raise NotImplementedError('interpolate_normalized requires GEOS 3.2+')
  596
+        return self._topology(capi.geos_interpolate_normalized(self.ptr, distance))
  597
+
584 598
     def intersection(self, other):
585 599
         "Returns a Geometry representing the points shared by this Geometry and other."
586 600
         return self._topology(capi.geos_intersection(self.ptr, other.ptr))
@@ -590,6 +604,24 @@ def point_on_surface(self):
590 604
         "Computes an interior point of this Geometry."
591 605
         return self._topology(capi.geos_pointonsurface(self.ptr))
592 606
 
  607
+    def project(self, point):
  608
+        if not isinstance(point, Point):
  609
+            raise TypeError('locate_point argument must be a Point')
  610
+        if not isinstance(self, (LineString, MultiLineString)):
  611
+            raise TypeError('locate_point only works on LineString and MultiLineString geometries')
  612
+        if not hasattr(capi, 'geos_project'):
  613
+            raise NotImplementedError('geos_project requires GEOS 3.2+')
  614
+        return capi.geos_project(self.ptr, point.ptr)
  615
+
  616
+    def project_normalized(self, point):
  617
+        if not isinstance(point, Point):
  618
+            raise TypeError('locate_point argument must be a Point')
  619
+        if not isinstance(self, (LineString, MultiLineString)):
  620
+            raise TypeError('locate_point only works on LineString and MultiLineString geometries')
  621
+        if not hasattr(capi, 'geos_project_normalized'):
  622
+            raise NotImplementedError('project_normalized requires GEOS 3.2+')
  623
+        return capi.geos_project_normalized(self.ptr, point.ptr)
  624
+
593 625
     def relate(self, other):
594 626
         "Returns the DE-9IM intersection matrix for this Geometry and the other."
595 627
         return capi.geos_relate(self.ptr, other.ptr).decode()
23  django/contrib/gis/geos/prototypes/topology.py
@@ -8,18 +8,18 @@
8 8
            'geos_simplify', 'geos_symdifference', 'geos_union', 'geos_relate']
9 9
 
10 10
 from ctypes import c_double, c_int
11  
-from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE
12  
-from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string
  11
+from django.contrib.gis.geos.libgeos import geos_version_info, GEOM_PTR, GEOS_PREPARE
  12
+from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_minus_one, check_string
13 13
 from django.contrib.gis.geos.prototypes.geom import geos_char_p
14 14
 from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc
15 15
 
16  
-def topology(func, *args):
  16
+def topology(func, *args, **kwargs):
17 17
     "For GEOS unary topology functions."
18 18
     argtypes = [GEOM_PTR]
19 19
     if args: argtypes += args
20 20
     func.argtypes = argtypes
21  
-    func.restype = GEOM_PTR
22  
-    func.errcheck = check_geom
  21
+    func.restype = kwargs.get('restype', GEOM_PTR)
  22
+    func.errcheck = kwargs.get('errcheck', check_geom)
23 23
     return func
24 24
 
25 25
 ### Topology Routines ###
@@ -49,3 +49,16 @@ def topology(func, *args):
49 49
     geos_cascaded_union.argtypes = [GEOM_PTR]
50 50
     geos_cascaded_union.restype = GEOM_PTR
51 51
     __all__.append('geos_cascaded_union')
  52
+
  53
+# Linear referencing routines
  54
+info = geos_version_info()
  55
+if info['version'] >= '3.2.0':
  56
+    geos_project = topology(GEOSFunc('GEOSProject'), GEOM_PTR,
  57
+        restype=c_double, errcheck=check_minus_one)
  58
+    geos_interpolate = topology(GEOSFunc('GEOSInterpolate'), c_double)
  59
+
  60
+    geos_project_normalized = topology(GEOSFunc('GEOSProjectNormalized'),
  61
+        GEOM_PTR, restype=c_double, errcheck=check_minus_one)
  62
+    geos_interpolate_normalized = topology(GEOSFunc('GEOSInterpolateNormalized'), c_double)
  63
+    __all__.extend(['geos_project', 'geos_interpolate',
  64
+        'geos_project_normalized', 'geos_interpolate_normalized'])
21  django/contrib/gis/geos/tests/test_geos.py
@@ -1023,6 +1023,27 @@ def test_valid_reason(self):
1023 1023
 
1024 1024
         print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n")
1025 1025
 
  1026
+    @unittest.skipUnless(geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required")
  1027
+    def test_linearref(self):
  1028
+        "Testing linear referencing"
  1029
+
  1030
+        ls = fromstr('LINESTRING(0 0, 0 10, 10 10, 10 0)')
  1031
+        mls = fromstr('MULTILINESTRING((0 0, 0 10), (10 0, 10 10))')
  1032
+
  1033
+        self.assertEqual(ls.project(Point(0, 20)), 10.0)
  1034
+        self.assertEqual(ls.project(Point(7, 6)), 24)
  1035
+        self.assertEqual(ls.project_normalized(Point(0, 20)), 1.0/3)
  1036
+
  1037
+        self.assertEqual(ls.interpolate(10), Point(0, 10))
  1038
+        self.assertEqual(ls.interpolate(24), Point(10, 6))
  1039
+        self.assertEqual(ls.interpolate_normalized(1.0/3), Point(0, 10))
  1040
+
  1041
+        self.assertEqual(mls.project(Point(0, 20)), 10)
  1042
+        self.assertEqual(mls.project(Point(7, 6)), 16)
  1043
+
  1044
+        self.assertEqual(mls.interpolate(9), Point(0, 9))
  1045
+        self.assertEqual(mls.interpolate(17), Point(10, 7))
  1046
+
1026 1047
     def test_geos_version(self):
1027 1048
         "Testing the GEOS version regular expression."
1028 1049
         from django.contrib.gis.geos.libgeos import version_regex
25  docs/ref/contrib/gis/geos.txt
@@ -416,11 +416,36 @@ quarter circle (defaults is 8).
416 416
 Returns a :class:`GEOSGeometry` representing the points making up this
417 417
 geometry that do not make up other.
418 418
 
  419
+.. method:: GEOSGeometry.interpolate(distance)
  420
+.. method:: GEOSGeometry.interpolate_normalized(distance)
  421
+
  422
+.. versionadded:: 1.5
  423
+
  424
+Given a distance (float), returns the point (or closest point) within the
  425
+geometry (:class:`LineString` or :class:`MultiLineString`) at that distance.
  426
+The normalized version takes the distance as a float between 0 (origin) and 1
  427
+(endpoint).
  428
+
  429
+Reverse of :meth:`GEOSGeometry.project`.
  430
+
419 431
 .. method:: GEOSGeometry:intersection(other)
420 432
 
421 433
 Returns a :class:`GEOSGeometry` representing the points shared by this
422 434
 geometry and other.
423 435
 
  436
+.. method:: GEOSGeometry.project(point)
  437
+.. method:: GEOSGeometry.project_normalized(point)
  438
+
  439
+.. versionadded:: 1.5
  440
+
  441
+Returns the distance (float) from the origin of the geometry
  442
+(:class:`LineString` or :class:`MultiLineString`) to the point projected on the
  443
+geometry (that is to a point of the line the closest to the given point).
  444
+The normalized version returns the distance as a float between 0 (origin) and 1
  445
+(endpoint).
  446
+
  447
+Reverse of :meth:`GEOSGeometry.interpolate`.
  448
+
424 449
 .. method:: GEOSGeometry.relate(other)
425 450
 
426 451
 Returns the DE-9IM intersection matrix (a string) representing the
14  docs/releases/1.5.txt
@@ -103,10 +103,22 @@ associated with proxy models.
103 103
 
104 104
 New ``view`` variable in class-based views context
105 105
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  106
+
106 107
 In all :doc:`generic class-based views </topics/class-based-views/index>`
107 108
 (or any class-based view inheriting from ``ContextMixin``), the context dictionary
108 109
 contains a ``view`` variable that points to the ``View`` instance.
109 110
 
  111
+GeoDjango
  112
+~~~~~~~~~
  113
+
  114
+* :class:`~django.contrib.gis.geos.LineString` and
  115
+  :class:`~django.contrib.gis.geos.MultiLineString` GEOS objects now support the
  116
+  :meth:`~django.contrib.gis.geos.GEOSGeometry.interpolate()` and
  117
+  :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods
  118
+  (so-called linear referencing).
  119
+
  120
+* Support for GDAL < 1.5 has been dropped.
  121
+
110 122
 Minor features
111 123
 ~~~~~~~~~~~~~~
112 124
 
@@ -379,8 +391,6 @@ on the form.
379 391
 Miscellaneous
380 392
 ~~~~~~~~~~~~~
381 393
 
382  
-* GeoDjango dropped support for GDAL < 1.5
383  
-
384 394
 * :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError`
385 395
   instead of :exc:`ValueError` for non-integer inputs.
386 396
 

0 notes on commit 2f6e00a

Please sign in to comment.
Something went wrong with that request. Please try again.