Repository: sunpy/sunpy

Issue:
Add Map support for Quadrangle
Currently, the `Map` method `draw_rectangle()` can be used to overlay rectangles, which internally uses `matplotlib.patches.Rectangle`.  `astropy.visualization.wcsaxes.Quadrangle` is better for nearly all situations because it will contort with transformations, but it is only available starting with Astropy 4.2 (when released).  Possible approaches to adding support for `Quadrangle`:

- Add a `draw_quadrangle()` method.  If the user's Astropy version is <4.2, it can raise an error, or it can raise a warning and fallback to `draw_rectangle()`.
- Switch the internals of `draw_rectangle()` to use `Quadrangle` if its available.
- Defer any action until our minimum Astropy dependency is 4.2, which would nominally be in a year with the release of SunPy 3.1.
- Defer any action since there are mixed opinions on having the `draw_*` methods where they currently live in the API.



Discussion:
Is this issue still open to work on? I've got a history of choosing wrong issues to work on so just checking beforehand xD
> Is this issue still open to work on? I've got a history of choosing wrong issues to work on so just checking beforehand xD

This issue is open to work on but only do so if you understand what the issue is asking for. This is not labeled as a good first issue for new people. 
Alright, so add another draw_quardrangle method which would replace draw_rectangle with all functionality, raise a warning and fall back to draw_rectangle if the user's astropy version is < 4.2. Correct me if I'm wrong
> Alright, so add another draw_quardrangle method which would replace draw_rectangle with all functionality, raise a warning and fall back to draw_rectangle if the user's astropy version is < 4.2. Correct me if I'm wrong

Sounds correct. 
Alright, I'll begin working on this and open a PR soon 
by making a new method, `draw_quadrangle` the initial setup of the coordinates are the same as `draw_rectangle` so I was wondering if replacing the method completely and handling the situation where astropy < 4.2 is taken care of in the method was a good idea?
Or do they have to be separate methods because a few lines of code is repeated using Quadrangle from astropy.
We can't change the API between releases without prior warnings. I would create the new method and then on the PR we can discuss if we want to replace draw_rectange with it. 
Makes sense, I'll open PR with a separate method.

Resolving patch:
diff --git a/examples/plotting/quadrangle.py b/examples/plotting/quadrangle.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/quadrangle.py
@@ -0,0 +1,45 @@
+"""
+=======================================
+Drawing a latitude-longitude quadrangle
+=======================================
+
+How to draw a latitude-longitude quadrangle on a map
+"""
+import matplotlib.pyplot as plt
+
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+
+import sunpy.map
+from sunpy.coordinates import HeliographicStonyhurst
+from sunpy.data.sample import AIA_171_IMAGE
+
+###############################################################################
+# The purpose of this example is to demonstrate how to draw a quadrangle on a
+# map using :meth:`~sunpy.map.GenericMap.draw_quadrangle`.  A quadrangle has
+# edges aligned with lines of constant latitude and longitude in some
+# coordinate system.  We start with the sample AIA image.
+aia = sunpy.map.Map(AIA_171_IMAGE)
+
+###############################################################################
+# Now let's define the bottom-left corner of the quadrangle.  Note that we
+# define it in a different coordinate frame (heliographic Stonyhurst) than the
+# coordinate frame of the map (which is helioprojective Cartesian).
+bottom_left = SkyCoord(30*u.deg, -10*u.deg,
+                       frame=HeliographicStonyhurst, obstime=aia.date)
+
+
+###############################################################################
+# Now let's draw a quadrangle on the map, with a width of 20 degrees and a
+# height of 60 degrees.  Because the coordinate frame of ``bottom_left`` is
+# in `~sunpy.coordinates.frames.HeliographicStonyhurst`, the width and height
+# correspond to the longitude and latitude directions, respectively, in that
+# coordinate frame.
+fig = plt.figure()
+ax = plt.subplot(projection=aia)
+aia.plot(clip_interval=(1, 99.99)*u.percent)
+aia.draw_grid()
+aia.draw_quadrangle(bottom_left, width=20*u.deg, height=60*u.deg,
+                    edgecolor='blue', linewidth=2)
+ax.plot_coord(bottom_left, 'x', color='red')
+plt.show()
diff --git a/sunpy/map/mapbase.py b/sunpy/map/mapbase.py
--- a/sunpy/map/mapbase.py
+++ b/sunpy/map/mapbase.py
@@ -41,6 +41,12 @@
 from sunpy.util.functools import seconddispatch
 from sunpy.visualization import axis_labels_from_ctype, peek_show, wcsaxes_compat
 
