From 60c53dd07bcd907829343b2a2a8b3374c19daf82 Mon Sep 17 00:00:00 2001
From: Peter Powers <pmpowers@usgs.gov>
Date: Thu, 27 Jan 2022 11:26:05 -0700
Subject: [PATCH] expanded map-region rectangle

---
 .../gov/usgs/earthquake/nshmp/calc/Sites.java | 34 +-------
 .../gov/usgs/earthquake/nshmp/geo/Region.java |  2 +-
 .../earthquake/nshmp/geo/json/GeoJson.java    | 79 +++++++++++++++++++
 .../earthquake/nshmp/model/HazardModel.java   |  5 +-
 4 files changed, 86 insertions(+), 34 deletions(-)

diff --git a/src/main/java/gov/usgs/earthquake/nshmp/calc/Sites.java b/src/main/java/gov/usgs/earthquake/nshmp/calc/Sites.java
index 28e892e5..29b8bac9 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/calc/Sites.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/calc/Sites.java
@@ -1,6 +1,5 @@
 package gov.usgs.earthquake.nshmp.calc;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static gov.usgs.earthquake.nshmp.Text.LOG_INDENT;
 import static gov.usgs.earthquake.nshmp.geo.BorderType.MERCATOR_LINEAR;
 import static java.util.stream.Collectors.toUnmodifiableList;
@@ -188,7 +187,7 @@ public final class Sites {
       mapRegionIndex = 1;
       Feature mapPoly = features.get(0);
       LocationList mapPolyBorder = mapPoly.asPolygonBorder();
-      mapBounds = Optional.of(validateExtents(mapPolyBorder).bounds());
+      mapBounds = Optional.of(GeoJson.validateBoundingBox(mapPolyBorder).bounds());
       boundsName = mapPoly.properties().getString(Site.Key.NAME).orElse("Map Extents");
     }
 
