diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 8ac4cfa21e..e563aa105e 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -226,13 +226,37 @@ class WKBWriter(IOBase): super(WKBWriter, self).__init__() self.outdim = dim + def _handle_empty_point(self, geom): + from django.contrib.gis.geos import Point + if isinstance(geom, Point) and geom.empty: + if self.srid: + # PostGIS uses POINT(NaN NaN) for WKB representation of empty + # points. Use it for EWKB as it's a PostGIS specific format. + # https://trac.osgeo.org/postgis/ticket/3181 + geom = Point(float('NaN'), float('NaN'), srid=geom.srid) + else: + raise ValueError('Empty point is not representable in WKB.') + return geom + def write(self, geom): "Returns the WKB representation of the given geometry." - return six.memoryview(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) + from django.contrib.gis.geos import Polygon + geom = self._handle_empty_point(geom) + wkb = wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t())) + if isinstance(geom, Polygon) and geom.empty: + # Fix GEOS output for empty polygon. + # See https://trac.osgeo.org/geos/ticket/680. + wkb = wkb[:-8] + b'\0' * 4 + return six.memoryview(wkb) def write_hex(self, geom): "Returns the HEXEWKB representation of the given geometry." - return wkb_writer_write_hex(self.ptr, geom.ptr, byref(c_size_t())) + from django.contrib.gis.geos.polygon import Polygon + geom = self._handle_empty_point(geom) + wkb = wkb_writer_write_hex(self.ptr, geom.ptr, byref(c_size_t())) + if isinstance(geom, Polygon) and geom.empty: + wkb = wkb[:-16] + b'0' * 8 + return wkb # ### WKBWriter Properties ### diff --git a/tests/gis_tests/geos_tests/test_io.py b/tests/gis_tests/geos_tests/test_io.py index 00fe3b67fd..601301331e 100644 --- a/tests/gis_tests/geos_tests/test_io.py +++ b/tests/gis_tests/geos_tests/test_io.py @@ -4,7 +4,8 @@ import binascii from unittest import skipUnless from django.contrib.gis.geos import ( - HAS_GEOS, GEOSGeometry, Point, WKBReader, WKBWriter, WKTReader, WKTWriter, + HAS_GEOS, GEOSGeometry, Point, Polygon, WKBReader, WKBWriter, WKTReader, + WKTWriter, ) from django.test import SimpleTestCase from django.utils.six import memoryview @@ -155,3 +156,41 @@ class GEOSIOTest(SimpleTestCase): with self.assertRaisesMessage(AttributeError, 'WKT output rounding precision must be '): wkt_w.precision = 'potato' + + def test_empty_point_wkb(self): + p = Point(srid=4326) + wkb_w = WKBWriter() + + wkb_w.srid = False + with self.assertRaisesMessage(ValueError, 'Empty point is not representable in WKB.'): + wkb_w.write(p) + with self.assertRaisesMessage(ValueError, 'Empty point is not representable in WKB.'): + wkb_w.write_hex(p) + + wkb_w.srid = True + for byteorder, hex in enumerate([ + b'0020000001000010E67FF80000000000007FF8000000000000', + b'0101000020E6100000000000000000F87F000000000000F87F', + ]): + wkb_w.byteorder = byteorder + self.assertEqual(wkb_w.write_hex(p), hex) + self.assertEqual(GEOSGeometry(wkb_w.write_hex(p)), p) + self.assertEqual(wkb_w.write(p), memoryview(binascii.a2b_hex(hex))) + self.assertEqual(GEOSGeometry(wkb_w.write(p)), p) + + def test_empty_polygon_wkb(self): + p = Polygon(srid=4326) + p_no_srid = Polygon() + wkb_w = WKBWriter() + wkb_w.srid = True + for byteorder, hexes in enumerate([ + (b'000000000300000000', b'0020000003000010E600000000'), + (b'010300000000000000', b'0103000020E610000000000000'), + ]): + wkb_w.byteorder = byteorder + for srid, hex in enumerate(hexes): + wkb_w.srid = srid + self.assertEqual(wkb_w.write_hex(p), hex) + self.assertEqual(GEOSGeometry(wkb_w.write_hex(p)), p if srid else p_no_srid) + self.assertEqual(wkb_w.write(p), memoryview(binascii.a2b_hex(hex))) + self.assertEqual(GEOSGeometry(wkb_w.write(p)), p if srid else p_no_srid)