+# Quadrangle is not present in Astropy<4.2, so we fall back to a vendored version
+try:
+    from astropy.visualization.wcsaxes import Quadrangle
+except ImportError:
+    from sunpy.visualization._quadrangle import Quadrangle
+
 TIME_FORMAT = config.get("general", "time_format")
 PixelPair = namedtuple('PixelPair', 'x y')
 SpatialPair = namedtuple('SpatialPair', 'axis1 axis2')
@@ -1949,6 +1955,72 @@ def draw_limb(self, axes=None, **kwargs):
 
         return [circ]
 
+    @u.quantity_input
+    def draw_quadrangle(self, bottom_left, *, width: u.deg = None, height: u.deg = None,
+                        axes=None, top_right=None, **kwargs):
+        """
+        Draw a quadrangle defined in world coordinates on the plot using Astropy's
+        `~astropy.visualization.wcsaxes.patches.Quadrangle`.
+
+        This draws a quadrangle that has corners at ``(bottom_left, top_right)``,
+        and has sides aligned with the coordinate axes of the frame of ``bottom_left``,
+        which may be different from the coordinate axes of the map.
+
+        If ``width`` and ``height`` are specified, they are respectively added to the
+        longitude and latitude of the ``bottom_left`` coordinate to calculate a
+        ``top_right`` coordinate.
+
+        Parameters
+        ----------
+        bottom_left : `~astropy.coordinates.SkyCoord`
+            The bottom-left coordinate of the quadrangle. It can
+            have shape ``(2,)`` to simultaneously define ``top_right``.
+        top_right : `~astropy.coordinates.SkyCoord`
+            The top-right coordinate of the quadrangle.
+        width : `astropy.units.Quantity`, optional
+            The width of the quadrangle. Required if ``top_right`` is omitted.
+        height : `astropy.units.Quantity`
+            The height of the quadrangle. Required if ``top_right`` is omitted.
+        axes : `matplotlib.axes.Axes`
+            The axes on which to plot the quadrangle. Defaults to the current
+            axes.
+
+        Returns
+        -------
+        quad : `~astropy.visualization.wcsaxes.patches.Quadrangle`
+            The added patch
+
+        Notes
+        -----
+        Extra keyword arguments to this function are passed through to the
+        `~astropy.visualization.wcsaxes.patches.Quadrangle` instance.
+
+        Examples
+        --------
+        .. minigallery:: sunpy.map.GenericMap.draw_quadrangle
+        """
+        bottom_left, top_right = get_rectangle_coordinates(
+            bottom_left, top_right=top_right, width=width, height=height)
+
+        width = Longitude(top_right.spherical.lon - bottom_left.spherical.lon)
+        height = top_right.spherical.lat - bottom_left.spherical.lat
+
+        if not axes:
+            axes = plt.gca()
+        if not wcsaxes_compat.is_wcsaxes(axes):
+            raise TypeError("The axes need to be an instance of WCSAxes. You may have neglected "
+                            "to use `projection=` when creating the axes.")
+
+        kwergs = {
+            "transform": axes.get_transform(bottom_left.frame.replicate_without_data()),
+            "edgecolor": "white",
+            "fill": False,
+        }
+        kwergs.update(kwargs)
+        quad = Quadrangle(self._get_lon_lat(bottom_left), width, height, **kwergs)
+        axes.add_artist(quad)
+        return quad
+
     @u.quantity_input
     def draw_rectangle(self, bottom_left, *, width: u.deg = None, height: u.deg = None,
                        axes=None, top_right=None, **kwargs):
