diff --git a/presto-docs/src/main/sphinx/functions/geospatial.rst b/presto-docs/src/main/sphinx/functions/geospatial.rst index 06e395d4c28f..755005d0afce 100644 --- a/presto-docs/src/main/sphinx/functions/geospatial.rst +++ b/presto-docs/src/main/sphinx/functions/geospatial.rst @@ -203,6 +203,10 @@ Accessors For GeometryCollection types, returns the sum of the areas of the individual geometries. +.. function:: ST_Area(SphericalGeography) -> double + + Returns the area of a polygon or multi-polygon in square meters using a spherical model for Earth. + .. function:: ST_Centroid(Geometry) -> Geometry Returns the point value that is the mathematical centroid of a geometry. diff --git a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java index d1fe6652b346..be5fbaccd39c 100644 --- a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java +++ b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java @@ -98,10 +98,12 @@ import static com.facebook.presto.spi.type.StandardTypes.VARBINARY; import static com.facebook.presto.spi.type.StandardTypes.VARCHAR; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import static io.airlift.slice.Slices.utf8Slice; import static io.airlift.slice.Slices.wrappedBuffer; import static java.lang.Double.isInfinite; import static java.lang.Double.isNaN; +import static java.lang.Math.PI; import static java.lang.Math.atan2; import static java.lang.Math.cos; import static java.lang.Math.sin; @@ -119,6 +121,7 @@ public final class GeoFunctions private static final Slice EMPTY_POLYGON = serialize(new OGCPolygon(new Polygon(), null)); private static final Slice EMPTY_MULTIPOINT = serialize(createFromEsriGeometry(new MultiPoint(), null, true)); private static final double EARTH_RADIUS_KM = 6371.01; + private static final double EARTH_RADIUS_M = EARTH_RADIUS_KM * 1000.0; private static final Map NON_SIMPLE_REASONS = ImmutableMap.builder() .put(DegenerateSegments, "Degenerate segments") .put(Clustering, "Repeated points") @@ -1507,6 +1510,166 @@ private static void validateSphericalType(String function, OGCGeometry geometry, } } + @SqlNullable + @Description("Returns the area of a geometry on the Earth's surface using spherical model") + @ScalarFunction("ST_Area") + @SqlType(DOUBLE) + public static Double stSphericalArea(@SqlType(SPHERICAL_GEOGRAPHY_TYPE_NAME) Slice input) + { + OGCGeometry geometry = deserialize(input); + if (geometry.isEmpty()) { + return null; + } + + validateSphericalType("ST_Area", geometry, EnumSet.of(POLYGON, MULTI_POLYGON)); + + Polygon polygon = (Polygon) geometry.getEsriGeometry(); + + // See https://www.movable-type.co.uk/scripts/latlong.html + // and http://osgeo-org.1560.x6.nabble.com/Area-of-a-spherical-polygon-td3841625.html + // and https://www.element84.com/blog/determining-if-a-spherical-polygon-contains-a-pole + // for the underlying Maths + + double sphericalExcess = 0.0; + + int numPaths = polygon.getPathCount(); + for (int i = 0; i < numPaths; i++) { + double sign = polygon.isExteriorRing(i) ? 1.0 : -1.0; + sphericalExcess += sign * Math.abs(computeSphericalExcess(polygon, polygon.getPathStart(i), polygon.getPathEnd(i))); + } + + // Math.abs is required here because for Polygons with a 2D area of 0 + // isExteriorRing returns false for the exterior ring + return Math.abs(sphericalExcess * EARTH_RADIUS_M * EARTH_RADIUS_M); + } + + private static double computeSphericalExcess(Polygon polygon, int start, int end) + { + // Our calculations rely on not processing the same point twice + if (polygon.getPoint(end - 1).equals(polygon.getPoint(start))) { + end = end - 1; + } + + if (end - start < 3) { + // A path with less than 3 distinct points is not valid for calculating an area + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Polygon is not valid: a loop contains less then 3 vertices."); + } + + Point point = new Point(); + + // Initialize the calculator with the last point + polygon.getPoint(end - 1, point); + SphericalExcessCalculator calculator = new SphericalExcessCalculator(point); + + for (int i = start; i < end; i++) { + polygon.getPoint(i, point); + calculator.add(point); + } + + return calculator.computeSphericalExcess(); + } + + private static class SphericalExcessCalculator + { + private static final double TWO_PI = 2 * Math.PI; + private static final double THREE_PI = 3 * Math.PI; + + private double sphericalExcess; + private double courseDelta; + + private boolean firstPoint; + private double firstInitialBearing; + private double previousFinalBearing; + + private double previousPhi; + private double previousCos; + private double previousSin; + private double previousTan; + private double previousLongitude; + + private boolean done; + + public SphericalExcessCalculator(Point endPoint) + { + previousPhi = toRadians(endPoint.getY()); + previousSin = Math.sin(previousPhi); + previousCos = Math.cos(previousPhi); + previousTan = Math.tan(previousPhi / 2); + previousLongitude = toRadians(endPoint.getX()); + firstPoint = true; + } + + private void add(Point point) throws IllegalStateException + { + checkState(!done, "Computation of spherical excess is complete"); + + double phi = toRadians(point.getY()); + double tan = Math.tan(phi / 2); + double longitude = toRadians(point.getX()); + + // We need to check for that specifically + // Otherwise calculating the bearing is not deterministic + if (longitude == previousLongitude && phi == previousPhi) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Polygon is not valid: it has two identical consecutive vertices"); + } + + double deltaLongitude = longitude - previousLongitude; + sphericalExcess += 2 * Math.atan2(Math.tan(deltaLongitude / 2) * (previousTan + tan), 1 + previousTan * tan); + + double cos = Math.cos(phi); + double sin = Math.sin(phi); + double sinOfDeltaLongitude = Math.sin(deltaLongitude); + double cosOfDeltaLongitude = Math.cos(deltaLongitude); + + // Initial bearing from previous to current + double y = sinOfDeltaLongitude * cos; + double x = previousCos * sin - previousSin * cos * cosOfDeltaLongitude; + double initialBearing = (Math.atan2(y, x) + TWO_PI) % TWO_PI; + + // Final bearing from previous to current = opposite of bearing from current to previous + double finalY = -sinOfDeltaLongitude * previousCos; + double finalX = previousSin * cos - previousCos * sin * cosOfDeltaLongitude; + double finalBearing = (Math.atan2(finalY, finalX) + PI) % TWO_PI; + + // When processing our first point we don't yet have a previousFinalBearing + if (firstPoint) { + // So keep our initial bearing around, and we'll use it at the end + // with the last final bearing + firstInitialBearing = initialBearing; + firstPoint = false; + } + else { + courseDelta += (initialBearing - previousFinalBearing + THREE_PI) % TWO_PI - PI; + } + + courseDelta += (finalBearing - initialBearing + THREE_PI) % TWO_PI - PI; + + previousFinalBearing = finalBearing; + previousCos = cos; + previousSin = sin; + previousPhi = phi; + previousTan = tan; + previousLongitude = longitude; + } + + public double computeSphericalExcess() + { + if (!done) { + // Now that we have our last final bearing, we can calculate the remaining course delta + courseDelta += (firstInitialBearing - previousFinalBearing + THREE_PI) % TWO_PI - PI; + + // The courseDelta should be 2Pi or - 2Pi, unless a pole is enclosed (and then it should be ~ 0) + // In which case we need to correct the spherical excess by 2Pi + if (Math.abs(courseDelta) < PI / 4) { + sphericalExcess = Math.abs(sphericalExcess) - TWO_PI; + } + done = true; + } + + return sphericalExcess; + } + } + private static Iterable getGeometrySlicesFromBlock(Block block) { requireNonNull(block, "block is null"); diff --git a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/BenchmarkSTArea.java b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/BenchmarkSTArea.java new file mode 100644 index 000000000000..2c3f91cb59c5 --- /dev/null +++ b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/BenchmarkSTArea.java @@ -0,0 +1,136 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.geospatial; + +import io.airlift.slice.Slice; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.VerboseMode; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.plugin.geospatial.GeoFunctions.stGeometryFromText; +import static com.facebook.presto.plugin.geospatial.GeoFunctions.toSphericalGeography; +import static com.facebook.presto.plugin.geospatial.GeometryBenchmarkUtils.loadPolygon; +import static io.airlift.slice.Slices.utf8Slice; +import static org.testng.Assert.assertEquals; + +@State(Scope.Thread) +@Fork(2) +@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class BenchmarkSTArea +{ + @Benchmark + public Object stSphericalArea(BenchmarkData data) + { + return GeoFunctions.stSphericalArea(data.geography); + } + + @Benchmark + public Object stSphericalArea500k(BenchmarkData data) + { + return GeoFunctions.stSphericalArea(data.geography500k); + } + + @Benchmark + public Object stArea(BenchmarkData data) + { + return GeoFunctions.stArea(data.geometry); + } + + @Benchmark + public Object stArea500k(BenchmarkData data) + { + return GeoFunctions.stArea(data.geometry500k); + } + + @State(Scope.Thread) + public static class BenchmarkData + { + private Slice geometry; + private Slice geometry500k; + private Slice geography; + private Slice geography500k; + + @Setup + public void setup() + throws IOException + { + geometry = stGeometryFromText(utf8Slice(loadPolygon("large_polygon.txt"))); + geometry500k = stGeometryFromText(utf8Slice(createPolygon(500000))); + geography = toSphericalGeography(geometry); + geography500k = toSphericalGeography(geometry500k); + } + } + + public static void main(String[] args) + throws IOException, RunnerException + { + // assure the benchmarks are valid before running + verify(); + + Options options = new OptionsBuilder() + .verbosity(VerboseMode.NORMAL) + .include(".*" + BenchmarkSTArea.class.getSimpleName() + ".*") + .build(); + new Runner(options).run(); + } + + @Test + public static void verify() throws IOException + { + BenchmarkData data = new BenchmarkData(); + data.setup(); + BenchmarkSTArea benchmark = new BenchmarkSTArea(); + + assertEquals(Math.round(1000 * (Double) benchmark.stSphericalArea(data) / 3.659E8), 1000); + assertEquals(Math.round(1000 * (Double) benchmark.stSphericalArea500k(data) / 38842273735.0), 1000); + assertEquals(benchmark.stArea(data), 0.05033099592771004); + assertEquals(Math.round(1000 * (Double) benchmark.stArea500k(data) / Math.PI), 1000); + } + + private static String createPolygon(double numVertices) + { + StringBuilder sb = new StringBuilder(); + sb.append("POLYGON(("); + String separator = ""; + for (int i = 0; i < numVertices; i++) { + double angle = i * Math.PI * 2 / numVertices; + sb.append(separator); + sb.append(Math.cos(angle)); + sb.append(" "); + sb.append(Math.sin(angle)); + separator = ","; + } + sb.append("))"); + return sb.toString(); + } +} diff --git a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestSphericalGeoFunctions.java b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestSphericalGeoFunctions.java index cd81dceabfb7..f17123abfa34 100644 --- a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestSphericalGeoFunctions.java +++ b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestSphericalGeoFunctions.java @@ -21,7 +21,13 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static com.facebook.presto.metadata.FunctionExtractor.extractFunctions; import static com.facebook.presto.plugin.geospatial.SphericalGeographyType.SPHERICAL_GEOGRAPHY; @@ -144,4 +150,57 @@ private void assertDistance(String wkt, String otherWkt, Double expectedDistance { assertFunction(format("ST_Distance(to_spherical_geography(ST_GeometryFromText('%s')), to_spherical_geography(ST_GeometryFromText('%s')))", wkt, otherWkt), DOUBLE, expectedDistance); } + + @Test + public void testArea() + throws IOException + { + // Empty polygon + assertFunction("ST_Area(to_spherical_geography(ST_GeometryFromText('POLYGON EMPTY')))", DOUBLE, null); + + // Invalid polygon (too few vertices) + assertInvalidFunction("ST_Area(to_spherical_geography(ST_GeometryFromText('POLYGON((90 0, 0 0))')))", "Polygon is not valid: a loop contains less then 3 vertices."); + + // Invalid data type (point) + assertInvalidFunction("ST_Area(to_spherical_geography(ST_GeometryFromText('POINT (0 1)')))", "When applied to SphericalGeography inputs, ST_Area only supports POLYGON or MULTI_POLYGON. Input type is: POINT"); + + //Invalid Polygon (duplicated point) + assertInvalidFunction("ST_Area(to_spherical_geography(ST_GeometryFromText('POLYGON((0 0, 0 1, 1 1, 1 1, 1 0, 0 0))')))", "Polygon is not valid: it has two identical consecutive vertices"); + + // A polygon around the North Pole + assertArea("POLYGON((-135 85, -45 85, 45 85, 135 85, -135 85))", 619.00E9); + + assertArea("POLYGON((0 0, 0 1, 1 1, 1 0))", 123.64E8); + + assertArea("POLYGON((-122.150124 37.486095, -122.149201 37.486606, -122.145725 37.486580, -122.145923 37.483961 , -122.149324 37.482480 , -122.150837 37.483238, -122.150901 37.485392))", 163290.93943446054); + + double angleOfOneKm = 0.008993201943349; + assertArea(format("POLYGON((0 0, %.15f 0, %.15f %.15f, 0 %.15f))", angleOfOneKm, angleOfOneKm, angleOfOneKm, angleOfOneKm), 1E6); + + // 1/4th of an hemisphere, ie 1/8th of the planet, should be close to 4PiR2/8 = 637.58E11 + assertArea("POLYGON((90 0, 0 0, 0 90))", 637.58E11); + + //A Polygon with a large hole + assertArea("POLYGON((90 0, 0 0, 0 90), (89 1, 1 1, 1 89))", 348.04E10); + + Path geometryPath = Paths.get(TestSphericalGeoFunctions.class.getClassLoader().getResource("us-states.tsv").getPath()); + Map stateGeometries = Files.lines(geometryPath) + .map(line -> line.split("\t")) + .collect(Collectors.toMap(parts -> parts[0], parts -> parts[1])); + + Path areaPath = Paths.get(TestSphericalGeoFunctions.class.getClassLoader().getResource("us-state-areas.tsv").getPath()); + Map stateAreas = Files.lines(areaPath) + .map(line -> line.split("\t")) + .filter(parts -> parts.length >= 2) + .collect(Collectors.toMap(parts -> parts[0], parts -> Double.valueOf(parts[1]))); + + for (String state : stateGeometries.keySet()) { + assertArea(stateGeometries.get(state), stateAreas.get(state)); + } + } + + private void assertArea(String wkt, double expectedArea) + { + assertFunction(format("ABS(ROUND((ST_Area(to_spherical_geography(ST_GeometryFromText('%s'))) / %f - 1 ) * %d, 0))", wkt, expectedArea, 10000), DOUBLE, 0.0); + } } diff --git a/presto-geospatial/src/test/resources/us-state-areas.tsv b/presto-geospatial/src/test/resources/us-state-areas.tsv new file mode 100644 index 000000000000..730e4c693344 --- /dev/null +++ b/presto-geospatial/src/test/resources/us-state-areas.tsv @@ -0,0 +1,50 @@ +AK 1512029420919.75 +AL 133798433564.089 +AR 138078359306.101 +AZ 295735747077.462 +CA 409564647161.456 +CO 269308715698.849 +CT 12873955652.9009 +DE 5083481242.97432 +FL 149761362689.816 +GA 152560105310.888 +HI 14715710071.3397 +IA 146458043140.78 +ID 215644605503.279 +IL 146057753165.062 +IN 93432145009.3321 +KS 213667500120.875 +KY 104609030725.925 +LA 122087680157.417 +MA 20641289915.7922 +MD 27054508743.8369 +ME 84380140281.5444 +MI 150263386643.448 +MN 217052585631.816 +MO 180004456640.739 +MS 123528032272.534 +MT 381032417364.706 +NC 130662589411.403 +ND 181977969472.119 +NE 198367040634.306 +NH 23917283751.4951 +NJ 19994599703.4562 +NM 314665412870.469 +NV 286314845000.724 +NY 126235067447.09 +OH 106856948024.766 +OK 182094848972.64 +OR 250811798127.051 +PA 117328433660.695 +RI 2665065564.06665 +SC 80925225017.8581 +SD 201400533632.426 +TN 109289108111.592 +TX 687081639377.503 +UT 219262788661.858 +VA 105088044470.682 +VT 24673886252.5229 +WA 176010024082.492 +WI 144941233960.424 +WV 63015759782.3764 +WY 253076654001.191 \ No newline at end of file diff --git a/presto-geospatial/src/test/resources/us-states.tsv b/presto-geospatial/src/test/resources/us-states.tsv index a29836594813..bb17478ce574 100644 --- a/presto-geospatial/src/test/resources/us-states.tsv +++ b/presto-geospatial/src/test/resources/us-states.tsv @@ -32,10 +32,10 @@ NM POLYGON ((-107.421329 37.000263, -106.868158 36.994786, -104.337812 36.994786 NY POLYGON ((-73.343806 45.013027, -73.332852 44.804903, -73.387622 44.618687, -73.294514 44.437948, -73.321898 44.246255, -73.436914 44.043608, -73.349283 43.769761, -73.404052 43.687607, -73.245221 43.523299, -73.278083 42.833204, -73.267129 42.745573, -73.508114 42.08834, -73.486206 42.050002, -73.55193 41.294184, -73.48073 41.21203, -73.727192 41.102491, -73.655992 40.987475, -73.22879 40.905321, -73.141159 40.965568, -72.774204 40.965568, -72.587988 40.998429, -72.28128 41.157261, -72.259372 41.042245, -72.100541 40.992952, -72.467496 40.845075, -73.239744 40.625997, -73.562884 40.582182, -73.776484 40.593136, -73.935316 40.543843, -74.022947 40.708151, -73.902454 40.998429, -74.236547 41.14083, -74.69661 41.359907, -74.740426 41.431108, -74.89378 41.436584, -75.074519 41.60637, -75.052611 41.754247, -75.173104 41.869263, -75.249781 41.863786, -75.35932 42.000709, -79.76278 42.000709, -79.76278 42.252649, -79.76278 42.269079, -79.149363 42.55388, -79.050778 42.690804, -78.853608 42.783912, -78.930285 42.953697, -79.012439 42.986559, -79.072686 43.260406, -78.486653 43.375421, -77.966344 43.369944, -77.75822 43.34256, -77.533665 43.233021, -77.391265 43.276836, -76.958587 43.271359, -76.695693 43.34256, -76.41637 43.523299, -76.235631 43.528776, -76.230154 43.802623, -76.137046 43.961454, -76.3616 44.070993, -76.312308 44.196962, -75.912491 44.366748, -75.764614 44.514625, -75.282643 44.848718, -74.828057 45.018503, -74.148916 44.991119, -73.343806 45.013027)) NC POLYGON ((-80.978661 36.562108, -80.294043 36.545677, -79.510841 36.5402, -75.868676 36.551154, -75.75366 36.151337, -76.032984 36.189676, -76.071322 36.140383, -76.410893 36.080137, -76.460185 36.025367, -76.68474 36.008937, -76.673786 35.937736, -76.399939 35.987029, -76.3616 35.943213, -76.060368 35.992506, -75.961783 35.899398, -75.781044 35.937736, -75.715321 35.696751, -75.775568 35.581735, -75.89606 35.570781, -76.147999 35.324319, -76.482093 35.313365, -76.536862 35.14358, -76.394462 34.973795, -76.279446 34.940933, -76.493047 34.661609, -76.673786 34.694471, -76.991448 34.667086, -77.210526 34.60684, -77.555573 34.415147, -77.82942 34.163208, -77.971821 33.845545, -78.179944 33.916745, -78.541422 33.851022, -79.675149 34.80401, -80.797922 34.820441, -80.781491 34.935456, -80.934845 35.105241, -81.038907 35.044995, -81.044384 35.149057, -82.276696 35.198349, -82.550543 35.160011, -82.764143 35.066903, -83.109191 35.00118, -83.618546 34.984749, -84.319594 34.990226, -84.29221 35.225734, -84.09504 35.247642, -84.018363 35.41195, -83.7719 35.559827, -83.498053 35.565304, -83.251591 35.718659, -82.994175 35.773428, -82.775097 35.997983, -82.638174 36.063706, -82.610789 35.965121, -82.216449 36.156814, -82.03571 36.118475, -81.909741 36.304691, -81.723525 36.353984, -81.679709 36.589492, -80.978661 36.562108)) ND POLYGON ((-97.228743 49.000239, -97.097296 48.682577, -97.16302 48.545653, -97.130158 48.140359, -97.053481 47.948667, -96.856311 47.609096, -96.823449 46.968294, -96.785111 46.924479, -96.801542 46.656109, -96.719387 46.437031, -96.598895 46.332969, -96.560556 45.933153, -104.047534 45.944106, -104.042057 47.861036, -104.047534 49.000239, -97.228743 49.000239)) -OH POLYGON ((-80.518598 41.978802, -80.518598 40.636951, -80.666475 40.582182, -80.595275 40.472643, -80.600752 40.319289, -80.737675 40.078303, -80.830783 39.711348, -81.219646 39.388209, -81.345616 39.344393, -81.455155 39.410117, -81.57017 39.267716, -81.685186 39.273193, -81.811156 39.0815, -81.783771 38.966484, -81.887833 38.873376, -82.03571 39.026731, -82.221926 38.785745, -82.172634 38.632391, -82.293127 38.577622, -82.331465 38.446175, -82.594358 38.424267, -82.731282 38.561191, -82.846298 38.588575, -82.890113 38.758361, -83.032514 38.725499, -83.142052 38.626914, -83.519961 38.703591, -83.678792 38.632391, -83.903347 38.769315, -84.215533 38.807653, -84.231963 38.895284, -84.43461 39.103408, -84.817996 39.103408, -84.801565 40.500028, -84.807042 41.694001, -83.454238 41.732339, -83.065375 41.595416, -82.933929 41.513262, -82.835344 41.589939, -82.616266 41.431108, -82.479343 41.381815, -82.013803 41.513262, -81.739956 41.485877, -81.444201 41.672093, -81.011523 41.852832, -80.518598 41.978802, -80.518598 41.978802)) +OH POLYGON ((-80.518598 41.978802, -80.518598 40.636951, -80.666475 40.582182, -80.595275 40.472643, -80.600752 40.319289, -80.737675 40.078303, -80.830783 39.711348, -81.219646 39.388209, -81.345616 39.344393, -81.455155 39.410117, -81.57017 39.267716, -81.685186 39.273193, -81.811156 39.0815, -81.783771 38.966484, -81.887833 38.873376, -82.03571 39.026731, -82.221926 38.785745, -82.172634 38.632391, -82.293127 38.577622, -82.331465 38.446175, -82.594358 38.424267, -82.731282 38.561191, -82.846298 38.588575, -82.890113 38.758361, -83.032514 38.725499, -83.142052 38.626914, -83.519961 38.703591, -83.678792 38.632391, -83.903347 38.769315, -84.215533 38.807653, -84.231963 38.895284, -84.43461 39.103408, -84.817996 39.103408, -84.801565 40.500028, -84.807042 41.694001, -83.454238 41.732339, -83.065375 41.595416, -82.933929 41.513262, -82.835344 41.589939, -82.616266 41.431108, -82.479343 41.381815, -82.013803 41.513262, -81.739956 41.485877, -81.444201 41.672093, -81.011523 41.852832, -80.518598 41.978802)) OK POLYGON ((-100.087706 37.000263, -94.616242 37.000263, -94.616242 36.501861, -94.430026 35.395519, -94.484796 33.637421, -94.868182 33.74696, -94.966767 33.861976, -95.224183 33.960561, -95.289906 33.87293, -95.547322 33.878407, -95.602092 33.933176, -95.8376 33.834591, -95.936185 33.889361, -96.149786 33.840068, -96.346956 33.686714, -96.423633 33.774345, -96.631756 33.845545, -96.850834 33.845545, -96.922034 33.960561, -97.173974 33.736006, -97.256128 33.861976, -97.371143 33.823637, -97.458774 33.905791, -97.694283 33.982469, -97.869545 33.851022, -97.946222 33.987946, -98.088623 34.004376, -98.170777 34.113915, -98.36247 34.157731, -98.488439 34.064623, -98.570593 34.146777, -98.767763 34.135823, -98.986841 34.223454, -99.189488 34.2125, -99.260688 34.404193, -99.57835 34.415147, -99.698843 34.382285, -99.923398 34.573978, -100.000075 34.563024, -100.000075 36.501861, -101.812942 36.501861, -103.001438 36.501861, -103.001438 37.000263, -102.042974 36.994786, -100.087706 37.000263)) OR POLYGON ((-123.211348 46.174138, -123.11824 46.185092, -122.904639 46.08103, -122.811531 45.960537, -122.762239 45.659305, -122.247407 45.549767, -121.809251 45.708598, -121.535404 45.725029, -121.217742 45.670259, -121.18488 45.604536, -120.637186 45.746937, -120.505739 45.697644, -120.209985 45.725029, -119.963522 45.823614, -119.525367 45.911245, -119.125551 45.933153, -118.988627 45.998876, -116.918344 45.993399, -116.78142 45.823614, -116.545912 45.752413, -116.463758 45.61549, -116.671881 45.319735, -116.732128 45.144473, -116.847143 45.02398, -116.830713 44.930872, -116.934774 44.782995, -117.038836 44.750133, -117.241483 44.394132, -117.170283 44.257209, -116.97859 44.240778, -116.896436 44.158624, -117.027882 43.830007, -117.027882 42.000709, -118.698349 41.989755, -120.001861 41.995232, -121.037003 41.995232, -122.378853 42.011663, -123.233256 42.006186, -124.213628 42.000709, -124.356029 42.115725, -124.432706 42.438865, -124.416275 42.663419, -124.553198 42.838681, -124.454613 43.002989, -124.383413 43.271359, -124.235536 43.55616, -124.169813 43.8081, -124.060274 44.657025, -124.076705 44.772041, -123.97812 45.144473, -123.939781 45.659305, -123.994551 45.944106, -123.945258 46.113892, -123.545441 46.261769, -123.370179 46.146753, -123.211348 46.174138)) -PA POLYGON ((-79.76278 42.252649, -79.76278 42.000709, -75.35932 42.000709, -75.249781 41.863786, -75.173104 41.869263, -75.052611 41.754247, -75.074519 41.60637, -74.89378 41.436584, -74.740426 41.431108, -74.69661 41.359907, -74.828057 41.288707, -74.882826 41.179168, -75.134765 40.971045, -75.052611 40.866983, -75.205966 40.691721, -75.195012 40.576705, -75.069042 40.543843, -75.058088 40.417874, -74.773287 40.215227, -74.82258 40.127596, -75.129289 39.963288, -75.145719 39.88661, -75.414089 39.804456, -75.616736 39.831841, -75.786521 39.722302, -79.477979 39.722302, -80.518598 39.722302, -80.518598 40.636951, -80.518598 41.978802, -80.518598 41.978802, -80.332382 42.033571, -79.76278 42.269079, -79.76278 42.252649)) +PA POLYGON ((-79.76278 42.252649, -79.76278 42.000709, -75.35932 42.000709, -75.249781 41.863786, -75.173104 41.869263, -75.052611 41.754247, -75.074519 41.60637, -74.89378 41.436584, -74.740426 41.431108, -74.69661 41.359907, -74.828057 41.288707, -74.882826 41.179168, -75.134765 40.971045, -75.052611 40.866983, -75.205966 40.691721, -75.195012 40.576705, -75.069042 40.543843, -75.058088 40.417874, -74.773287 40.215227, -74.82258 40.127596, -75.129289 39.963288, -75.145719 39.88661, -75.414089 39.804456, -75.616736 39.831841, -75.786521 39.722302, -79.477979 39.722302, -80.518598 39.722302, -80.518598 40.636951, -80.518598 41.978802, -80.332382 42.033571, -79.76278 42.269079, -79.76278 42.252649)) RI MULTIPOLYGON (((-71.196845 41.67757, -71.120168 41.496831, -71.317338 41.474923, -71.196845 41.67757)), ((-71.530939 42.01714, -71.383061 42.01714, -71.328292 41.781632, -71.22423 41.710431, -71.344723 41.726862, -71.448785 41.578985, -71.481646 41.370861, -71.859555 41.321569, -71.799309 41.414677, -71.799309 42.006186, -71.530939 42.01714))) SC POLYGON ((-82.764143 35.066903, -82.550543 35.160011, -82.276696 35.198349, -81.044384 35.149057, -81.038907 35.044995, -80.934845 35.105241, -80.781491 34.935456, -80.797922 34.820441, -79.675149 34.80401, -78.541422 33.851022, -78.716684 33.80173, -78.935762 33.637421, -79.149363 33.380005, -79.187701 33.171881, -79.357487 33.007573, -79.582041 33.007573, -79.631334 32.887081, -79.866842 32.755634, -79.998289 32.613234, -80.206412 32.552987, -80.430967 32.399633, -80.452875 32.328433, -80.660998 32.246279, -80.885553 32.032678, -81.115584 32.120309, -81.121061 32.290094, -81.279893 32.558464, -81.416816 32.629664, -81.42777 32.843265, -81.493493 33.007573, -81.761863 33.160928, -81.937125 33.347144, -81.926172 33.462159, -82.194542 33.631944, -82.325988 33.81816, -82.55602 33.94413, -82.714851 34.152254, -82.747713 34.26727, -82.901067 34.486347, -83.005129 34.469916, -83.339222 34.683517, -83.322791 34.787579, -83.109191 35.00118, -82.764143 35.066903)) SD POLYGON ((-104.047534 45.944106, -96.560556 45.933153, -96.582464 45.818137, -96.856311 45.604536, -96.681049 45.412843, -96.451017 45.297827, -96.451017 43.501391, -96.582464 43.479483, -96.527695 43.397329, -96.560556 43.222067, -96.434587 43.123482, -96.511264 43.052282, -96.544125 42.855112, -96.631756 42.707235, -96.44554 42.488157, -96.626279 42.515542, -96.692003 42.657942, -97.217789 42.844158, -97.688806 42.844158, -97.831206 42.866066, -97.951699 42.767481, -98.466531 42.94822, -98.499393 42.997512, -101.626726 42.997512, -103.324578 43.002989, -104.053011 43.002989, -104.058488 44.996596, -104.042057 44.996596, -104.047534 45.944106))