Repository: opendatacube/datacube-core

Issue:
Data Cube Geometry creation fails with 3D Geometries
The [polygon drill](http://datacube-core.readthedocs.io/en/stable/user/guide.html#polygon-drill) code recipe shows how to create a Data Cube geometry from a Shapefile. Unfortunately it fails when the Shapefile contains 3D coordinates. This has occurred with a Shapefile created from KML files created in Google Earth, then converted using ArcMap.

We don't necessarily need to do much with 3D coordinates, but we could:
- Fail with a more helpful error message
- Strip the `Z` coordinate out and display a warning
- (maybe) extend geometry into the third dimension

Failing code:
```python
from datacube.utils import geometry

geom = {'coordinates': [[(115.8929714190001, -28.577007674999948, 0.0),
  (115.90275429200005, -28.57698532699993, 0.0),
  (115.90412631000004, -28.577577566999935, 0.0),
  (115.90157040700001, -28.58521105999995, 0.0),
  (115.89382838900008, -28.585473711999953, 0.0),
  (115.8929714190001, -28.577007674999948, 0.0)]],
'type': 'Polygon'}

g = geometry.Geometry(geom)
```
```
TypeError                                 Traceback (most recent call last)
<ipython-input-34-da159511be4b> in <module>()
      9 'type': 'Polygon'}
     10 
---> 11 g = geometry.Geometry(geom)

/g/data/v10/public/modules/agdc-py3/1.2.2/lib/python3.5/site-packages/datacube/utils/geometry.py in __init__(self, geo, crs)
    343     def __init__(self, geo, crs=None):
    344         self.crs = crs
--> 345         self._geom = Geometry._geom_makers[geo['type']](geo['coordinates'])
    346 
    347     @property

/g/data/v10/public/modules/agdc-py3/1.2.2/lib/python3.5/site-packages/datacube/utils/geometry.py in _make_polygon(coordinates)
    252 
    253 def _make_polygon(coordinates):
--> 254     return _make_multi(ogr.wkbPolygon, functools.partial(_make_linear, ogr.wkbLinearRing), coordinates)
    255 
    256 

/g/data/v10/public/modules/agdc-py3/1.2.2/lib/python3.5/site-packages/datacube/utils/geometry.py in _make_multi(type_, maker, coords)
    228     geom = ogr.Geometry(type_)
    229     for coord in coords:
--> 230         geom.AddGeometryDirectly(maker(coord))
    231     return geom
    232 

/g/data/v10/public/modules/agdc-py3/1.2.2/lib/python3.5/site-packages/datacube/utils/geometry.py in _make_linear(type_, coordinates)
    235     geom = ogr.Geometry(type_)
    236     for pt in coordinates:
--> 237         geom.AddPoint_2D(*pt)
    238     return geom
    239 

/g/data/v10/public/modules/agdc-py3-env/20170221/envs/agdc/lib/python3.5/site-packages/osgeo/ogr.py in AddPoint_2D(self, *args)
   6216     def AddPoint_2D(self, *args):
   6217         """AddPoint_2D(Geometry self, double x, double y)"""
-> 6218         return _ogr.Geometry_AddPoint_2D(self, *args)
   6219 
   6220 

TypeError: Geometry_AddPoint_2D() takes exactly 3 arguments (4 given)
```


Discussion:

Resolving patch:
diff --git a/datacube/utils/geometry.py b/datacube/utils/geometry.py
--- a/datacube/utils/geometry.py
+++ b/datacube/utils/geometry.py
@@ -250,7 +250,8 @@ def _make_multi(type_, maker, coords):
 def _make_linear(type_, coordinates):
     geom = ogr.Geometry(type_)
     for pt in coordinates:
-        geom.AddPoint_2D(*pt)
+        # Ignore the third dimension
+        geom.AddPoint_2D(*pt[0:2])
     return geom
 
 
@@ -321,7 +322,11 @@ def wrapped(self, other):
 
 class Geometry(object):
     """
-    Geometry with CRS
+    2D Geometry with CRS
+
+    Instantiate with a GeoJSON structure
+
+    If 3D coordinates are supplied, they are converted to 2D by dropping the Z points.
 
     :type _geom: ogr.Geometry
     :type crs: CRS


Test patch:
diff --git a/tests/test_geometry.py b/tests/test_geometry.py
--- a/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -1,4 +1,3 @@
-
 from __future__ import absolute_import
 
 import osgeo
@@ -111,19 +110,19 @@ def test_unary_union():
 
     union1 = geometry.unary_union([box1, box4])
     assert union1.type == 'MultiPolygon'
-    assert union1.area == 2.0*box1.area
+    assert union1.area == 2.0 * box1.area
 
     union2 = geometry.unary_union([box1, box2])
     assert union2.type == 'Polygon'
-    assert union2.area == 1.5*box1.area
+    assert union2.area == 1.5 * box1.area
 
     union3 = geometry.unary_union([box1, box2, box3, box4])
     assert union3.type == 'Polygon'
-    assert union3.area == 2.5*box1.area
+    assert union3.area == 2.5 * box1.area
 
     union4 = geometry.unary_union([union1, box2, box3])
     assert union4.type == 'Polygon'
-    assert union4.area == 2.5*box1.area
+    assert union4.area == 2.5 * box1.area
 
 
 def test_unary_intersection():
@@ -194,7 +193,7 @@ def test_geobox():
         [(-148.2697, 35.20111), (-149.31254, 35.20111), (-149.31254, 36.331431), (-148.2697, 36.331431)],
         [(-148.2697, -35.20111), (-149.31254, -35.20111), (-149.31254, -36.331431), (-148.2697, -36.331431),
          (148.2697, -35.20111)],
-        ]
+    ]
     for points in points_list:
         polygon = geometry.polygon(points, crs=geometry.CRS('EPSG:3577'))
         resolution = (-25, 25)
@@ -263,3 +262,23 @@ def test_wrap_dateline():
     wrapped = wrap.to_crs(geog_crs, wrapdateline=True)
     assert wrapped.type == 'MultiPolygon'
     assert not wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=geog_crs))
+
+
+def test_3d_geometry_converted_to_2d_geometry():
+    coordinates = [(115.8929714190001, -28.577007674999948, 0.0),
+                   (115.90275429200005, -28.57698532699993, 0.0),
+                   (115.90412631000004, -28.577577566999935, 0.0),
+                   (115.90157040700001, -28.58521105999995, 0.0),
+                   (115.89382838900008, -28.585473711999953, 0.0),
+                   (115.8929714190001, -28.577007674999948, 0.0)]
+    geom_3d = {'coordinates': [coordinates],
+               'type': 'Polygon'}
+    geom_2d = {'coordinates': [[(x, y) for x, y, z in coordinates]],
+               'type': 'Polygon'}
+
+    g_2d = geometry.Geometry(geom_2d)
+    g_3d = geometry.Geometry(geom_3d)
+
+    assert {2} == set(len(pt) for pt in g_3d.boundary.coords)  # All coordinates are 2D
+
+    assert g_2d == g_3d  # 3D geometry has been converted to a 2D by dropping the Z axis