diff --git a/sunpy/visualization/_quadrangle.py b/sunpy/visualization/_quadrangle.py
new file mode 100644
--- /dev/null
+++ b/sunpy/visualization/_quadrangle.py
@@ -0,0 +1,71 @@
+# Vendored from astropy.visualization.wcsaxes.patches
+# Licensed under a 3-clause BSD style license - see Astropy's LICENSE.rst
+#
+# This file can be removed when our minimum Astropy dependency is >= 4.2
+
+import numpy as np
+from matplotlib.patches import Polygon
+
+from astropy import units as u
+
+__all__ = ['Quadrangle']
+
+
+class Quadrangle(Polygon):
+    """
+    Create a patch representing a latitude-longitude quadrangle.
+    The edges of the quadrangle lie on two lines of constant longitude and two
+    lines of constant latitude (or the equivalent component names in the
+    coordinate frame of interest, such as right ascension and declination).
+    Note that lines of constant latitude are not great circles.
+    Unlike `matplotlib.patches.Rectangle`, the edges of this patch will render
+    as curved lines if appropriate for the WCS transformation.
+    Parameters
+    ----------
+    anchor : tuple or `~astropy.units.Quantity`
+        This can be either a tuple of two `~astropy.units.Quantity` objects, or
+        a single `~astropy.units.Quantity` array with two elements.
+    width : `~astropy.units.Quantity`
+        The width of the quadrangle in longitude (or, e.g., right ascension)
+    height : `~astropy.units.Quantity`
+        The height of the quadrangle in latitude (or, e.g., declination)
+    resolution : int, optional
+        The number of points that make up each side of the quadrangle -
+        increase this to get a smoother quadrangle.
+    vertex_unit : `~astropy.units.Unit`
+        The units in which the resulting polygon should be defined - this
+        should match the unit that the transformation (e.g. the WCS
+        transformation) expects as input.
+    Notes
+    -----
+    Additional keyword arguments are passed to `~matplotlib.patches.Polygon`
+    """
+
+    def __init__(self, anchor, width, height, resolution=100, vertex_unit=u.degree, **kwargs):
+
+        # Extract longitude/latitude, either from a tuple of two quantities, or
+        # a single 2-element Quantity.
+        longitude, latitude = u.Quantity(anchor).to_value(vertex_unit)
+
+        # Convert the quadrangle dimensions to the appropriate units
+        width = width.to_value(vertex_unit)
+        height = height.to_value(vertex_unit)
+
+        # Create progressions in longitude and latitude
+        lon_seq = longitude + np.linspace(0, width, resolution + 1)
+        lat_seq = latitude + np.linspace(0, height, resolution + 1)
+
+        # Trace the path of the quadrangle
+        lon = np.concatenate([lon_seq[:-1],
+                              np.repeat(lon_seq[-1], resolution),
+                              np.flip(lon_seq[1:]),
+                              np.repeat(lon_seq[0], resolution)])
+        lat = np.concatenate([np.repeat(lat_seq[0], resolution),
+                              lat_seq[:-1],
+                              np.repeat(lat_seq[-1], resolution),
+                              np.flip(lat_seq[1:])])
+
+        # Create polygon vertices
+        vertices = np.array([lon, lat]).transpose()
+
+        super().__init__(vertices, **kwargs)


Test patch:
diff --git a/sunpy/map/tests/test_plotting.py b/sunpy/map/tests/test_plotting.py
--- a/sunpy/map/tests/test_plotting.py
+++ b/sunpy/map/tests/test_plotting.py
@@ -7,6 +7,7 @@
 import matplotlib.pyplot as plt
 import numpy as np
 import pytest
+from matplotlib.figure import Figure
 
 import astropy.units as u
 from astropy.coordinates import SkyCoord
@@ -15,6 +16,7 @@
 import sunpy.coordinates
 import sunpy.data.test
 import sunpy.map
+from sunpy.coordinates import HeliographicStonyhurst
 from sunpy.tests.helpers import figure_test, fix_map_wcs
 from sunpy.util.exceptions import SunpyUserWarning
 
@@ -137,6 +139,26 @@ def test_rectangle_aia171_hgs_width_height(aia171_test_map):
     aia171_test_map.draw_rectangle(bottom_left, width=w, height=h)
 
 
+@figure_test
+def test_quadrangle_aia17_width_height(aia171_test_map):
+    aia171_test_map.plot()
+    bottom_left = SkyCoord(
+        50 * u.deg, -10 * u.deg, frame=HeliographicStonyhurst, obstime=aia171_test_map.date)
+    w = 30 * u.deg
+    h = 90 * u.deg
+    aia171_test_map.draw_quadrangle(bottom_left=bottom_left, width=w, height=h)
+
+
+@figure_test
+def test_quadrangle_aia17_top_right(aia171_test_map):
+    aia171_test_map.plot()
+    bottom_left = SkyCoord(
+        50 * u.deg, -10 * u.deg, frame=HeliographicStonyhurst, obstime=aia171_test_map.date)
+    top_right = SkyCoord(
+        65 * u.deg, 50 * u.deg, frame=HeliographicStonyhurst, obstime=aia171_test_map.date)
+    aia171_test_map.draw_quadrangle(bottom_left, top_right=top_right)
+
+
 @figure_test
 def test_plot_masked_aia171(aia171_test_map_with_mask):
     aia171_test_map_with_mask.plot()
@@ -206,6 +228,26 @@ def test_heliographic_rectangle_top_right(heliographic_test_map):
     heliographic_test_map.draw_rectangle(bottom_left, top_right=top_right, edgecolor='cyan')
 
 
+@figure_test
+def test_heliographic_quadrangle_width_height(heliographic_test_map):
+    heliographic_test_map.plot()
+    bottom_left = SkyCoord(
+        60 * u.deg, 50 * u.deg, frame=heliographic_test_map.coordinate_frame)
+    w = 13 * u.deg
+    h = 13 * u.deg
+    heliographic_test_map.draw_quadrangle(bottom_left, width=w, height=h, edgecolor='cyan')
+
+
+@figure_test
+def test_heliographic_quadrangle_top_right(heliographic_test_map):
+    heliographic_test_map.plot()
+    bottom_left = SkyCoord(
+        60 * u.deg, 50 * u.deg, frame=heliographic_test_map.coordinate_frame)
+    top_right = SkyCoord(
+        80 * u.deg, 90 * u.deg, frame=heliographic_test_map.coordinate_frame)
+    heliographic_test_map.draw_quadrangle(bottom_left, top_right=top_right, edgecolor='cyan')
+
+
 # See https://github.com/sunpy/sunpy/issues/4294 to track this warning. Ideally
 # it should not be filtered, and the cause of it fixed.
 @pytest.mark.filterwarnings(r'ignore:Numpy has detected that you \(may be\) writing to an array with\noverlapping memory')
@@ -222,3 +264,12 @@ def test_plot_norm_error(aia171_test_map):
         aia171_test_map.plot(norm=norm, vmin=0)
     with pytest.raises(ValueError, match='Cannot manually specify vmax'):
         aia171_test_map.plot(norm=norm, vmax=0)
+
+
+def test_quadrangle_no_wcsaxes(aia171_test_map):
+    ax = Figure().add_subplot(projection=None)  # create a non-WCSAxes plot
+
+    bottom_left = SkyCoord(
+        [0, 1] * u.arcsec, [0, 1] * u.arcsec, frame=aia171_test_map.coordinate_frame)
+    with pytest.raises(TypeError, match='WCSAxes'):
+        aia171_test_map.draw_quadrangle(bottom_left, axes=ax)
diff --git a/sunpy/tests/figure_hashes_mpl_332_ft_261_astropy_403.json b/sunpy/tests/figure_hashes_mpl_332_ft_261_astropy_403.json
--- a/sunpy/tests/figure_hashes_mpl_332_ft_261_astropy_403.json
+++ b/sunpy/tests/figure_hashes_mpl_332_ft_261_astropy_403.json
@@ -16,6 +16,8 @@
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_width_height": "455246d338564add5be94c1dc0ff5740a90d21d8ef7da9f392ba3a01237ef5b8",
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_top_right": "455246d338564add5be94c1dc0ff5740a90d21d8ef7da9f392ba3a01237ef5b8",
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_hgs_width_height": "53cdb641cb2de3223dc1c0fa7e22857ed9ff62d71d27b08186b98d19958f1af6",
+  "sunpy.map.tests.test_plotting.test_quadrangle_aia17_width_height": "f7df3ec501d9dfc7d4ad1e29d8e6f774385e0a4328e04895de45099a52c19452",
+  "sunpy.map.tests.test_plotting.test_quadrangle_aia17_top_right": "6efa26f67f3d1797f8bc26234377f3cea7dc83b44143a2b93b1b5baf82617148",
   "sunpy.map.tests.test_plotting.test_plot_masked_aia171": "2dff076c0eaaf5cf5849ad541b434bfbda00d99491db51eb35d2ab0ee729bbb2",
   "sunpy.map.tests.test_plotting.test_plot_masked_aia171_nowcsaxes": "69e28235fa42379a94f7da6cc39b375f361d1e26dc0f94e6b8c5f05894cc64c7",
   "sunpy.map.tests.test_plotting.test_plot_aia171_superpixel": "b147682850aad35e4c414de836c5880dcdba9f9e9fed13e53854b6685dffb39f",
