diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py index 8fa1c2cd44..ba3d3bdf67 100644 --- a/django/contrib/gis/db/backends/base/operations.py +++ b/django/contrib/gis/db/backends/base/operations.py @@ -27,10 +27,10 @@ class BaseSpatialOperations: unsupported_functions = { 'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope', - 'ForceRHR', 'GeoHash', 'Intersection', 'IsValid', 'Length', 'MakeValid', - 'MemSize', 'NumGeometries', 'NumPoints', 'Perimeter', 'PointOnSurface', - 'Reverse', 'Scale', 'SnapToGrid', 'SymDifference', 'Transform', - 'Translate', 'Union', + 'ForceRHR', 'GeoHash', 'Intersection', 'IsValid', 'Length', + 'LineLocatePoint', 'MakeValid', 'MemSize', 'NumGeometries', + 'NumPoints', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', + 'SnapToGrid', 'SymDifference', 'Transform', 'Translate', 'Union', } # Constructors diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 58aeda4deb..33f148f5f6 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -73,8 +73,9 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): def unsupported_functions(self): unsupported = { 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle', 'ForceRHR', - 'MakeValid', 'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse', - 'Scale', 'SnapToGrid', 'Transform', 'Translate', + 'LineLocatePoint', 'MakeValid', 'MemSize', 'Perimeter', + 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid', 'Transform', + 'Translate', } if self.connection.mysql_version < (5, 7, 5): unsupported.update({'AsGeoJSON', 'GeoHash', 'IsValid'}) diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 62506e4227..649449b160 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -112,7 +112,8 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): unsupported_functions = { 'AsGeoJSON', 'AsKML', 'AsSVG', 'Envelope', 'ForceRHR', 'GeoHash', - 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', 'Translate', + 'LineLocatePoint', 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', + 'Translate', } def geo_quote_name(self, name): diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 46cac8c0b3..23b38694f8 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -81,6 +81,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): def function_names(self): return { 'Length': 'ST_Length', + 'LineLocatePoint': 'ST_Line_Locate_Point', 'NumPoints': 'ST_NPoints', 'Reverse': 'ST_Reverse', 'Scale': 'ScaleCoords', diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index c595a143d0..ee2df7cfb4 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -389,6 +389,12 @@ class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc): return super().as_sql(compiler, connection, function=function) +class LineLocatePoint(GeoFunc): + output_field_class = FloatField + arity = 2 + geom_param_pos = (0, 1) + + class MakeValid(GeoFunc): pass diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index d0555ec145..c3d7003636 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -386,6 +386,7 @@ Function PostGIS Oracle MySQL Spat :class:`Intersection` X X X (≥ 5.6.1) X :class:`IsValid` X X X (≥ 5.7.5) X (LWGEOM) :class:`Length` X X X X +:class:`LineLocatePoint` X X :class:`MakeValid` X X (LWGEOM) :class:`MemSize` X :class:`NumGeometries` X X X X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index b54feb03a1..4bf1994ea6 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -358,6 +358,19 @@ resource-intensive) with the ``spheroid`` keyword argument. In older versions, a raw value was returned on MySQL when used on projected SRS. +``LineLocatePoint`` +=================== + +.. class:: LineLocatePoint(linestring, point, **extra) + +.. versionadded:: 2.0 + +*Availability*: `PostGIS `__, +SpatiaLite + +Returns a float between 0 and 1 representing the location of the closest point on +``linestring`` to the given ``point``, as a fraction of the 2D line length. + ``MakeValid`` ============= diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 0006ef4c45..33f969d443 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -68,6 +68,9 @@ Minor features :class:`~django.contrib.gis.db.models.functions.IsValid` function, and :lookup:`isvalid` lookup. +* Added the :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` + function, supported on PostGIS and SpatiaLite. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 818ec56e93..ff6d1db9a8 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -286,6 +286,11 @@ class GISFunctionsTests(TestCase): with self.assertRaisesMessage(ValueError, 'AreaField only accepts Area measurement objects.'): qs.get(area__lt=500000) + @skipUnlessDBFeature("has_LineLocatePoint_function") + def test_line_locate_point(self): + pos_expr = functions.LineLocatePoint(LineString((0, 0), (0, 3), srid=4326), Point(0, 1, srid=4326)) + self.assertAlmostEqual(State.objects.annotate(pos=pos_expr).first().pos, 0.3333333) + @skipUnlessDBFeature("has_MakeValid_function") def test_make_valid(self): invalid_geom = fromstr('POLYGON((0 0, 0 1, 1 1, 1 0, 1 1, 1 0, 0 0))')