Repository: natcap/invest (python)

Issue:
Offer a more helpful validation message when a user provides an OVR file
This has happened on the forums a couple of times now, but here's the latest example: https://community.naturalcapitalproject.org/t/pollination-model-error-after-updating-to-invest-3-8-0-unable-to-initiate-run/856

When a user provides an overviews file instead of the GeoTiff it's associated with, the overviews file triggers the validation error stating that the file must be projected.  It would be nice if we could check if this file is an overviews file and provide a more specific and helpful error message for this common use case.

Marked as a 3 difficulty because the slightly more interesting thing will be figuring out how to build an overviews file within GDAL as part of the tests.  I think maybe @richpsharp has done that before?

Pull request description:
Handle validation message for accidental .ovr files
# Description
Overview `.ovr` files pose a validation glitch when input instead of a proper `.tif` raster. These files can be opened as a raster but ultimately is invalid and fails when checking projection, because it's empty. 

This PR adds a validation check specifically for `.ovr` files and provides a more useful validation error message.

Fixes #50 

# Checklist
- [x] Updated HISTORY.rst (if these changes are user-facing)

~~- [ ] Updated the user's guide (if needed)~~


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 73fb310fe..3cdbad88d 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -52,14 +52,14 @@ Unreleased Changes (3.9.1)
     * Fixed an issue on Mac OS when certain models would loop indefinitely and
       never complete.  This was addressed by bumping the ``taskgraph``
       requirement version to ``0.10.3``
+    * Provide a better validation error message when an overview '.ovr' file
+      is input instead of a valid raster.
 * Fisheries Habitat Scenario Tool
     * Fixed divide-by-zero bug that was causing a RuntimeWarning in the logs.
       This bug did not affect the output.
 * SDR
     * Fixed a bug in validation that did not warn against different coordinate
       systems (all SDR inputs must share a common coordinate system).
-* Wind Energy
-    * Raising ValueError when AOI does not intersect Wind Data points.
 
 3.9.0 (2020-12-11)
 ------------------
diff --git a/src/natcap/invest/validation.py b/src/natcap/invest/validation.py
index 0813feba2..3e21baf56 100644
--- a/src/natcap/invest/validation.py
+++ b/src/natcap/invest/validation.py
@@ -306,6 +306,9 @@ def check_raster(filepath, projected=False, projection_units=None):
 
     if gdal_dataset is None:
         return "File could not be opened as a GDAL raster"
+    # Check that an overview .ovr file wasn't opened.
+    if os.path.splitext(filepath)[1] == '.ovr':
+        return "File found to be an overview '.ovr' file."
 
     srs = osr.SpatialReference()
     srs.ImportFromWkt(gdal_dataset.GetProjection())
diff --git a/src/natcap/invest/wind_energy.py b/src/natcap/invest/wind_energy.py
index d990422f4..fed74d215 100644
--- a/src/natcap/invest/wind_energy.py
+++ b/src/natcap/invest/wind_energy.py
@@ -2405,14 +2405,6 @@ def _clip_vector_by_vector(
         base_layer_defn.GetName(), base_layer.GetSpatialRef(), base_geom_type)
     base_layer.Clip(clip_layer, target_layer)
 
-    empty_clip = False
-    # Check if the feature count is less than 1, indicating the two vectors
-    # did not intersect. This will raise a ValueError below. GetFeatureCount
-    # can return -1 if the count is not known or too expensive to compute.
-    if target_layer.GetFeatureCount() <= 0:
-        empty_clip = True
-
-    # Allow function to clean up resources
     target_layer = None
     target_vector = None
     clip_vector = None