@@ -204,7 +203,7 @@ public final class Sites {
 
     Region calcRegion = null;
     try {
-      Bounds b = validateExtents(sitesPolyBorder).bounds();
+      Bounds b = GeoJson.validateBoundingBox(sitesPolyBorder).bounds();
       calcRegion = Regions.createRectangular(mapName, b.min, b.max);
     } catch (IllegalArgumentException iae) {
       calcRegion = Regions.create(mapName, sitesPolyBorder, MERCATOR_LINEAR);
@@ -230,35 +229,6 @@ public final class Sites {
         .collect(toUnmodifiableList());
   }
 
-  private static LocationList validateExtents(LocationList locations) {
-
-    /* Rectangular linear ring check. */
-    checkArgument(
-        locations.size() == 5,
-        "Extents polygon must contain 5 coordinates: %s",
-        locations);
-
-    /* Order agnostic rectilinear lat-lon check. */
-    Location p1 = locations.get(0);
-    Location p2 = locations.get(1);
-    Location p3 = locations.get(2);
-    Location p4 = locations.get(3);
-    boolean rectangular = (p1.latitude == p2.latitude)
-        ? (p3.latitude == p4.latitude &&
-            p1.longitude == p4.longitude &&
-            p2.longitude == p3.longitude)
-        : (p1.latitude == p4.latitude &&
-            p2.latitude == p3.latitude &&
-            p1.longitude == p2.longitude &&
-            p3.longitude == p4.longitude);
-    checkArgument(
-        rectangular,
-        "Extents polygon does not define a lat-lon Mercator rectangle: %s",
-        locations);
-
-    return locations;
-  }
-
   private static final int TO_STRING_LIMIT = 5;
   private static final String SITE_INDENT = LOG_INDENT + "       ";
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/geo/Region.java b/src/main/java/gov/usgs/earthquake/nshmp/geo/Region.java
index f65eb534..0bae48db 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/geo/Region.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/geo/Region.java
@@ -435,7 +435,7 @@ public class Region {
 
   /*
    * Initialize a rectangular region from two opposing corners expanding north
-   * and east border slightly to satisfy constains operations
+   * and east border slightly to satisfy contains operations
    */
   void initRectangular(Location loc1, Location loc2) {
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/geo/json/GeoJson.java b/src/main/java/gov/usgs/earthquake/nshmp/geo/json/GeoJson.java
index 8f93c878..bab75c23 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/geo/json/GeoJson.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/geo/json/GeoJson.java
@@ -23,6 +23,10 @@ import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
 
 import gov.usgs.earthquake.nshmp.Text;
+import gov.usgs.earthquake.nshmp.geo.Bounds;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.geo.LocationList;
+import gov.usgs.earthquake.nshmp.geo.Locations;
 
 /**
  * Entry point for creating and parsing <a href="http://geojson.org">GeoJSON</a>
@@ -191,6 +195,81 @@ public abstract class GeoJson {
     return new FromUrl(json);
   }
 
+  /**
+   * Checks that bounding box location list contains 5 coordinates defining a
+   * mercator lat/lon rectangle.
+   *
+   * @param locations to check
+   * @return the supplied locaiton list
+   */
+  public static LocationList validateBoundingBox(LocationList locations) {
+
+    /* Rectangular linear ring check. */
+    checkArgument(
+        locations.size() == 5,
+        "Extents polygon must contain 5 coordinates: %s",
+        locations);
+
+    /* Order agnostic rectilinear lat-lon check. */
+    Location p1 = locations.get(0);
+    Location p2 = locations.get(1);
+    Location p3 = locations.get(2);
+    Location p4 = locations.get(3);
+    boolean rectangular = (p1.latitude == p2.latitude)
+        ? (p3.latitude == p4.latitude &&
+            p1.longitude == p4.longitude &&
+            p2.longitude == p3.longitude)
+        : (p1.latitude == p4.latitude &&
+            p2.latitude == p3.latitude &&
+            p1.longitude == p2.longitude &&
+            p3.longitude == p4.longitude);
+    checkArgument(
+        rectangular,
+        "Extents polygon does not define a lat-lon Mercator rectangle: %s",
+        locations);
+
+    return locations;
+  }
+
+  /**
+   * Checks that the supplied location list defines a rectangular lat/lon area
+   * and returns a slightly expanded version that will enclosepoints on the
+   * borders of the area. Points coincident with east and north edges of
+   * polygons are considered inside as a result.
+   */
+  public static LocationList expandRectangularArea(LocationList locations) {
+    Bounds b = validateBoundingBox(locations).bounds();
+
+    double lat1 = b.min.latitude;
+    double lat2 = b.max.latitude;
+    double lon1 = b.min.longitude;
+    double lon2 = b.max.longitude;
+
+    // checkArgument(lat1 != lat2, "Input lats cannot be the same");
+    // checkArgument(lon1 != lon2, "Input lons cannot be the same");
+
+    double minLat = Math.min(lat1, lat2);
+    double minLon = Math.min(lon1, lon2);
+    double maxLat = Math.max(lat1, lat2);
+    double maxLon = Math.max(lon1, lon2);
+    double offset = Locations.TOLERANCE;
+
+    // ternaries prevent exceedance of max lat-lon values
+    maxLat += (maxLat <= 90.0 - offset) ? offset : 0.0;
+    maxLon += (maxLon <= 180.0 - offset) ? offset : 0.0;
+    minLat -= (minLat >= -90.0 + offset) ? offset : 0.0;
+    minLon -= (minLon >= -180.0 + offset) ? offset : 0.0;
+
+    LocationList expanded = LocationList.of(
+        Location.create(minLon, minLat),
+        Location.create(minLon, maxLat),
+        Location.create(maxLon, maxLat),
+        Location.create(maxLon, minLat));
+
+    // initBordered(locs, MERCATOR_LINEAR);
+    return expanded;
+  }
+
   /**
    * A reusable GeoJSON builder.
    */
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java b/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
index 8634d422..98f2003d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
@@ -33,8 +33,10 @@ import com.google.gson.annotations.SerializedName;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.geo.LocationList;
 import gov.usgs.earthquake.nshmp.geo.Locations;
 import gov.usgs.earthquake.nshmp.geo.json.Feature;
+import gov.usgs.earthquake.nshmp.geo.json.GeoJson;
 import gov.usgs.earthquake.nshmp.gmm.Gmm;
 import gov.usgs.earthquake.nshmp.gmm.GroundMotionModel;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
@@ -354,7 +356,8 @@ public final class HazardModel implements Iterable<SourceTree> {
 
     MapRegion(Feature feature) {
       this.feature = feature;
-      this.area = new Area(Locations.toPath(feature.asPolygonBorder()));
+      LocationList areaLocs = GeoJson.expandRectangularArea(feature.asPolygonBorder());
+      this.area = new Area(Locations.toPath(areaLocs));
     }
 
     boolean contains(Location loc) {
-- 
GitLab