Skip to content

Commit

Permalink
Added methods to check if below sea-level points were considered sea …
Browse files Browse the repository at this point in the history
…when the mask was generated and the sea slope was added.
  • Loading branch information
MichaelClerx committed Jun 19, 2024
1 parent 8eede68 commit c2cfef3
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 11 deletions.
6 changes: 6 additions & 0 deletions docs/source/os_terrain_50.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Accessing the data

.. autofunction:: spacing

Checking if points are sea or just below sea-level
==================================================

.. autofunction:: inland_below_sea_level

.. autofunction:: is_sea

Downloading the data
====================
Expand Down
2 changes: 2 additions & 0 deletions nevis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
DataNotFoundError,
download_os_terrain_50,
gb,
inland_below_sea_level,
is_sea,
spacing,
)
from ._interpolation import ( # noqa
Expand Down
90 changes: 82 additions & 8 deletions nevis/_os_terrain_50.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
terrain_file = 'terr50_gagg_gb'
terrain_file_zip = os.path.join(nevis._DIR_DATA, terrain_file + '.zip')
terrain_file_npy = os.path.join(nevis._DIR_DATA, terrain_file + '.npy')
not_sea_file = 'not_sea'
not_sea_file_npy = os.path.join(nevis._DIR_DATA, not_sea_file + '.npy')

# URL to download it from
url = ('https://api.os.uk/downloads/v1/products/Terrain50/downloads?'
Expand All @@ -41,8 +43,10 @@
# .asc file encoding
ENC = 'utf-8'

# Cached heights
# Cached heights and not-sea-mask
_heights = None
_not_sea = None # y * width + x
_not_sea_unpacked = None # array of y, x indices


class DataNotFoundError(RuntimeError):
Expand All @@ -64,21 +68,30 @@ def gb(downsampling=None):
A downsampled version can be returned for testing purposes, by setting
``downsampling`` to any integer greater than one.
"""
global _heights
global _heights, _not_sea

# Load data (or retrieve from cache)
if _heights is None:
# Check file exists
if _heights is None or _not_sea is None:
# Check files exists
if not os.path.isfile(terrain_file_npy):
raise DataNotFoundError(
f'OS Terrain 50 data not found in {nevis._DIR_DATA}.'
' Please use nevis.download_os_terrain_50() to download and'
' process this data set.'
)
# Don't load the heights data yet, may have to be rebuild it anyway!
if not os.path.isfile(not_sea_file_npy):
raise DataNotFoundError(
'List of below sea-level inland points not found in'
' {nevis._DIR_DATA}. Please call'
' nevis.download_os_terrain_50() to create this file.'
)

# Load
# Load both files
_heights = np.load(terrain_file_npy)
_heights.setflags(write=False)
_not_sea = np.load(not_sea_file_npy)
_not_sea.setflags(write=False)

# Return downsampled version for testing (but keep full version in cache)
if downsampling is not None:
Expand All @@ -89,6 +102,44 @@ def gb(downsampling=None):
return _heights


def inland_below_sea_level_indices():
"""
Returns a list of indices in the heights data that are below sea-level, but
which nevis's preprocessing did not consider to be "sea".
"""
global _not_sea_unpacked

if _not_sea_unpacked is None:
if _not_sea is None:
gb()
x = _not_sea % _heights.shape[1]
y = _not_sea // _heights.shape[1]
_not_sea_unpacked = np.vstack((y, x)).T

return _not_sea_unpacked


def inland_below_sea_level():
"""
Returns a list of points (x, y) in meters that are below sea-level, but
which nevis's preprocessing did not consider to be "sea".
"""
ij = inland_below_sea_level_indices() * resolution
return np.vstack((ij[:, 1], ij[:, 0])).T


def is_sea(coords):
"""
Returns ``True`` if and only if the given :class:`nevis.Coords` were
classified as "sea" by nevis's pre-processing.
"""
if _heights is None:
gb()
x, y = coords.grid // nevis.spacing()
return _heights[y, x] < 0 and (
(y * _heights.shape[1] + x) not in _not_sea)


def download_os_terrain_50(force=False):
"""
Downloads, unpacks and processes the OS Terrain 50 data set.
Expand All @@ -102,7 +153,9 @@ def download_os_terrain_50(force=False):
global _heights

# Already done and not forcing? Then return
if (not force) and os.path.isfile(terrain_file_npy):
have_terrain = os.path.isfile(terrain_file_npy)
have_not_sea = os.path.isfile(not_sea_file_npy)
if (not force) and have_terrain and have_not_sea:
print('Downloaded, unpacked, and processed file already found:'
' Skipping.')
return
Expand Down Expand Up @@ -141,7 +194,9 @@ def download_os_terrain_50(force=False):
download_gagg(url, terrain_file_zip)

# Unpack / process
if force or not os.path.isfile(terrain_file_npy):
if force or not have_terrain or not have_not_sea:
# Temporary sea-mask level
s = -100

# Create empty array
width, height = nevis.dimensions()
Expand All @@ -153,7 +208,6 @@ def download_os_terrain_50(force=False):
extract(terrain_file, heights, resolution)

# Replace missing values by far-below-sea level
s = -100
heights[np.isnan(heights)] = s

# Fix odd squares
Expand All @@ -165,6 +219,12 @@ def download_os_terrain_50(force=False):
# Create a "sea mask" in the loaded data, indicating sea with -100
set_sea_level(heights, s)

# Store below-zero points that are not in the sea, so that we can
# later detect if something has been labelled sea or not.
not_sea = inland_below_sea_level_points(heights, s)
print(f'Saving to {not_sea_file_npy}...')
np.save(not_sea_file_npy, not_sea)

# Make the points labelled as sea slope towards the land, to make it
# more findable.
add_sea_slope(heights, s)
Expand All @@ -174,6 +234,9 @@ def download_os_terrain_50(force=False):

# Store
_heights = heights
_heights.setflags(write=False)
_not_sea = not_sea
_not_sea.setflags(write=False)


def download_gagg(url, fname, print_to_screen=True):
Expand Down Expand Up @@ -386,6 +449,17 @@ def save_cambridgeshire(heights):
heights[5851, 13013] = 0.01 # TM59


def inland_below_sea_level_points(heights, s, print_to_screen=True):
"""
Return an array of (x, y) coordinates that are below sea-level, but not to
be treated as sea.
"""
y, x = np.nonzero(np.logical_and(heights < 0, heights > s))
p = y * heights.shape[1] + x
assert np.max(p) < 2**32, 'Not-sea point does not fit uint32'
return np.array(p, dtype=np.uint32)


def set_sea_level(heights, s, print_to_screen=True):
"""
Create a "mask" on the heights map, by setting all entries suspected to be
Expand Down
8 changes: 5 additions & 3 deletions nevis/_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,15 @@ def meters2indices(x, y):

# Show requested points
if points is not None:
points = np.asarray(points)
alpha, ms, mw = (0.3, 4, 1) if len(points) > 20 else (1, 12, 2)
x, y = meters2indices(points[:, 0], points[:, 1])
ax.plot(
x, y, 'x', color='#0000ff',
markeredgewidth=1, markersize=4, alpha=0.3)
ax.plot(x, y, 'x', color='#0000ff',
markeredgewidth=mw, markersize=ms, alpha=alpha)

# Show trajectory
if trajectory is not None:
trajectory = np.asarray(trajectory)
x, y = meters2indices(trajectory[:, 0], trajectory[:, 1])
ax.plot(
x, y, 'o-', color='#000000',
Expand Down

0 comments on commit c2cfef3

Please sign in to comment.