diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py index f6eaf8f5037..287841b29e2 100644 --- a/django/contrib/gis/db/backends/base/operations.py +++ b/django/contrib/gis/db/backends/base/operations.py @@ -45,6 +45,8 @@ class BaseSpatialOperations: "Difference", "Distance", "Envelope", + "FromWKB", + "FromWKT", "GeoHash", "GeometryDistance", "Intersection", diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 46d8fc5d6ab..0799a6b28f1 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -62,6 +62,11 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): models.Union, ) + function_names = { + "FromWKB": "ST_GeomFromWKB", + "FromWKT": "ST_GeomFromText", + } + @cached_property def unsupported_functions(self): unsupported = { diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index aefb2f74e2b..c191d0b1f78 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -76,6 +76,8 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): "Difference": "SDO_GEOM.SDO_DIFFERENCE", "Distance": "SDO_GEOM.SDO_DISTANCE", "Envelope": "SDO_GEOM_MBR", + "FromWKB": "SDO_UTIL.FROM_WKBGEOMETRY", + "FromWKT": "SDO_UTIL.FROM_WKTGEOMETRY", "Intersection": "SDO_GEOM.SDO_INTERSECTION", "IsValid": "SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT", "Length": "SDO_GEOM.SDO_LENGTH", diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 070f670a0b5..efa1d0f204e 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -172,6 +172,8 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): "AsWKB": "ST_AsBinary", "AsWKT": "ST_AsText", "BoundingCircle": "ST_MinimumBoundingCircle", + "FromWKB": "ST_GeomFromWKB", + "FromWKT": "ST_GeomFromText", "NumPoints": "ST_NPoints", } return function_names diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 47f3d4ca75f..0b8b26ab6f8 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -67,6 +67,8 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): function_names = { "AsWKB": "St_AsBinary", "ForcePolygonCW": "ST_ForceLHR", + "FromWKB": "ST_GeomFromWKB", + "FromWKT": "ST_GeomFromText", "Length": "ST_Length", "LineLocatePoint": "ST_Line_Locate_Point", "NumPoints": "ST_NPoints", diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index f97c540a1ab..d886f8916e9 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -69,6 +69,8 @@ class GeoFuncMixin: def resolve_expression(self, *args, **kwargs): res = super().resolve_expression(*args, **kwargs) + if not self.geom_param_pos: + return res # Ensure that expressions are geometric. source_fields = res.get_source_fields() @@ -351,6 +353,18 @@ class ForcePolygonCW(GeomOutputGeoFunc): arity = 1 +class FromWKB(GeoFunc): + output_field = GeometryField(srid=0) + arity = 1 + geom_param_pos = () + + +class FromWKT(GeoFunc): + output_field = GeometryField(srid=0) + arity = 1 + geom_param_pos = () + + class GeoHash(GeoFunc): output_field = TextField() diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 97f1f6ca6b6..d215eb1ecb1 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -360,6 +360,8 @@ Function PostGIS Oracle MariaDB MySQL :class:`Distance` X X X X X :class:`Envelope` X X X X X :class:`ForcePolygonCW` X X +:class:`FromWKB` X X X X X +:class:`FromWKT` X X X X X :class:`GeoHash` X X X (LWGEOM/RTTOPO) :class:`Intersection` X X X X X :class:`IsEmpty` X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 1c062515c60..a07feebfb7d 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -20,17 +20,17 @@ get a ``NotImplementedError`` exception. Function's summary: -========================= ======================== ====================== ======================= ================== ===================== -Measurement Relationships Operations Editors Output format Miscellaneous -========================= ======================== ====================== ======================= ================== ===================== -:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForcePolygonCW` :class:`AsGeoJSON` :class:`IsEmpty` -:class:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`IsValid` -:class:`GeometryDistance` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`MemSize` -:class:`Length` :class:`Envelope` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumGeometries` -:class:`Perimeter` :class:`LineLocatePoint` :class:`SnapToGrid` :class:`AsWKB` :class:`NumPoints` -.. :class:`PointOnSurface` :class:`Transform` :class:`AsWKT` -.. :class:`Translate` :class:`GeoHash` -========================= ======================== ====================== ======================= ================== ===================== +========================= ======================== ====================== ======================= ================== ================== ====================== +Measurement Relationships Operations Editors Input format Output format Miscellaneous +========================= ======================== ====================== ======================= ================== ================== ====================== +:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForcePolygonCW` :class:`AsGeoJSON` :class:`IsEmpty` +:class:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`IsValid` +:class:`GeometryDistance` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`MemSize` +:class:`Length` :class:`Envelope` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumGeometries` +:class:`Perimeter` :class:`LineLocatePoint` :class:`SnapToGrid` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints` + :class:`PointOnSurface` :class:`Transform` :class:`FromWKT` :class:`AsWKT` + :class:`Translate` :class:`GeoHash` +========================= ======================== ====================== ======================= ================== ================== ====================== ``Area`` ======== @@ -174,15 +174,13 @@ __ https://www.w3.org/Graphics/SVG/ Oracle, `PostGIS `__, SpatiaLite Accepts a single geographic field or expression and returns a `Well-known -binary (WKB)`__ representation of the geometry. +binary (WKB)`_ representation of the geometry. Example:: >>> bytes(City.objects.annotate(wkb=AsWKB('point')).get(name='Chelyabinsk').wkb) b'\x01\x01\x00\x00\x00]3\xf9f\x9b\x91K@\x00X\x1d9\xd2\xb9N@' -__ https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary - ``AsWKT`` ========= @@ -193,15 +191,13 @@ __ https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well Oracle, `PostGIS `__, SpatiaLite Accepts a single geographic field or expression and returns a `Well-known text -(WKT)`__ representation of the geometry. +(WKT)`_ representation of the geometry. Example:: >>> City.objects.annotate(wkt=AsWKT('point')).get(name='Chelyabinsk').wkt 'POINT (55.137555 61.451728)' -__ https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry - ``Azimuth`` =========== @@ -327,6 +323,32 @@ of the polygon/multipolygon in which all exterior rings are oriented clockwise and all interior rings are oriented counterclockwise. Non-polygonal geometries are returned unchanged. +``FromWKB`` +=========== + +.. versionadded:: 4.2 + +.. class:: FromWKB(expression, **extra) + +*Availability*: MariaDB, `MySQL +`__, +Oracle, `PostGIS `__, SpatiaLite + +Creates geometry from `Well-known binary (WKB)`_ representation. + +``FromWKT`` +=========== + +.. versionadded:: 4.2 + +.. class:: FromWKT(expression, **extra) + +*Availability*: MariaDB, `MySQL +`__, +Oracle, `PostGIS `__, SpatiaLite + +Creates geometry from `Well-known text (WKT)`_ representation. + ``GeoHash`` =========== @@ -596,3 +618,6 @@ parameters. Accepts two geographic fields or expressions and returns the union of both geometries. + +.. _`Well-known binary (WKB)`: https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary +.. _`Well-known text (WKT)`: https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index c04d0ab57c8..aa2deed932e 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -167,6 +167,11 @@ Minor features :class:`IsEmpty() ` expression allow filtering empty geometries on PostGIS. +* The new :class:`FromWKB() ` + and :class:`FromWKT() ` + functions allow creating geometries from Well-known binary (WKB) and + Well-known text (WKT) representations. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 535e552aa19..59b0410aca5 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -326,6 +326,24 @@ class GISFunctionsTests(FuncTestMixin, TestCase): ).get(name="Foo") self.assertEqual(rhr_rings, st.force_polygon_cw.coords) + @skipUnlessDBFeature("has_FromWKB_function") + def test_fromwkb(self): + g = Point(56.811078, 60.608647) + g2 = City.objects.values_list( + functions.FromWKB(Value(g.wkb.tobytes())), + flat=True, + )[0] + self.assertIs(g.equals_exact(g2, 0.00001), True) + + @skipUnlessDBFeature("has_FromWKT_function") + def test_fromwkt(self): + g = Point(56.811078, 60.608647) + g2 = City.objects.values_list( + functions.FromWKT(Value(g.wkt)), + flat=True, + )[0] + self.assertIs(g.equals_exact(g2, 0.00001), True) + @skipUnlessDBFeature("has_GeoHash_function") def test_geohash(self): # Reference query: