[Git][debian-gis-team/pyresample][master] 4 commits: New upstream version 1.21.0

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sat Sep 4 11:42:29 BST 2021



Antonio Valentino pushed to branch master at Debian GIS Project / pyresample


Commits:
9afeb6c4 by Antonio Valentino at 2021-09-04T10:13:31+00:00
New upstream version 1.21.0
- - - - -
621eab0e by Antonio Valentino at 2021-09-04T10:13:54+00:00
Update upstream source from tag 'upstream/1.21.0'

Update to upstream version '1.21.0'
with Debian dir 9542ba59913d827f1e4c1ab88e784fe413caf5a4
- - - - -
1e5fdc65 by Antonio Valentino at 2021-09-04T10:16:59+00:00
New upstream release

- - - - -
3afe6ff0 by Antonio Valentino at 2021-09-04T10:19:51+00:00
Update the copyright file

- - - - -


17 changed files:

- .readthedocs.yml
- CHANGELOG.md
- debian/changelog
- debian/copyright
- + docs/environment.yml
- docs/requirements.txt
- + docs/source/_static/images/2_passes_between_202001051137_and_202001051156.png
- docs/source/geo_def.rst
- docs/source/index.rst
- + docs/source/roadmap.rst
- + docs/source/spherical_geometry.rst
- pyresample/ewa/dask_ewa.py
- pyresample/spherical.py
- + pyresample/spherical_utils.py
- pyresample/test/test_spherical.py
- + pyresample/test/test_spherical_utils.py
- pyresample/version.py


Changes:

=====================================
.readthedocs.yml
=====================================
@@ -1,7 +1,10 @@
 version: 2
+
+conda:
+    environment: docs/environment.yml
+
 python:
+    version: 3.8
     install:
-        - requirements: docs/requirements.txt
         - method: pip
           path: .
-    system_packages: true


=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,22 @@
+## Version 1.21.0 (2021/08/19)
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 370](https://github.com/pytroll/pyresample/pull/370) - Fix dask ewa issues with newer versions of dask
+
+#### Features added
+
+* [PR 347](https://github.com/pytroll/pyresample/pull/347) - Add spherical geometry support for deriving total/common area coverage of several satellite overpasses
+
+#### Documentation changes
+
+* [PR 373](https://github.com/pytroll/pyresample/pull/373) - Add initial draft of a Roadmap page
+
+In this release 3 pull requests were closed.
+
+
 ## Version 1.20.0 (2021/06/04)
 
 ### Issues Closed


=====================================
debian/changelog
=====================================
@@ -1,4 +1,4 @@
-pyresample (1.20.0-1) UNRELEASED; urgency=medium
+pyresample (1.21.0-1) UNRELEASED; urgency=medium
 
   [ Bas Couwenberg ]
   * Bump Standards-Version to 4.5.1, no changes.


=====================================
debian/copyright
=====================================
@@ -9,7 +9,7 @@ Copyright:   2015-2021, Pyresample developers
              2013-2019, Leon Majewski <leon.majewski at bom.gov.au>
              2010-2016, Esben S. Nielsen <esn at dmi.dk>
              2010-2016, Thomas Lavergne <t.lavergne at met.no>
-       2010, 2014-2016, Adam Dybbroe
+ 2010, 2014-2016, 2021, Adam Dybbroe <Firstname.Lastname at smhi.se>
 License: LGPL-3+
 
 Files: versioneer.py


=====================================
docs/environment.yml
=====================================
@@ -0,0 +1,15 @@
+name: readthedocs
+dependencies:
+  - python
+  - xarray
+  - zarr
+  - dask
+  - toolz
+  - numpy
+  - cython
+  - cartopy
+  - pytest
+  - proj4
+  - pyproj
+  - shapely
+  - pykdtree


=====================================
docs/requirements.txt
=====================================
@@ -4,3 +4,7 @@ toolz
 dask
 zarr
 numpy
+cython>=0.29
+pyproj
+cartopy>=0.18
+shapely


=====================================
docs/source/_static/images/2_passes_between_202001051137_and_202001051156.png
=====================================
Binary files /dev/null and b/docs/source/_static/images/2_passes_between_202001051137_and_202001051156.png differ


=====================================
docs/source/geo_def.rst
=====================================
@@ -203,55 +203,3 @@ using the **projection_x_coord** or **projection_y_coords** property of a geogra
  ...                           width, height, area_extent)
  >>> proj_x_range = area_def.projection_x_coords
 
-Spherical geometry operations
------------------------------
-
-Some basic spherical operations are available for geometry definition objects. The
-spherical geometry operations are calculated based on the corners of a GeometryDefinition
-(:class:`~pyresample.geometry.GridDefinition`,
-:class:`~pyresample.geometry.AreaDefinition`, or a 2D
-:class:`~pyresample.geometry.SwathDefinition`) assuming the edges are great circle arcs.
-
-Geometries can be checked for overlap:
-
-.. doctest::
-
- >>> import numpy as np
- >>> area_id = 'ease_sh'
- >>> description = 'Antarctic EASE grid'
- >>> proj_id = 'ease_sh'
- >>> projection = '+proj=laea +lat_0=-90 +lon_0=0 +a=6371228.0 +units=m'
- >>> width = 425
- >>> height = 425
- >>> area_extent = (-5326849.0625,-5326849.0625,5326849.0625,5326849.0625)
- >>> area_def = AreaDefinition(area_id, description, proj_id, projection,
- ...                           width, height, area_extent)
- >>> lons = np.array([[-40, -11.1], [9.5, 19.4], [65.5, 47.5], [90.3, 72.3]])
- >>> lats = np.array([[-70.1, -58.3], [-78.8, -63.4], [-73, -57.6], [-59.5, -50]])
- >>> swath_def = SwathDefinition(lons, lats)
- >>> print(swath_def.overlaps(area_def))
- True
-
-The fraction of overlap can be calculated
-
-.. doctest::
-
- >>> overlap_fraction = swath_def.overlap_rate(area_def)
- >>> overlap_fraction = round(overlap_fraction, 10)
- >>> print(overlap_fraction)
- 0.0584395313
-
-And the polygon defining the (great circle) boundaries over the overlapping area can be calculated
-
-.. doctest::
-
- >>> overlap_polygon = swath_def.intersection(area_def)
- >>> print(overlap_polygon)
- [(-40.0, -70.1), (-11.1, -58.3), (72.3, -50.0), (90.3, -59.5)]
-
-It can be tested if a (lon, lat) point is inside a GeometryDefinition
-
-.. doctest::
-
- >>> print((0, -90) in area_def)
- True


=====================================
docs/source/index.rst
=====================================
@@ -43,8 +43,10 @@ Documentation
    swath
    multi
    preproc
+   spherical_geometry
    plot
    data_reduce
+   roadmap
    API <api/pyresample>
 
 


=====================================
docs/source/roadmap.rst
=====================================
@@ -0,0 +1,192 @@
+Roadmap
+=======
+
+Roadmap to 2.0!
+---------------
+
+Pyresample has had some real growing pains. These have become more and more
+apparent as Pytroll developers have worked on Satpy where we had more freedom
+to start fresh and create interfaces that worked for us. That development along
+with the Pyresample User Survey conducted in 2021 have guided us to a new design
+for Pyresample that we hope to release as version 2.0.
+
+Below are the various categories of components of Pyresample and how we see them
+existing. In most of the cases for existing interfaces in Pyresample, we expect
+things to be backwards compatible or in the extreme cases we want to add new
+interfaces alongside the existing ones for an easier transition to the new
+functionality. This will mean some deprecated interfaces so that we can make
+the user experience more consistent regardless of resampling algorithm or other
+use case.
+
+You can track the progress of Pyresample 2.0 by following the issues and pull
+requests on the
+`v2.0 milestone <https://github.com/pytroll/pyresample/milestone/3>`_ on
+GitHub.
+
+What is not planned for 2.0?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Vertical or other higher dimension resampling: This is not a simple problem
+  and with the other changes planned for 2.0 we won't be able to look into
+  this.
+* New resampling algorithms: The 2.0 version bump is not meant for new
+  algorithms. We'll keep these more for minor version releases. Version 2.0
+  is about larger breaking changes.
+
+Geometry Classes
+^^^^^^^^^^^^^^^^
+
+There are currently X types of user-facing geometry objects in Pyresample:
+
+* ~GridDefinition~
+* SwathDefinition
+* DynamicAreaDefinition
+* AreaDefinition
+* StackedAreaDefinition
+
+We've realized that the ``GridDefinition`` is actually a special case of an
+``AreaDefinition`` and therefore it will be deprecated. The other classes,
+as far as purpose, are still needed in some sense and will most likely not
+go anywhere. Most changes to these classes will be internal for code
+cleanliness.
+
+However, we'd like these classes to be easier to create and use. We'd like to
+focus on what is actually required to work with these objects with the rest of
+Pyresample. This means separating the "numbers" from the metadata.
+AreaDefinitions will no longer require a name or other descriptive information.
+You can provide them, but they won't be required.
+
+Going forward we'd like users to focus on using classmethod's to create these
+objects. We hope this will provide an easier connection from what information
+you have to a useable object. For example, instead of:
+
+.. code-block:: python
+
+    area = AreaDefinition(name, description, proj_id, projection, width, height, area_extent)
+
+You would do this in pyresample 2.0:
+
+.. code-block:: python
+
+    metadata = {"name": name, "description": description}  # optional
+    area = AreaDefinition.from_extent_shape(projection, area_extent, (height, width), metadata)
+
+You'll also be allowed to provide arbitrary metadata to swath definitions and
+the other geometry types.
+
+Resamplers
+^^^^^^^^^^
+
+Currently there are different interfaces for calling the different resampling
+options in Pyresample. Sometimes you call a function, sometimes you create a
+class and call a "resample" method on it, and sometimes if you want finer control
+you call multiple functions and have to pass things between them. In Pyresample
+2.0 we want to get things down to a few consistent interfaces all wrapped up
+into a series of Resampler classes.
+
+Creating Resamplers
+*******************
+
+You'll create resampler classes by doing:
+
+.. code-block:: python
+
+    from pyresample import create_resampler
+    resampler = create_resampler(src_geom, dst_geom, resampler='some-resampler-name')
+
+Pyresample 2.0 will maintain a "registry" of available resampler classes that
+you can refer to by name or get one by default based on the passed geometries.
+This registry of resamplers will also make it easier for users or third-party
+libraries to add their own resamplers.
+
+We hope with this basic creation process that we can have more control over
+what algorithms support what features and let the user know when something
+isn't allowed early on with clear error messages. For example, what
+combinations of geometry types are supported by the resampler or what types
+of arrays (xarray.DataArray, dask, or numpy) can be provided.
+
+Using Resamplers
+****************
+
+Once you have your resampler instance you can resample your data by doing:
+
+.. code-block:: python
+
+    new_data = resampler.resample(data, **kwargs)
+
+That's it. There are of course a lot of options hidden in the ``**kwargs``,
+but those will be specific to each algorithm. Our hope is that any
+optimizations or conversions that need to happen to get your data resampled
+can all be contained in these resampler objects and hopefully require less
+from the user.
+
+Alternatively to the ``.resample`` call, users can first call two methods:
+
+.. code-block:: python
+
+    resampler.precompute()
+    new_data = resampler.compute(data, **kwargs)
+
+This ``precompute`` method will perform any computations that can be done
+without needing the actual "image" data. You can then call ``.compute``
+to do the actual resampling. This separation is important when we start talking
+about caching (see below).
+
+Caching
+*******
+
+One major simplification we're hoping to achieve with Pyresample 2.0 is a
+defined set of caching functionality all encapsulated in "Cache" objects.
+These objects can be passed to ``create_resampler`` to enable the resampler
+to store intermediate computation results for reuse. How and where that storing
+is done is up to the specific cache object. It could be in-memory only, or
+to zarr datasets on local disk, or to some remote storage.
+
+By calling the ``.precompute`` method, the user will be able to pre-fill this
+cache without needing any image data. This will be useful for users using
+pyresample in operations where they may want to manually fill the cache before
+spawning realtime (time sensitive) processing.
+
+Indexes
+^^^^^^^
+
+From our survey we learned that a lot of users use the indexes returned by
+``get_neighbour_info`` for their own custom analyses. We recognize this need
+and while Cache objects could be written to get the same result, we think
+there is a better way. We plan to implement this functionality through a
+separate "Index" interface. Like Resamplers, these would provide you a way
+of relating a source geometry to a destination geometry. However, these
+objects would only be responsible for returning the index information.
+
+We haven't defined the interface for these yet, but hope that having something
+separate from resamplers will serve more people.
+
+Xarray and Geoxarray
+^^^^^^^^^^^^^^^^^^^^
+
+We would like to support pyresample users who use the xarray and dask libraries
+more. Behind the scenes over the last couple years we've added a lot of
+dask-based support to pyresample through the Satpy library. We've slowly moved
+that functionality over to Pyresample and the Resampler objects mentioned above
+are the first defined interface for that. However, there is still a lot of work
+to be done to completely take advantage of the parallel nature that dask arrays
+provide us.
+
+It should also be easier for users with data in xarray DataArray or Dataset
+objects to access pyresample functionality; even without knowing the
+metadata that pyresample will need to do some resampling (ex. CRS, extents,
+etc). Usually that type of information is held in the metadata of the xarray
+object already. New tools are in development to make this information easier
+to access; mainly
+`the Geoxarray project <https://geoxarray.github.io/latest/>`_.
+We will be working on Geoxarray and Pyresample to simplify common resampling
+tasks for xarray users.
+
+Documentation
+^^^^^^^^^^^^^
+
+The documentation for Pyresample is in need of a lot of love. As Pyresample
+has grown the documentation hasn't really been restructured to best present
+the new information it has taken on. We hope that as part of Pyresample 2.0
+we can clean out the cobwebs and make it easier to find the information you
+are looking for.
\ No newline at end of file


=====================================
docs/source/spherical_geometry.rst
=====================================
@@ -0,0 +1,106 @@
+Spherical Geometry Operations
+=============================
+
+Pyresample provides some basic support for various geometrical calculations applicable to
+a spherical earth. These spherical operations are available for geometry definition objects.
+This includes for instance finding the intersection of two
+great circles or finding the area of a spherical polygon given by a set of
+great circle arcs.
+
+The spherical geometry operations are calculated based on the corners of a GeometryDefinition
+(:class:`~pyresample.geometry.GridDefinition`,
+:class:`~pyresample.geometry.AreaDefinition`, or a 2D
+:class:`~pyresample.geometry.SwathDefinition`) assuming the edges are great circle arcs.
+
+Geometries can be checked for overlap:
+
+.. doctest::
+
+ >>> import numpy as np
+ >>> from pyresample.geometry import AreaDefinition
+ >>> from pyresample.geometry import SwathDefinition
+ >>> area_id = 'ease_sh'
+ >>> description = 'Antarctic EASE grid'
+ >>> proj_id = 'ease_sh'
+ >>> projection = '+proj=laea +lat_0=-90 +lon_0=0 +a=6371228.0 +units=m'
+ >>> width = 425
+ >>> height = 425
+ >>> area_extent = (-5326849.0625,-5326849.0625,5326849.0625,5326849.0625)
+ >>> area_def = AreaDefinition(area_id, description, proj_id, projection,
+ ...                           width, height, area_extent)
+ >>> lons = np.array([[-40, -11.1], [9.5, 19.4], [65.5, 47.5], [90.3, 72.3]])
+ >>> lats = np.array([[-70.1, -58.3], [-78.8, -63.4], [-73, -57.6], [-59.5, -50]])
+ >>> swath_def = SwathDefinition(lons, lats)
+ >>> print(swath_def.overlaps(area_def))
+ True
+
+The fraction of overlap can be calculated
+
+.. doctest::
+
+ >>> overlap_fraction = swath_def.overlap_rate(area_def)
+ >>> overlap_fraction = round(overlap_fraction, 10)
+ >>> print(overlap_fraction)
+ 0.0584395313
+
+And the polygon defining the (great circle) boundaries over the overlapping area can be calculated
+
+.. doctest::
+
+ >>> overlap_polygon = swath_def.intersection(area_def)
+ >>> print(overlap_polygon)
+ [(-40.0, -70.1), (-11.1, -58.3), (72.3, -50.0), (90.3, -59.5)]
+
+It can be tested if a (lon, lat) point is inside a GeometryDefinition
+
+.. doctest::
+
+ >>> print((0, -90) in area_def)
+ True
+
+
+Satellite swath coverage over area of interest
+----------------------------------------------
+
+With this support and the help of Cartopy_ it is for instance also possible to
+draw the outline of a satellite swath on the earth, and calculate the relative
+coverage by one or more swaths over an area of interest.
+
+Below is an example calculating how much of an area of interest is covered by
+two satellite overpasses. It operates on a list of `trollsched.satpass.Pass`
+satellite passes. See trollschedule_ how to generate a list of satellite overpasses.
+`area_def` is an :class:`~pyresample.geometry.AreaDefinition` object.
+
+ >>> from pyresample.spherical_utils import GetNonOverlapUnions
+ >>> from pyresample.boundary import AreaDefBoundary
+   
+ >>> area_boundary = AreaDefBoundary(area_def, frequency=100) # doctest: +SKIP 
+ >>> area_boundary = area_boundary.contour_poly # doctest: +SKIP 
+
+ >>> list_of_polygons = []
+ >>> for mypass in passes: # doctest: +SKIP 
+ >>>     list_of_polygons.append(mypass.boundary.contour_poly) # doctest: +SKIP 
+
+ >>> non_overlaps = GetNonOverlapUnions(list_of_polygons) # doctest: +SKIP
+ >>> non_overlaps.merge() # doctest: +SKIP 
+
+ >>> polygons = non_overlaps.get_polygons() # doctest: +SKIP 
+
+ >>> coverage = 0
+ >>> for polygon in polygons: # doctest: +SKIP 
+ >>>     isect = polygon.intersection(area_boundary) # doctest: +SKIP
+ >>>     if isect: # doctest: +SKIP 
+ >>>         coverage = coverage + isect.area() # doctest: +SKIP 
+
+ >>> area_cov = coverage / area_boundary.area() # doctest: +SKIP 
+ >>> print("Area coverage = {0}".format(area_cov)) # doctest: +SKIP
+ 0.889317815
+
+.. image:: _static/images/2_passes_between_202001051137_and_202001051156.png
+   
+In this case the relative area covered by the two passes (blue outlines) over
+the area of interest (red outlines) is 89%.
+
+           
+.. _Cartopy: http://scitools.org.uk/cartopy/
+.. _trollschedule: https://github.com/pytroll/pytroll-schedule.git


=====================================
pyresample/ewa/dask_ewa.py
=====================================
@@ -53,8 +53,12 @@ except ImportError:
 logger = logging.getLogger(__name__)
 
 
-def _call_ll2cr(lons, lats, target_geo_def):
+def _call_ll2cr(lons, lats, target_geo_def, computing_meta=False):
     """Wrap ll2cr() for handling dask delayed calls better."""
+    if computing_meta:
+        # produce a representative meta array in the best case
+        # avoids errors when we return our "empty" tuples below
+        return np.zeros((2, *lons.shape), dtype=lons.dtype)
     new_src = SwathDefinition(lons, lats)
     swath_points_in_grid, cols, rows = ll2cr(new_src, target_geo_def)
     if swath_points_in_grid == 0:
@@ -107,7 +111,7 @@ def _chunk_callable(x_chunk, axis, keepdims, **kwargs):
 
 def _combine_fornav(x_chunk, axis, keepdims, computing_meta=False,
                     maximum_weight_mode=False):
-    if isinstance(x_chunk, tuple) and len(x_chunk) == 2 and isinstance(x_chunk[0], tuple):
+    if computing_meta or _is_empty_chunk(x_chunk):
         # single "empty" chunk
         return x_chunk
     if not isinstance(x_chunk, list):
@@ -138,6 +142,10 @@ def _combine_fornav(x_chunk, axis, keepdims, computing_meta=False,
     return sum(weights), sum(accums)
 
 
+def _is_empty_chunk(x_chunk):
+    return isinstance(x_chunk, tuple) and len(x_chunk) == 2 and isinstance(x_chunk[0], tuple)
+
+
 def _average_fornav(x_chunk, axis, keepdims, computing_meta=False, dtype=None,
                     fill_value=None,
                     weight_sum_min=-1.0, maximum_weight_mode=False):


=====================================
pyresample/spherical.py
=====================================
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2013, 2014, 2015 Martin Raspaud
+# Copyright (c) 2013 - 2021 Pyresample developers
 
 # Author(s):
 
@@ -259,7 +259,6 @@ class Arc(object):
 
     def intersects(self, other_arc):
         """Check if the current arc and the *other_arc* intersect.
-
         An arc is defined as the shortest tracks between two points.
         """
 
@@ -407,7 +406,7 @@ class SphPolygon(object):
         By default, or when sign is 1, the union is perfomed. If sign is -1,
         the intersection of the polygons is returned.
 
-        The algorithm works this way: find an intersection between the two
+        The algorithm works this way: Find an intersection between the two
         polygons. If none can be found, then the two polygons are either not
         overlapping, or one is entirely included in the other. Otherwise,
         follow the edges of a polygon until another intersection is
@@ -476,7 +475,10 @@ class SphPolygon(object):
         return SphPolygon(np.array([(node.lon, node.lat) for node in nodes]), radius=self.radius)
 
     def union(self, other):
-        """Return the union of this and `other` polygon."""
+        """Return the union of this and `other` polygon.
+
+        NB! If the two polygons do not overlap (they have nothing in common) None is returned.
+        """
         return self._bool_oper(other, 1)
 
     def intersection(self, other):


=====================================
pyresample/spherical_utils.py
=====================================
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Pyresample developers
+
+# Author(s):
+
+#   Adam Dybbroe <Firstname.Lastname at smhi.se>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Functions to support the calculation of a coverage of an area by a set of spherical polygons.
+
+It can for instance be a set of satellite overpasses to be received of a given
+local stations over a certain time window where we want to calculate how much
+of an area is covered by the onboard scanning instrument(s).
+"""
+
+import itertools
+
+
+class GetNonOverlapUnionsBaseClass():
+    """Base class to get the smallest set of union objects that does not overlap.
+
+    The objects are here Python sets of integers - but are abstracts for
+    geometrical shapes on a sphere.
+
+    """
+
+    def __init__(self, geom_objects):
+        self.geometries = geom_objects
+        self.geom_numbers = range(len(geom_objects))
+
+        self._geoms = dict(enumerate(geom_objects))
+
+    def merge(self):
+        """Merge all overlapping objects (sets or polygons).
+
+        """
+        all_unions = self._merge_unions(self._geoms)
+        check_keys_int_or_tuple(all_unions)
+
+        self._geoms = {}
+        for key, value in all_unions.items():
+            if isinstance(key, int):
+                self._geoms[key] = value
+            else:
+                newkey = merge_tuples(key)
+                self._geoms[newkey] = value
+
+    def get_polygons(self):
+        """Get a list of all non-overlapping polygon unions."""
+        return list(self._geoms.values())
+
+    def get_ids(self):
+        """Get a list of identifiers identifying the gemoetry objects in each polygon union."""
+        return list(self._geoms.keys())
+
+    def _overlaps(self, set1, set2):
+        """Do the two sets overlap each other (have anything in common)?"""
+        if set1 != set1.difference(set2):
+            return True
+
+        return False
+
+    def _find_union_pair(self, geoms):
+        """From a set of geometries find a pair that overlaps.
+
+        *geoms* is here expected to be a numbered dict with SphPolygon
+        polygons. If no pair of two polygons overlap (that is the union
+        returns something different from None or False) then return None.
+
+        Strictly the geometries/objects does not need to be a SphPolygon. The
+        only requirement is that it has a union method with the same behaviour.
+
+        """
+
+        if len(geoms) == 1:
+            return None
+
+        for id_, komb_pair in zip(itertools.combinations(geoms.keys(), 2),
+                                  itertools.combinations(geoms.values(), 2)):
+            if self._overlaps(komb_pair[0], komb_pair[1]):
+                return id_, komb_pair[0].union(komb_pair[1])
+
+        return None
+
+    def _merge_unions(self, geoms):
+        """Merge all overlapping geometry unions.
+
+        Go through the dictionary of geometries and merge overlapping ones until
+        there is no change anymore. That is, the input dict of geometries is the
+        same as the output:
+
+        """
+        retv = self._find_union_pair(geoms)
+        if retv is None:
+            return geoms
+
+        cc_poly = geoms.copy()
+
+        for idx in [0, 1]:
+            del cc_poly[retv[0][idx]]
+        cc_poly[retv[0]] = retv[1]
+
+        return self._merge_unions(cc_poly)
+
+
+class GetNonOverlapUnions(GetNonOverlapUnionsBaseClass):
+    """NonOverlapUnions class.
+
+    """
+
+    def __init__(self, polygons):
+        """Init the GetNonOverlapUnions."""
+        super(GetNonOverlapUnions, self).__init__(polygons)
+
+    def _overlaps(self, polygon1, polygon2):
+        """Do two polygons overlap each other (have anything in common)?
+
+        Return True if they do overlap, otherwise False.
+
+        *polygon1* and *polygon2* are here expected to be SphPolygon
+        polygons. Strictly the two input objects do not need to be of type
+        SphPolygon. The only requirement is that they have a union method with
+        the same behaviour.
+
+        """
+        return check_if_two_polygons_overlap(polygon1, polygon2)
+
+
+def merge_tuples(atuple):
+    """Take a nested tuple of integers and concatenate it to a tuple of integers."""
+
+    if not isinstance(atuple, tuple):
+        raise TypeError("Function argument must be a tuple!")
+
+    while True:
+        # Test if all items in tuple are scalars and can be summed:
+        try:
+            _ = sum(atuple)
+        except TypeError:
+            pass
+        else:
+            return atuple
+
+        atuple = _int_items_to_tuples(atuple)
+
+        try:
+            atuple = sum(atuple, ())
+        except TypeError:
+            return atuple
+
+
+def _int_items_to_tuples(mytuple):
+    """Turn integer scalars in a tuple into tuples."""
+    newtup = []
+    for item in mytuple:
+        if isinstance(item, int):
+            newtup.append((item,))
+        else:
+            newtup.append(item)
+
+    return tuple(newtup)
+
+
+def check_keys_int_or_tuple(adict):
+    """Check if the dictionary keys are integers or tuples.
+
+    If they are not, raise a KeyError
+    """
+    for key in adict:
+        if not isinstance(key, (int, tuple)):
+            raise KeyError("Key must be integer or a tuple (of integers)")
+
+
+def check_if_two_polygons_overlap(polygon1, polygon2):
+    """Check if two SphPolygons overlaps."""
+    if polygon1.union(polygon2):
+        return True
+
+    return False


=====================================
pyresample/test/test_spherical.py
=====================================
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2013-2020 Martin Raspaud
+# Copyright (c) 2013-2021 Pyresample Developers
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -22,6 +22,88 @@ from pyresample.spherical import SphPolygon, Arc, SCoordinate, CCoordinate
 import unittest
 import numpy as np
 
+VERTICES_TEST_IS_INSIDE1 = np.array([[-1.54009253, 82.62402855],
+                                     [3.4804808, 82.8105746],
+                                     [20.7214892, 83.00875812],
+                                     [32.8857629, 82.7607758],
+                                     [41.53844302, 82.36024339],
+                                     [47.92062759, 81.91317164],
+                                     [52.82785062, 81.45769791],
+                                     [56.75107895, 81.00613046],
+                                     [59.99843787, 80.56042986],
+                                     [62.76998034, 80.11814453],
+                                     [65.20076209, 79.67471372],
+                                     [67.38577498, 79.22428],
+                                     [69.39480149, 78.75981318],
+                                     [71.28163984, 78.27283234],
+                                     [73.09016378, 77.75277976],
+                                     [74.85864685, 77.18594725],
+                                     [76.62327682, 76.55367303],
+                                     [78.42162204, 75.82918893],
+                                     [80.29698409, 74.97171721],
+                                     [82.30538638, 73.9143231],
+                                     [84.52973107, 72.53535661],
+                                     [87.11696138, 70.57600156],
+                                     [87.79163209, 69.98712409],
+                                     [72.98142447, 67.1760143],
+                                     [61.79517279, 63.2846272],
+                                     [53.50600609, 58.7098766],
+                                     [47.26725347, 53.70533139],
+                                     [42.44083259, 48.42199571],
+                                     [38.59682041, 42.95008531],
+                                     [35.45189206, 37.3452509],
+                                     [32.43435578, 30.72373327],
+                                     [31.73750748, 30.89485287],
+                                     [29.37284023, 31.44344415],
+                                     [27.66001308, 31.81016309],
+                                     [26.31358296, 32.08057499],
+                                     [25.1963477, 32.29313986],
+                                     [24.23118049, 32.46821821],
+                                     [23.36993508, 32.61780082],
+                                     [22.57998837, 32.74952569],
+                                     [21.8375532, 32.86857867],
+                                     [21.12396693, 32.97868717],
+                                     [20.42339605, 33.08268331],
+                                     [19.72121983, 33.18284728],
+                                     [19.00268283, 33.28113306],
+                                     [18.2515215, 33.3793305],
+                                     [17.4482606, 33.47919405],
+                                     [16.56773514, 33.58255576],
+                                     [15.57501961, 33.6914282],
+                                     [14.4180087, 33.8080799],
+                                     [13.01234319, 33.93498577],
+                                     [11.20625437, 34.0742239],
+                                     [8.67990371, 34.22415978],
+                                     [7.89344478, 34.26018768],
+                                     [8.69446485, 41.19823568],
+                                     [9.25707165, 47.17351118],
+                                     [9.66283477, 53.14128114],
+                                     [9.84134875, 59.09937166],
+                                     [9.65054241, 65.04458004],
+                                     [8.7667375, 70.97023122],
+                                     [6.28280904, 76.85731403]], dtype='float64')
+
+VERTICES_TEST_IS_INSIDE2 = np.array([[49.94506701, 46.52610743],
+                                     [51.04293649, 46.52610743],
+                                     [62.02163129, 46.52610743],
+                                     [73.0003261, 46.52610743],
+                                     [83.9790209, 46.52610743],
+                                     [85.05493299, 46.52610743],
+                                     [85.05493299, 45.76549301],
+                                     [85.05493299, 37.58315571],
+                                     [85.05493299, 28.39260587],
+                                     [85.05493299, 18.33178739],
+                                     [85.05493299, 17.30750918],
+                                     [83.95706351, 17.30750918],
+                                     [72.97836871, 17.30750918],
+                                     [61.9996739, 17.30750918],
+                                     [51.0209791, 17.30750918],
+                                     [49.94506701, 17.30750918],
+                                     [49.94506701, 18.35262921],
+                                     [49.94506701, 28.41192025],
+                                     [49.94506701, 37.60055422],
+                                     [49.94506701, 45.78080831]], dtype='float64')
+
 
 class TestSCoordinate(unittest.TestCase):
     """Test SCoordinates."""
@@ -301,7 +383,10 @@ class TestSphericalPolygon(unittest.TestCase):
         polygon2 = SphPolygon(np.deg2rad(vertices))
 
         self.assertTrue(polygon1._is_inside(polygon2))
+
         self.assertFalse(polygon2._is_inside(polygon1))
+
+        # Why checking the areas here!? It has nothing to do with the is_inside function!
         self.assertTrue(polygon2.area() > polygon1.area())
 
         polygon2.invert()
@@ -333,95 +418,19 @@ class TestSphericalPolygon(unittest.TestCase):
         self.assertTrue(polygon1._is_inside(polygon2))
         self.assertFalse(polygon2._is_inside(polygon1))
 
-        vertices = np.array([[-1.54009253, 82.62402855],
-                             [3.4804808, 82.8105746],
-                             [20.7214892, 83.00875812],
-                             [32.8857629, 82.7607758],
-                             [41.53844302, 82.36024339],
-                             [47.92062759, 81.91317164],
-                             [52.82785062, 81.45769791],
-                             [56.75107895, 81.00613046],
-                             [59.99843787, 80.56042986],
-                             [62.76998034, 80.11814453],
-                             [65.20076209, 79.67471372],
-                             [67.38577498, 79.22428],
-                             [69.39480149, 78.75981318],
-                             [71.28163984, 78.27283234],
-                             [73.09016378, 77.75277976],
-                             [74.85864685, 77.18594725],
-                             [76.62327682, 76.55367303],
-                             [78.42162204, 75.82918893],
-                             [80.29698409, 74.97171721],
-                             [82.30538638, 73.9143231],
-                             [84.52973107, 72.53535661],
-                             [87.11696138, 70.57600156],
-                             [87.79163209, 69.98712409],
-                             [72.98142447, 67.1760143],
-                             [61.79517279, 63.2846272],
-                             [53.50600609, 58.7098766],
-                             [47.26725347, 53.70533139],
-                             [42.44083259, 48.42199571],
-                             [38.59682041, 42.95008531],
-                             [35.45189206, 37.3452509],
-                             [32.43435578, 30.72373327],
-                             [31.73750748, 30.89485287],
-                             [29.37284023, 31.44344415],
-                             [27.66001308, 31.81016309],
-                             [26.31358296, 32.08057499],
-                             [25.1963477, 32.29313986],
-                             [24.23118049, 32.46821821],
-                             [23.36993508, 32.61780082],
-                             [22.57998837, 32.74952569],
-                             [21.8375532, 32.86857867],
-                             [21.12396693, 32.97868717],
-                             [20.42339605, 33.08268331],
-                             [19.72121983, 33.18284728],
-                             [19.00268283, 33.28113306],
-                             [18.2515215, 33.3793305],
-                             [17.4482606, 33.47919405],
-                             [16.56773514, 33.58255576],
-                             [15.57501961, 33.6914282],
-                             [14.4180087, 33.8080799],
-                             [13.01234319, 33.93498577],
-                             [11.20625437, 34.0742239],
-                             [8.67990371, 34.22415978],
-                             [7.89344478, 34.26018768],
-                             [8.69446485, 41.19823568],
-                             [9.25707165, 47.17351118],
-                             [9.66283477, 53.14128114],
-                             [9.84134875, 59.09937166],
-                             [9.65054241, 65.04458004],
-                             [8.7667375, 70.97023122],
-                             [6.28280904, 76.85731403]])
+        vertices = VERTICES_TEST_IS_INSIDE1
+
         polygon1 = SphPolygon(np.deg2rad(vertices))
 
-        vertices = np.array([[49.94506701, 46.52610743],
-                             [51.04293649, 46.52610743],
-                             [62.02163129, 46.52610743],
-                             [73.0003261, 46.52610743],
-                             [83.9790209, 46.52610743],
-                             [85.05493299, 46.52610743],
-                             [85.05493299, 45.76549301],
-                             [85.05493299, 37.58315571],
-                             [85.05493299, 28.39260587],
-                             [85.05493299, 18.33178739],
-                             [85.05493299, 17.30750918],
-                             [83.95706351, 17.30750918],
-                             [72.97836871, 17.30750918],
-                             [61.9996739, 17.30750918],
-                             [51.0209791, 17.30750918],
-                             [49.94506701, 17.30750918],
-                             [49.94506701, 18.35262921],
-                             [49.94506701, 28.41192025],
-                             [49.94506701, 37.60055422],
-                             [49.94506701, 45.78080831]])
+        vertices = VERTICES_TEST_IS_INSIDE2
+
         polygon2 = SphPolygon(np.deg2rad(vertices))
 
         self.assertFalse(polygon2._is_inside(polygon1))
         self.assertFalse(polygon1._is_inside(polygon2))
 
-    def test_bool(self):
-        """Test the intersection and union functions."""
+    def test_union_polygons_overlap_partially(self):
+        """Test the union method."""
         vertices = np.array([[180, 90, 0, -90],
                              [89, 89, 89, 89]]).T
         poly1 = SphPolygon(np.deg2rad(vertices))
@@ -445,6 +454,41 @@ class TestSphericalPolygon(unittest.TestCase):
                         [-135.,   89.],
                         [-157.5,   89.23460094],
                         [-180.,   89.]])
+
+        poly_union = poly1.union(poly2)
+
+        self.assertTrue(np.allclose(poly_union.vertices, np.deg2rad(uni)))
+
+    def test_union_polygons_overlaps_completely(self):
+        """Test the union method when one polygon is entirely inside the other."""
+        vertices = np.array([[1, 1, 20, 20],
+                             [1, 20, 20, 1]]).T
+
+        poly1 = SphPolygon(np.deg2rad(vertices))
+
+        vertices = np.array([[0, 0, 30, 30],
+                             [0, 30, 30, 0]]).T
+        poly2 = SphPolygon(np.deg2rad(vertices))
+
+        poly_union1 = poly1.union(poly2)
+        poly_union2 = poly2.union(poly1)
+
+        expected = np.deg2rad(np.array([[0, 0, 30, 30],
+                                        [0, 30, 30, 0]]).T)
+
+        self.assertTrue(np.allclose(poly_union1.vertices, expected))
+
+        self.assertTrue(np.allclose(poly_union2.vertices, expected))
+
+    def test_intersection(self):
+        """Test the intersection function."""
+        vertices = np.array([[180, 90, 0, -90],
+                             [89, 89, 89, 89]]).T
+        poly1 = SphPolygon(np.deg2rad(vertices))
+        vertices = np.array([[-45, -135, 135, 45],
+                             [89, 89, 89, 89]]).T
+        poly2 = SphPolygon(np.deg2rad(vertices))
+
         inter = np.array([[157.5,   89.23460094],
                           [112.5,   89.23460094],
                           [67.5,   89.23460094],
@@ -454,14 +498,9 @@ class TestSphericalPolygon(unittest.TestCase):
                           [-112.5,   89.23460094],
                           [-157.5,   89.23460094]])
         poly_inter = poly1.intersection(poly2)
-        poly_union = poly1.union(poly2)
-
-        self.assertTrue(poly_inter.area() <= poly_union.area())
 
         self.assertTrue(np.allclose(poly_inter.vertices,
                                     np.deg2rad(inter)))
-        self.assertTrue(np.allclose(poly_union.vertices,
-                                    np.deg2rad(uni)))
 
         # Test 2 polygons sharing 2 contiguous edges.
 


=====================================
pyresample/test/test_spherical_utils.py
=====================================
@@ -0,0 +1,411 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Adam.Dybbroe
+
+# Author(s):
+
+#   Adam Dybbroe <Firstname.Lastname at smhi.se>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+"""
+
+from unittest.mock import patch
+import pytest
+import numpy as np
+from pyresample.spherical_utils import GetNonOverlapUnionsBaseClass
+from pyresample.spherical_utils import merge_tuples
+from pyresample.spherical_utils import check_keys_int_or_tuple
+from pyresample.spherical_utils import check_if_two_polygons_overlap
+from pyresample.spherical import SphPolygon
+
+
+SET_A = {1, 3, 5, 7, 9}
+SET_B = {2, 4, 6, 8, 10}
+SET_C = {12, 14, 16, 18}
+SET_D = set(range(10, 20))
+SET_E = set(range(20, 30, 3))
+SET_F = set(range(21, 30, 3))
+SET_G = set(range(22, 30, 2))
+
+
+def fake_merge_tuples(intuple):
+    """Fake the merge tuples method."""
+    if intuple == (2, (1, 3)):
+        return (2, 1, 3)
+    if intuple == (5, (4, 6)):
+        return (5, 4, 6)
+
+    return None
+
+
+def test_check_overlap_one_polygon_entirely_inide_another():
+    """Test the function to check if two polygons overlap each other.
+
+    In this case one polygon is entirely inside the other.
+    """
+    vertices = np.array([[1, 1, 20, 20],
+                         [1, 20, 20, 1]]).T
+    poly1 = SphPolygon(np.deg2rad(vertices))
+    vertices = np.array([[0, 0, 30, 30],
+                         [0, 30, 30, 0]]).T
+    poly2 = SphPolygon(np.deg2rad(vertices))
+
+    result = check_if_two_polygons_overlap(poly1, poly2)
+    assert result is True
+
+
+def test_check_overlap_one_polygon_not_entirely_inside_another():
+    """Test the function to check if two polygons overlap each other.
+
+    In this case one polygon is not entirely inside the other but they overlap
+    each other.
+    """
+    vertices = np.array([[180, 90, 0, -90],
+                         [89, 89, 89, 89]]).T
+    poly1 = SphPolygon(np.deg2rad(vertices))
+
+    vertices = np.array([[-45, -135, 135, 45],
+                         [89, 89, 89, 89]]).T
+    poly2 = SphPolygon(np.deg2rad(vertices))
+
+    res = check_if_two_polygons_overlap(poly1, poly2)
+    assert res is True
+
+
+def test_check_overlap_two_polygons_having_no_overlap():
+    """Test the function to check if two polygons overlap each other.
+
+    In this case the two polygons do not have any overlap.
+    """
+    vertices = np.array([[10, 10, 20, 20],
+                         [10, 20, 20, 10]]).T
+    poly1 = SphPolygon(np.deg2rad(vertices))
+    vertices = np.array([[25, 25, 40, 40],
+                         [25, 40, 40, 25]]).T
+    poly2 = SphPolygon(np.deg2rad(vertices))
+
+    res = check_if_two_polygons_overlap(poly1, poly2)
+
+    assert res is False
+
+
+ at patch('pyresample.spherical_utils.check_keys_int_or_tuple')
+def test_merge_when_input_objects_do_not_overlap(keys_int_or_tuple):
+    """Test main method (merge) of the GetNonOverlapUnionsBaseClass class."""
+    mysets = [{1, 3, 5, 7, 9}, {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}]
+    myobjects = GetNonOverlapUnionsBaseClass(mysets)
+
+    keys_int_or_tuple.return_code = None
+
+    with patch('pyresample.spherical_utils.merge_tuples', return_value=0):
+        myobjects.merge()
+
+    polygons = myobjects.get_polygons()
+
+    assert polygons == mysets
+
+
+ at patch('pyresample.spherical_utils.check_keys_int_or_tuple')
+def test_merge_overlapping_and_nonoverlapping_objects(keys_int_or_tuple):
+    """Test main method (merge) of the GetNonOverlapUnionsBaseClass class."""
+    mysets = [SET_A, SET_B, SET_C, SET_D, SET_E, SET_F, SET_G]
+    myobjects = GetNonOverlapUnionsBaseClass(mysets)
+
+    keys_int_or_tuple.return_code = None
+
+    with patch('pyresample.spherical_utils.merge_tuples') as mypatch:
+        mypatch.side_effect = fake_merge_tuples
+        myobjects.merge()
+
+    polygons = myobjects.get_polygons()
+    ids = myobjects.get_ids()
+
+    polygons_expected = [{1, 3, 5, 7, 9},
+                         {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
+                         {20, 21, 22, 23, 24, 26, 27, 28, 29}]
+
+    assert polygons == polygons_expected
+
+    ids_expected = [0, (2, 1, 3), (5, 4, 6)]
+
+    assert ids == ids_expected
+
+
+def test_flatten_tuple_input_requires_tuple():
+    """Test flatten a nested tuple of integers.
+
+    Input a scalar (not a tuple).
+
+    """
+    with pytest.raises(TypeError) as exec_info:
+        _ = merge_tuples(7)
+
+    exception_raised = exec_info.value
+    assert str(exception_raised) == "Function argument must be a tuple!"
+
+
+def test_flatten_tuple_input_1tuple():
+    """Test flatten a nested tuple of integers.
+
+    Input a tuple of one scalar.
+
+    """
+    intuple = (0,)
+    res = merge_tuples(intuple)
+    assert res == (0,)
+
+
+def test_flatten_tuple_input_2tuple_of_2tuples_of_2tuples():
+    """Test flatten a nested tuple of integers.
+
+    Input a 2-tuple of 2-tuples of 2-tuples
+
+    """
+    intuple = (((0, 1), (2, 3)), ((4, 5), (6, 7)))
+    res = merge_tuples(intuple)
+    assert res == (0, 1, 2, 3, 4, 5, 6, 7)
+
+
+def test_flatten_tuple_input_2tuple_of_scalar_and_2tuple():
+    """Test flatten a nested tuple of integers.
+
+    Input a 2-tuple of a scalar and a 2-tuple
+
+    """
+    intuple = (3, (1, 2))
+    res = merge_tuples(intuple)
+    assert res == (3, 1, 2)
+
+
+def test_flatten_tuple_input_3tuple_of_scalar_and_2tuple_and_2tuple_of_scalar_and_tuple():
+    """Test flatten a nested tuple of integers.
+
+    Input a 3-tuple of a scalar, a 2-tuple and a 2-tuple of a scalar and a tuple
+
+    """
+    intuple = (0, (1, 2), (3, (4, 5)))
+    res = merge_tuples(intuple)
+    assert res == (0, 1, 2, 3, 4, 5)
+
+
+def test_flatten_tuple_input_tuple_of_scalar_and_2tuple_and_2tuple_of_scalar_and_tuple_and_1tuple():
+    """Test flatten a nested tuple of integers.
+
+    Input a tuple of a scalar, a 2-tuple, a 2-tuple of a scalar and a tuple, and a 1-tuple.
+
+    """
+    intuple = (0, (1, 2), (3, (4, 5)), (6,))
+    res = merge_tuples(intuple)
+    assert res == (0, 1, 2, 3, 4, 5, 6)
+
+
+def test_find_union_pairs_input_one_set():
+    """Test finding a union pair.
+
+    In this case input only one listed set (~polygon).
+
+    """
+    listed_sets = dict(enumerate((SET_A, )))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv is None
+
+
+def test_find_union_pairs_input_two_non_overlapping_sets():
+    """Test finding a union pair.
+
+    In this case input two non-overlapping sets giving no union.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv is None
+
+
+def test_find_union_pairs_input_two_overlapping_sets():
+    """Test finding a union pair.
+
+    In this case input two overlapping sets.
+
+    """
+    listed_sets = dict(enumerate((SET_C, SET_D)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv == ((0, 1), set(range(10, 20)))
+
+
+def test_find_union_pairs_input_three_sets_one_entirely_without_overlap():
+    """Test finding a union pair.
+
+    In this case input three sets where one does not overlap the two others
+    which in turn do overlap each other.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B, SET_D)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv == ((1, 2), {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19})
+
+
+def test_find_union_pairs_input_four_sets_where_only_two_have_overlap():
+    """Test finding a union pair.
+
+    In this case input four sets where two overlap and the other two does not
+    overlap with any other.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B, SET_C, SET_D)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv == ((1, 3), {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19})
+
+
+def test_find_union_pairs_input_three_sets_one_entirely_without_overlap_one_already_a_union():
+    """Test finding a union pair.
+
+    In this case input three sets, one with no overlap of the others, and one
+    of the overlapping ones is already a paired union.
+
+    """
+    listed_sets = {0: {1, 3, 5, 7, 9},
+                   4: {0, 10},
+                   (2, (1, 3)): {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}
+
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv == ((4, (2, (1, 3))), {0, 2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19})
+
+
+def test_find_union_pairs_input_two_sets_without_overlap_one_already_a_union():
+    """Test finding a union pair.
+
+    In this case input two sets with no overlap, but one is already a paired
+    union.
+
+    """
+    listed_sets = {0: {1, 3, 5, 7, 9},
+                   (2, (1, 3)): {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}
+
+    this = GetNonOverlapUnionsBaseClass([])
+    retv = this._find_union_pair(listed_sets)
+
+    assert retv is None
+
+
+def test_merge_unions_input_three_sets_without_overlap():
+    """Test merging union pairs iteratively.
+
+    In this case input three sets without any overlap.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B, SET_C)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._merge_unions(listed_sets)
+
+    assert retv == {0: SET_A, 1: SET_B, 2: SET_C}
+
+
+def test_merge_unions_input_two_overlapping_sets():
+    """Test merging union pairs iteratively.
+
+    In this case input two overlapping sets.
+    """
+    listed_sets = dict(enumerate((SET_C, SET_D)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._merge_unions(listed_sets)
+
+    assert retv == {(0, 1): set(range(10, 20))}
+
+
+def test_merge_unions_input_four_sets_one_overlapping_two_others():
+    """Test merging union pairs iteratively.
+
+    In this case input 4 sets, where one is overlapping two others.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B, SET_C, SET_D)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._merge_unions(listed_sets)
+
+    assert retv == {0: {1, 3, 5, 7, 9},
+                    (2, (1, 3)): {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}
+
+
+def test_merge_unions_input_two_non_overlapping_sets():
+    """Test merging union pairs iteratively.
+
+    In this case input 2 sets that do not overlap, but one is already a paired union.
+
+    """
+    listed_sets = {0: {1, 3, 5, 7, 9},
+                   (2, (1, 3)): {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._merge_unions(listed_sets)
+
+    assert retv == listed_sets
+
+
+def test_merge_unions_input_seven_sets_with_overlaps():
+    """Test merging union pairs iteratively.
+
+    In this case input 7 sets, several of which overlap each other but one is
+    completely unique and has no overlap with any of the other 6.
+
+    """
+    listed_sets = dict(enumerate((SET_A, SET_B, SET_C, SET_D, SET_E, SET_F, SET_G)))
+    this = GetNonOverlapUnionsBaseClass(listed_sets)
+    retv = this._merge_unions(listed_sets)
+
+    assert retv == {0: {1, 3, 5, 7, 9},
+                    (2, (1, 3)): {2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
+                    (5, (4, 6)): {20, 21, 22, 23, 24, 26, 27, 28, 29}}
+
+
+def test_check_keys_int_or_tuple_input_okay():
+    """Test the check for dictionary keys and input only a dict with the accepted keys of integers and tuples."""
+
+    adict = {1: [1, 2, 3], (2, 3): [1, 2, 3, 4], (6, (4, 5)): [1, 2, 3, 4, 5]}
+    check_keys_int_or_tuple(adict)
+
+
+def test_check_keys_int_or_tuple_input_string():
+    """Test the check for dictionary keys and input a dict with a key which is a string."""
+
+    adict = {1: [1, 2, 3], 'set B': [1, 2, 3, 4]}
+    with pytest.raises(KeyError) as exec_info:
+        check_keys_int_or_tuple(adict)
+
+    exception_raised = exec_info.value
+
+    assert str(exception_raised) == "'Key must be integer or a tuple (of integers)'"
+
+
+def test_check_keys_int_or_tuple_input_float():
+    """Test the check for dictionary keys and input a dict with a key which is a float."""
+    adict = {1.1: [1, 2, 3]}
+    with pytest.raises(KeyError) as exec_info:
+        check_keys_int_or_tuple(adict)
+
+    exception_raised = exec_info.value
+    assert str(exception_raised) == "'Key must be integer or a tuple (of integers)'"


=====================================
pyresample/version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
     # setup.py/versioneer.py will grep for the variable names, so they must
     # each be defined on a line of their own. _version.py will just call
     # get_keywords().
-    git_refnames = " (tag: v1.20.0)"
-    git_full = "2456508a6d18e04dcce517a0c09238fd5e7bb0da"
-    git_date = "2021-06-04 10:29:10 -0500"
+    git_refnames = " (HEAD -> main, tag: v1.21.0)"
+    git_full = "031260c097ed2cb28f1a27c286147fa3f2ed536b"
+    git_date = "2021-08-19 08:03:26 -0500"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyresample/-/compare/d5e0534fbf35184d38b3a9c680f7dcc2338ba251...3afe6ff07bfae7bfd4e7d9fd9dc130d1870cf440

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyresample/-/compare/d5e0534fbf35184d38b3a9c680f7dcc2338ba251...3afe6ff07bfae7bfd4e7d9fd9dc130d1870cf440
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210904/5874850a/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list