@@ -2423,12 +2415,6 @@ def _clip_vector_by_vector(
     if base_sr_wkt != target_sr_wkt:
         shutil.rmtree(temp_dir, ignore_errors=True)
 
-    if empty_clip:
-        raise ValueError(
-            f"Clipping {base_vector_path} by {clip_vector_path} returned 0"
-            " features. If an AOI was provided this could mean the AOI and"
-            " Wind Data do not intersect spatially.")
-
     LOGGER.info('Finished _clip_vector_by_vector')
 
 


Test patch:
diff --git a/tests/test_validation.py b/tests/test_validation.py
index c7930b524..70d3442cc 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -347,6 +347,30 @@ class RasterValidation(unittest.TestCase):
         error_msg = validation.check_raster(filepath)
         self.assertTrue('could not be opened as a GDAL raster' in error_msg)
 
+    def test_invalid_ovr_raster(self):
+        """Validation: test when a .tif.ovr file is input as a raster."""
+        from natcap.invest import validation
+
+        # Use EPSG:32731  # WGS84 / UTM zone 31s
+        driver = gdal.GetDriverByName('GTiff')
+        filepath = os.path.join(self.workspace_dir, 'raster.tif')
+        raster = driver.Create(filepath, 3, 3, 1, gdal.GDT_Int32)
+        meters_srs = osr.SpatialReference()
+        meters_srs.ImportFromEPSG(32731)
+        raster.SetProjection(meters_srs.ExportToWkt())
+        raster = None
+        # I could only create overviews when opening the file, not on creation.
+        # Build overviews taken from:
+        # https://gis.stackexchange.com/questions/270498/compress-gtiff-external-overviews-with-gdal-api
+        raster = gdal.OpenEx(filepath)
+        gdal.SetConfigOption("COMPRESS_OVERVIEW", "DEFLATE")
+        raster.BuildOverviews("AVERAGE", [2, 4, 8, 16, 32, 64, 128, 256])
+        raster = None
+
+        filepath_ovr = os.path.join(self.workspace_dir, 'raster.tif.ovr')
+        error_msg = validation.check_raster(filepath_ovr)
+        self.assertTrue('File found to be an overview' in error_msg)
+
     def test_raster_not_projected(self):
         """Validation: test when a raster is not linearly projected."""
         from natcap.invest import validation
diff --git a/tests/test_wind_energy.py b/tests/test_wind_energy.py
index ecfcef410..359c5e2fe 100644
--- a/tests/test_wind_energy.py
+++ b/tests/test_wind_energy.py
@@ -734,36 +734,6 @@ class WindEnergyRegressionTests(unittest.TestCase):
 
         self.assertRaises(ValueError, wind_energy.execute, args)
 
-    def test_clip_vector_value_error(self):
-        """WindEnergy: Test AOI doesn't intersect Wind Data points."""
-        from natcap.invest import wind_energy
-        from natcap.invest.utils import _assert_vectors_equal
-
-        args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
-        args['aoi_vector_path'] = os.path.join(
-            SAMPLE_DATA, 'New_England_US_Aoi.shp')
-
-        # Make up some Wind Data points that live outside AOI
-        wind_data_csv = os.path.join(args['workspace_dir'], 'temp-wind-data.csv')
-        with open(wind_data_csv, 'w') as open_table:
-            open_table.write('LONG,LATI,LAM,K,REF\n')
-            open_table.write('-60.5,25.0,7.59,2.6,10\n')
-            open_table.write('-59.5,24.0,7.59,2.6,10\n')
-            open_table.write('-58.5,24.5,7.59,2.6,10\n')
-            open_table.write('-58.95,24.95,7.59,2.6,10\n')
-            open_table.write('-57.95,24.95,7.59,2.6,10\n')
-            open_table.write('-57.95,25.95,7.59,2.6,10\n')
-
-        args['wind_data_path'] = wind_data_csv
-
-        # AOI and wind data should not overlap, leading to a ValueError in 
-        # clip_vector_by_vector
-        with self.assertRaises(ValueError) as cm:
-            wind_energy.execute(args)
-
-        self.assertTrue(
-            "returned 0 features. If an AOI was" in str(cm.exception))
-
 
 class WindEnergyValidationTests(unittest.TestCase):
     """Tests for the Wind Energy Model ARGS_SPEC and validation."""