Repository: SciTools/cartopy

Issue:
Factor out the ellipse generation code
Geostationary added an ellipse function which is defined in its constructor, factor this out and consider whether this is common functionality for the circular boundary type projections.



Discussion:

Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -642,6 +642,20 @@ class _CylindricalProjection(_RectangularProjection):
     """
 
 
+def _ellipse_boundary(semimajor=2, semiminor=1, easting=0, northing=0, n=201):
+    """
+    Defines a projection boundary using an ellipse.
+
+    This type of boundary is used by several projections.
+
+    """
+
+    t = np.linspace(0, 2 * np.pi, n)
+    coords = np.vstack([semimajor * np.cos(t), semiminor * np.sin(t)])
+    coords += ([easting], [northing])
+    return coords
+
+
 class PlateCarree(_CylindricalProjection):
     def __init__(self, central_longitude=0.0, globe=None):
         proj4_params = [('proj', 'eqc'), ('lon_0', central_longitude)]
@@ -1187,14 +1201,6 @@ def __init__(self, central_latitude=0.0, central_longitude=0.0,
             proj4_params.append(('lat_ts', true_scale_latitude))
         super(Stereographic, self).__init__(proj4_params, globe=globe)
 
-        # TODO: Factor this out, particularly if there are other places using
-        # it (currently: Stereographic & Geostationary). (#340)
-        def ellipse(semimajor=2, semiminor=1, easting=0, northing=0, n=200):
-            t = np.linspace(0, 2 * np.pi, n)
-            coords = np.vstack([semimajor * np.cos(t), semiminor * np.sin(t)])
-            coords += ([easting], [northing])
-            return coords
-
         # TODO: Let the globe return the semimajor axis always.
         a = np.float(self.globe.semimajor_axis or 6378137.0)
         b = np.float(self.globe.semiminor_axis or 6356752.3142)
@@ -1212,8 +1218,8 @@ def ellipse(semimajor=2, semiminor=1, easting=0, northing=0, n=200):
             point = sgeom.Point(false_easting, false_northing)
             self._boundary = point.buffer(self._x_limits[1]).exterior
         else:
-            coords = ellipse(self._x_limits[1], self._y_limits[1],
-                             false_easting, false_northing, 90)
+            coords = _ellipse_boundary(self._x_limits[1], self._y_limits[1],
+                                       false_easting, false_northing, 91)
             coords = tuple(tuple(pair) for pair in coords.T)
             self._boundary = sgeom.polygon.LinearRing(coords)
         self._threshold = np.diff(self._x_limits)[0] * 1e-3
@@ -1491,23 +1497,15 @@ def __init__(self, central_longitude=0.0, satellite_height=35785831,
                         ('units', 'm')]
         super(Geostationary, self).__init__(proj4_params, globe=globe)
 
-        # TODO: Factor this out, particularly if there are other places using
-        # it (currently: Stereographic & Geostationary). (#340)
-        def ellipse(semimajor=2, semiminor=1, easting=0, northing=0, n=200):
-            t = np.linspace(0, 2 * np.pi, n)
-            coords = np.vstack([semimajor * np.cos(t), semiminor * np.sin(t)])
-            coords += ([easting], [northing])
-            return coords
-
         # TODO: Let the globe return the semimajor axis always.
         a = np.float(self.globe.semimajor_axis or 6378137.0)
-        b = np.float(self.globe.semiminor_axis or 6378137.0)
+        b = np.float(self.globe.semiminor_axis or a)
         h = np.float(satellite_height)
         max_x = h * math.atan(a / (a + h))
         max_y = h * math.atan(b / (b + h))
 
-        coords = ellipse(max_x, max_y,
-                         false_easting, false_northing, 60)
+        coords = _ellipse_boundary(max_x, max_y,
+                                   false_easting, false_northing, 61)
         coords = tuple(tuple(pair) for pair in coords.T)
         self._boundary = sgeom.polygon.LinearRing(coords)
         self._xlim = self._boundary.bounds[::2]
@@ -1640,11 +1638,23 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0,
                         ('lat_0', central_latitude),
                         ('x_0', false_easting), ('y_0', false_northing)]
         super(AzimuthalEquidistant, self).__init__(proj4_params, globe=globe)
-        self._max = 2e7
+
+        # TODO: Let the globe return the semimajor axis always.
+        a = np.float(self.globe.semimajor_axis or 6378137.0)
+        b = np.float(self.globe.semiminor_axis or a)
+
+        coords = _ellipse_boundary(a * np.pi, b * np.pi,
+                                   false_easting, false_northing, 61)
+        coords = tuple(tuple(pair) for pair in coords.T)
+
+        self._boundary = sgeom.polygon.LinearRing(coords)
+        bounds = self._boundary.bounds
+        self._x_limits = bounds[0], bounds[2]
+        self._y_limits = bounds[1], bounds[3]
 
     @property
     def boundary(self):
-        return sgeom.Point(0, 0).buffer(self._max).exterior
+        return self._boundary
 
     @property
     def threshold(self):
@@ -1652,11 +1662,11 @@ def threshold(self):
 
     @property
     def x_limits(self):
-        return (-self._max, self._max)
+        return self._x_limits
 
     @property
     def y_limits(self):
-        return (-self._max, self._max)
+        return self._y_limits
 
 
 class _BoundaryPoint(object):


Test patch:
diff --git a/lib/cartopy/tests/crs/test_azimuthal_equidistant.py b/lib/cartopy/tests/crs/test_azimuthal_equidistant.py
--- a/lib/cartopy/tests/crs/test_azimuthal_equidistant.py
+++ b/lib/cartopy/tests/crs/test_azimuthal_equidistant.py
@@ -34,9 +34,9 @@ def test_default(self):
         assert_equal(aeqd.proj4_init, expected)
 
         assert_almost_equal(np.array(aeqd.x_limits),
-                            [-2e7, 2e7], decimal=4)
+                            [-20037508.34278924, 20037508.34278924], decimal=6)
         assert_almost_equal(np.array(aeqd.y_limits),
-                            [-2e7, 2e7], decimal=4)
+                            [-20037508.34278924, 20037508.34278924], decimal=6)
 
     def test_eccentric_globe(self):
         globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500,
@@ -46,6 +46,11 @@ def test_eccentric_globe(self):
                     '+x_0=0.0 +y_0=0.0 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-3141.59265359, 3141.59265359], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            [-1570.796326795, 1570.796326795], decimal=6)
+
     def test_eastings(self):
         aeqd_offset = ccrs.AzimuthalEquidistant(false_easting=1234,
                                                 false_northing=-4321)
@@ -54,6 +59,11 @@ def test_eastings(self):
                     '+x_0=1234 +y_0=-4321 +no_defs')
         assert_equal(aeqd_offset.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd_offset.x_limits),
+                            [-20036274.34278924, 20038742.34278924], decimal=6)
+        assert_almost_equal(np.array(aeqd_offset.y_limits),
+                            [-20041829.34278924, 20033187.34278924], decimal=6)
+
     def test_grid(self):
         # USGS Professional Paper 1395, pp 196--197, Table 30
         globe = ccrs.Globe(ellipse=None,
@@ -67,6 +77,11 @@ def test_grid(self):
                     '+x_0=0.0 +y_0=0.0 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-3.14159265, 3.14159265], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            [-3.14159265, 3.14159265], decimal=6)
+
         lats, lons = np.mgrid[0:100:10, 0:100:10]
         result = aeqd.transform_points(geodetic, lons.ravel(), lats.ravel())
 
@@ -131,6 +146,11 @@ def test_sphere_transform(self):
                     '+x_0=0.0 +y_0=0.0 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-9.42477796, 9.42477796], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            [-9.42477796, 9.42477796], decimal=6)
+
         result = aeqd.transform_point(100.0, -20.0, geodetic)
 
         assert_array_almost_equal(result, [-5.8311398, 5.5444634])
@@ -148,6 +168,14 @@ def test_ellipsoid_polar_transform(self):
                     '+lon_0=-100.0 +lat_0=90.0 +x_0=0.0 +y_0=0.0 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-20038296.88254529, 20038296.88254529], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            # TODO: This is wrong. Globe.semiminor_axis does
+                            # not account for flattening.
+                            # [-19970827.86969727, 19970827.86969727]
+                            [-20038296.88254529, 20038296.88254529], decimal=6)
+
         result = aeqd.transform_point(5.0, 80.0, geodetic)
 
         assert_array_almost_equal(result, [1078828.3, 289071.2], decimal=1)
@@ -170,6 +198,14 @@ def test_ellipsoid_guam_transform(self):
                     '+x_0=50000.0 +y_0=50000.0 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-19987726.36931940, 20087726.36931940], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            # TODO: This is wrong. Globe.semiminor_axis does
+                            # not account for flattening.
+                            # [-19919796.94787477, 20019796.94787477]
+                            [-19987726.36931940, 20087726.36931940], decimal=6)
+
         pt_lat = 13 + (20 + 20.53846 / 60) / 60
         pt_lon = 144 + (38 + 7.19265 / 60) / 60
         result = aeqd.transform_point(pt_lon, pt_lat, geodetic)
@@ -196,6 +232,14 @@ def test_ellipsoid_micronesia_transform(self):
                     '+x_0=28657.52 +y_0=67199.99000000001 +no_defs')
         assert_equal(aeqd.proj4_init, expected)
 
+        assert_almost_equal(np.array(aeqd.x_limits),
+                            [-20009068.84931940, 20066383.88931940], decimal=6)
+        assert_almost_equal(np.array(aeqd.y_limits),
+                            # TODO: This is wrong. Globe.semiminor_axis does
+                            # not account for flattening.
+                            # [-19902596.95787477, 20036996.93787477]
+                            [-19970526.37931940, 20104926.35931940], decimal=6)
+
         pt_lat = 15 + (14 + 47.4930 / 60) / 60
         pt_lon = 145 + (47 + 34.9080 / 60) / 60
         result = aeqd.transform_point(pt_lon, pt_lat, geodetic)
diff --git a/lib/cartopy/tests/crs/test_geostationary.py b/lib/cartopy/tests/crs/test_geostationary.py
--- a/lib/cartopy/tests/crs/test_geostationary.py
+++ b/lib/cartopy/tests/crs/test_geostationary.py
@@ -15,7 +15,7 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with cartopy.  If not, see <http://www.gnu.org/licenses/>.
 """
-Tests for the Transverse Mercator projection, including OSGB and OSNI.
+Tests for the Geostationary projection.
 
 """
 
@@ -41,8 +41,8 @@ def test_default(self):
         self.check_proj4_params(geos, expected)
 
         assert_almost_equal(geos.boundary.bounds,
-                            (-5364970.19679699, -5370680.80015303,
-                             5372584.78443894, 5370680.80015303),
+                            (-5372584.78443894, -5372584.78443894,
+                             5372584.78443894, 5372584.78443894),
                             decimal=4)
 
     def test_eccentric_globe(self):
@@ -55,7 +55,7 @@ def test_eccentric_globe(self):
         self.check_proj4_params(geos, expected)
 
         assert_almost_equal(geos.boundary.bounds,
-                            (-8245.7306, -4531.3879, 8257.4339, 4531.3879),
+                            (-8257.4338, -4532.9943, 8257.4338, 4532.9943),
                             decimal=4)
 
     def test_eastings(self):
@@ -67,8 +67,8 @@ def test_eastings(self):
         self.check_proj4_params(geos, expected)
 
         assert_almost_equal(geos.boundary.bounds,
-                            (-364970.19679699, -5495680.80015303,
-                             10372584.78443894, 5245680.80015303),
+                            (-372584.78443894, -5497584.78443894,
+                             10372584.78443894, 5247584.78443894),
                             decimal=4)
 
 if __name__ == '__main__':