How To Avoid Places

metadata

The eagle-eyed amongst you may have noticed something interesting in : I chose to highlight some countries in particular (the UK and the US) and I chose to call this list avoids in the source code (line 19).

Whilst the previous script did indeed easily display where the antipode of any point was (thus helping me to find that Tasmania would be a great bet) it did have one huge flaw if it was to be used to avoid places: it would only tell you how to avoid a point. The script was not helpful at all at telling you how to avoid an area, such as a country: I therefore set out to write version 2 of my script.

My chosen method was to expand the coastlines of a list of countries by set distances. By successively increasing the coastlines I would create an area on the far side of the Earth that was further away than the expanded coastline (or indeed outside this “exclusion zone”). I could then increase the distance until there was nowhere left on the surface of the Earth that was that far away from the coastlines. The technical term for such an operation is to buffer an object and Shapely has a function to do just that.

Unfortunately, the Shapely function has no knowledge that it is being used to describe areas on the surface of the Earth: it is just a library that describes shapes. Consequently, the buffer function expands objects by whatever unit the object is described in, in this case: degrees. This poses a major problem: the length of a degree changes depending on where you are in the world and which direction you would like to go. Additionally, it has no knowledge of the anti-meridian and the problem that it poses. What I needed was a function that took in an area defined in degrees and buffered it by a single distance in kilometres: the resultant object would be buffered by a varying amount in units of degrees but by a constant amount in units of kilometres. For this script to work I wrote two functions:

Once these two functions were written (and extra logic was added to deal with areas overlapping the anti-meridian) it was relatively easy to buffer the coastlines of the UK and the US and find out where in the world to go if you wanted to avoid them. Below is the script that I wrote that uses these two functions (I found that simplifying the polygons whenever I could drastically reduced runtime).

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

#!/usr/bin/env python3