@@ -26,6 +28,8 @@
   "sunpy.map.tests.test_plotting.test_heliographic_peek": "d8f4042d62f7ec00f1c9ddcd477a1c0133e87953864bf40fc676726d1da2909d",
   "sunpy.map.tests.test_plotting.test_heliographic_rectangle_width_height": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
   "sunpy.map.tests.test_plotting.test_heliographic_rectangle_top_right": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
+  "sunpy.map.tests.test_plotting.test_heliographic_quadrangle_width_height": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
+  "sunpy.map.tests.test_plotting.test_heliographic_quadrangle_top_right": "b4d2229be76ed1fdcfa5740aaeb26ab6eed92608661cf2f34bca3685f524caf1",
   "sunpy.map.tests.test_plotting.test_heliographic_grid_annotations": "eae3267f21837571a7b10bf1e2fe0f672cf59c11ebefa4e5eadc3e9906609be3",
   "sunpy.timeseries.tests.test_timeseriesbase.test_eve_peek": "2a22f93af44971411800e4fcd0d93d2128fe2b9e6b97e576e8372c2cd12d7d78",
   "sunpy.timeseries.tests.test_timeseriesbase.test_esp_peek": "d069eee419c45875cffe9a10c3262f83b3d28ccf7f13ee66ca1b99fe557f09b8",
diff --git a/sunpy/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json b/sunpy/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json
--- a/sunpy/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json
+++ b/sunpy/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json
@@ -16,6 +16,8 @@
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_width_height": "455246d338564add5be94c1dc0ff5740a90d21d8ef7da9f392ba3a01237ef5b8",
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_top_right": "455246d338564add5be94c1dc0ff5740a90d21d8ef7da9f392ba3a01237ef5b8",
   "sunpy.map.tests.test_plotting.test_rectangle_aia171_hgs_width_height": "53cdb641cb2de3223dc1c0fa7e22857ed9ff62d71d27b08186b98d19958f1af6",
+  "sunpy.map.tests.test_plotting.test_quadrangle_aia17_width_height": "f7df3ec501d9dfc7d4ad1e29d8e6f774385e0a4328e04895de45099a52c19452",
+  "sunpy.map.tests.test_plotting.test_quadrangle_aia17_top_right": "6efa26f67f3d1797f8bc26234377f3cea7dc83b44143a2b93b1b5baf82617148",
   "sunpy.map.tests.test_plotting.test_plot_masked_aia171": "2dff076c0eaaf5cf5849ad541b434bfbda00d99491db51eb35d2ab0ee729bbb2",
   "sunpy.map.tests.test_plotting.test_plot_masked_aia171_nowcsaxes": "69e28235fa42379a94f7da6cc39b375f361d1e26dc0f94e6b8c5f05894cc64c7",
   "sunpy.map.tests.test_plotting.test_plot_aia171_superpixel": "b147682850aad35e4c414de836c5880dcdba9f9e9fed13e53854b6685dffb39f",
@@ -26,6 +28,8 @@
   "sunpy.map.tests.test_plotting.test_heliographic_peek": "650f4e9ad9ae71e58e2c33f4843202f52edf9ea393c874e0b288de0788e16e5d",
   "sunpy.map.tests.test_plotting.test_heliographic_rectangle_width_height": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
   "sunpy.map.tests.test_plotting.test_heliographic_rectangle_top_right": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
+  "sunpy.map.tests.test_plotting.test_heliographic_quadrangle_width_height": "f1dc10238c83fbdf510d1f912c921fe3b53f437f9a46f3b71ad46b48273369fb",
+  "sunpy.map.tests.test_plotting.test_heliographic_quadrangle_top_right": "b4d2229be76ed1fdcfa5740aaeb26ab6eed92608661cf2f34bca3685f524caf1",
   "sunpy.map.tests.test_plotting.test_heliographic_grid_annotations": "eae3267f21837571a7b10bf1e2fe0f672cf59c11ebefa4e5eadc3e9906609be3",
   "sunpy.timeseries.tests.test_timeseriesbase.test_eve_peek": "2a22f93af44971411800e4fcd0d93d2128fe2b9e6b97e576e8372c2cd12d7d78",
   "sunpy.timeseries.tests.test_timeseriesbase.test_esp_peek": "d069eee419c45875cffe9a10c3262f83b3d28ccf7f13ee66ca1b99fe557f09b8",