# Use the proper idiom in the main module ...
# NOTE: See https://docs.python.org/3.11/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
if __name__ == "__main__":
    # Import standard modules ...
    import os

    # Import special modules ...
    try:
        import cartopy
        cartopy.config.update(
            {
                "cache_dir" : os.path.expanduser("~/.local/share/cartopy_cache"),
            }
        )
    except:
        raise Exception("\"cartopy\" is not installed; run \"pip install --user Cartopy\"") from None
    try:
        import matplotlib
        matplotlib.rcParams.update(
            {
                       "backend" : "Agg",                                       # NOTE: See https://matplotlib.org/stable/gallery/user_interfaces/canvasagg.html
                    "figure.dpi" : 300,
                "figure.figsize" : (9.6, 7.2),                                  # NOTE: See https://github.com/Guymer/misc/blob/main/README.md#matplotlib-figure-sizes
                     "font.size" : 8,
            }
        )
        import matplotlib.pyplot
    except:
        raise Exception("\"matplotlib\" is not installed; run \"pip install --user matplotlib\"") from None
    try:
        import shapely
        import shapely.geometry
        import shapely.ops
    except:
        raise Exception("\"shapely\" is not installed; run \"pip install --user Shapely\"") from None

    # Import my modules ...
    try:
        import pyguymer3
        import pyguymer3.geo
        import pyguymer3.image
    except:
        raise Exception("\"pyguymer3\" is not installed; you need to have the Python module from https://github.com/Guymer/PyGuymer3 located somewhere in your $PYTHONPATH") from None

    # **************************************************************************

    # List countries to find the antipodes of ...
    avoids = [
        "United Kingdom",
        "United States of America",
    ]

    # Create colours and define number of angles and resolution ...
    colours = ["red", "green", "blue", "cyan", "magenta", "yellow", "red", "green", "blue", "cyan", "magenta", "yellow"]
    fill = 0.1                                                                  # [°]
    nang = 361                                                                  # [#]
    res = "10m"
    simp = 0.01                                                                 # [°]
    tol = 1.0e-10                                                               # [°]

    # Find file containing all the country shapes ...
    sfile = cartopy.io.shapereader.natural_earth(
          category = "cultural",
              name = "admin_0_countries",
        resolution = res,
    )

    # **************************************************************************
    # *               CREATE MULTIPOLYGON TO FIND THE BUFFER OF                *
    # **************************************************************************

    # Create empty list ...
    geoms = []

    # Loop over records ...
    for record in cartopy.io.shapereader.Reader(sfile).records():
        # Create short-hand ...
        neName = pyguymer3.geo.getRecordAttribute(record, "NAME")

        # Check if this record is to be avoided ...
        skip = True
        if neName in avoids:
            skip = False

        # Skip this record if it is not a country to be avoided ...
        if skip:
            continue

        # Add Polygons to the list ...
        geoms += pyguymer3.geo.extract_polys(record.geometry)

    # Convert list of Polygons to (unified) [Multi]Polygon ...
    geoms = shapely.ops.unary_union(geoms).simplify(tol)
    pyguymer3.geo.check(geoms)

    # **************************************************************************

    # Create figure ...
    fg = matplotlib.pyplot.figure(figsize = (12.8, 7.2))

    # Create axis ...
    ax = pyguymer3.geo.add_axis(fg)

    # Configure axis ...
    pyguymer3.geo.add_map_background(ax, resolution = "large8192px")

    # Add country outlines ...
    for record in cartopy.io.shapereader.Reader(sfile).records():
        ax.add_geometries(
            pyguymer3.geo.extract_polys(record.geometry),
            cartopy.crs.PlateCarree(),
            edgecolor = "black",
            facecolor = "none",
            linewidth = 0.5,
        )

    # Initialize distance, labels and lines ...
    dist2 = 0.0                                                                 # [km]
    labels = []
    lines = []

    # Loop over distances ...
    for i in range(10):
        # Set distance ...
        if i >= 6:
            dist1 = 0.5e6                                                       # [m]
        else:
            dist1 = 2.0e6                                                       # [m]
        dist2 += 0.001 * dist1                                                  # [km]

        # Find out how complicated the [Multi]Polygon is ...
        npoints = 0                                                             # [#]
        npolys = 0                                                              # [#]
        for poly in pyguymer3.geo.extract_polys(geoms):
            npoints += len(poly.exterior.coords)                                # [#]
            npolys += 1                                                         # [#]
            for interior in poly.interiors:
                npoints += len(interior.coords)                                 # [#]

        print(f"Calculating for {dist2:6,.0f} km (using {npoints:,d} points in {npolys:,d} Polygons) ...")

        # Buffer area ...
        # NOTE: geoms is the [Multi]Polygon to be buffered.
        geoms = pyguymer3.geo.buffer(
            geoms,
            dist1,
            debug = False,
             fill = fill,
             nang = nang,
             simp = simp,
              tol = tol,
        )
        # NOTE: geoms is now the simplified [Multi]Polygon buffer of geoms.

        # Add to plot ...
        ax.add_geometries(
            pyguymer3.geo.extract_polys(geoms),
            cartopy.crs.PlateCarree(),
            edgecolor = colours[i],
            facecolor = "none",
            linewidth = 1.0,
        )
        labels.append(f"{dist2:6,.0f} km")
        lines.append(matplotlib.lines.Line2D([], [], color = colours[i]))

    print("Plotting ...")

    # Configure axis ...
    ax.legend(
        lines,
        labels,
         loc = "upper center",
        ncol = 5,
    )

    # Configure figure ...
    fg.tight_layout()

    # Save figure ...
    fg.savefig("find_antipode_v2.png")
    matplotlib.pyplot.close(fg)

    # Optimize PNG ...
    pyguymer3.image.optimize_image(
        "find_antipode_v2.png",
        strip = True,
    )

              
You may also download “find_antipode_v2.py” directly or view “find_antipode_v2.py” on GitHub Gist (you may need to manually checkout the “main” branch).

The image produced by this script is shown below.

Download:
  1. 512 px × 288 px (0.1 Mpx; 224.7 KiB)
  2. 1,024 px × 576 px (0.6 Mpx; 851.0 KiB)
  3. 2,048 px × 1,152 px (2.4 Mpx; 3.0 MiB)
  4. 3,840 px × 2,160 px (8.3 Mpx; 7.0 MiB)

A couple of things strike me when I look at this image: