"""Provides functionality for traversing the British National Grid (BNG) index system.
It supports spatial analyses such as distance-constrained nearest neighbour searches and
'distance within' queries by offering:
- **Grid traversal**: Generate k-discs and k-rings around a given grid square.
- **Neighbourhood operations**: Identify neighbouring grid squares and checking
adjacency.
- **Distance computation**: Calculate the distance between grid square centroids.
- **Proximity queries**: Retrieve all grid squares within a specified absolute
distance.
"""
import warnings
import numpy as np
from osbng.bng_reference import BNGReference, _validate_bngreferences
from osbng.errors import BNGExtentError, BNGNeighbourError
from osbng.hierarchy import bng_to_parent
from osbng.indexing import bng_to_xy, xy_to_bng
__all__ = [
"bng_kring",
"bng_kdisc",
"bng_distance",
"bng_neighbours",
"bng_is_neighbour",
"bng_dwithin",
]
def _ring_or_disc(
bng_ref: BNGReference, k: int, is_disc: bool, return_relations: bool
) -> list[BNGReference] | list[(BNGReference, int, int)]:
"""Helper function to extract grid squares in a disc or ring.
Args:
bng_ref (BNGReference): A BNGReference object.
k (int): Grid distance in units of grid squares.
is_disc (bool): If True, returns all grid squares within distance k. If False,
only returns the outer ring.
return_relations (bool): If True, returns a list of (BNGReference, dx, dy)
tuples where dx, dy are integer offsets in grid units. If False, returns a
list of BNGReference objects.
Returns:
if return_relations==True:
list[(BNGReference, dx, dy)]: All BNGReference objects representing grid
squares in a square ring or disc of radius k, with the x- and y-offsets
(in grid square units) between bng_ref and each returned BNGReference.
else:
list[BNGReference]: All BNGReference objects representing grid squares in a
square ring or disc of radius k.
"""
# Check that k is a positive integer
if k <= 0:
raise ValueError("k must be a positive integer.")
# Derive point location of root square
xc, yc = bng_to_xy(bng_ref, position="centre")
# Initialise list of ring BNG reference objects
kring_refs = []
# Track whether we need to raise an extent warning
raise_extent_warning = False
# Iterate over all dx/dy within range
for dy in range(-k, k + 1)[::-1]:
for dx in range(-k, k + 1):
# Include all dx/dy combinations for disks
# Only include edges for rings
if is_disc | (abs(dy) == k) | (abs(dx) == k):
try:
ring_ref = xy_to_bng(
xc + (dx * bng_ref.resolution_metres),
yc + (dy * bng_ref.resolution_metres),
bng_ref.resolution_metres,
)
# Catch extent errors and track whether warning is needed
except BNGExtentError:
raise_extent_warning = True
else:
kring_refs.append(
(ring_ref, dx, dy)
) if return_relations else kring_refs.append(ring_ref)
# Raise an extent warning if an error has been caught
# Note: do this after the above, otherwise repeated warnings will be raised!
if raise_extent_warning:
warnings.warn(
"One or more of the requested grid squares falls outside of the BNG index "
+ "system extent and will not be returned."
)
return kring_refs
[docs]
@_validate_bngreferences
def bng_kring(
bng_ref: BNGReference, k: int, *, return_relations: bool = False
) -> list[BNGReference] | list[tuple[BNGReference, int, int]]:
"""Returns a hollow ring of BNGReference objects around a ``BNGReference`` object.
Returns all :class:`~osbng.bng_reference.BNGReference` objects at a grid distance
``k``.
Notes:
Returned :class:`~osbng.bng_reference.BNGReference` objects are ordered North to
South then West to East, therefore not in ring order.
Args:
bng_ref (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
k (int): Grid distance in units of grid squares.
Keyword Args:
return_relations (bool, optional): If True, returns a list of
(:class:`~osbng.bng_reference.BNGReference`, dx, dy) tuples where dx, dy are
integer offsets in grid units. If False (default), returns a list of
:class:`~osbng.bng_reference.BNGReference` objects. Keyword only.
Returns:
list[BNGReference]: If ``return_relations`` is False (default), returns all
:class:`~osbng.bng_reference.BNGReference` objects representing squares in a
square ring of radius ``k``.
If ``return_relations`` is True, returns a list of
(:class:`~osbng.bng_reference.BNGReference`, dx, dy) tuples, where dx and dy are
the x and y offsets between ``bng_ref`` and each returned
:class:`~osbng.bng_reference.BNGReference` object in units of grid squares.
Examples:
>>> bng_kring(BNGReference("SU1234"), 1)
[
BNGReference(bng_ref_formatted=SU 11 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 33, resolution_label=1km)
]
>>> bng_kring(BNGReference("SU1234"), 1, return_relations=True)
[(BNGReference(bng_ref_formatted=SU 11 35, resolution_label=1km), -1, 1),
(BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km), 0, 1),
(BNGReference(bng_ref_formatted=SU 13 35, resolution_label=1km), 1, 1),
(BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km), -1, 0),
(BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km), 1, 0),
(BNGReference(bng_ref_formatted=SU 11 33, resolution_label=1km), -1, -1),
(BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km), 0, -1),
(BNGReference(bng_ref_formatted=SU 13 33, resolution_label=1km), 1, -1)]
>>> bng_kring(BNGReference("SU1234"), 3)
[list of 24 BNGReference objects]
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_kring` instance
method.
- The :func:`~osbng.traversal.bng_kdisc` function and
:meth:`osbng.bng_reference.BNGReference.bng_kdisc` instance method for
finding all :class:`~osbng.bng_reference.BNGReference` objects within a
distance d from an input :class:`~osbng.bng_reference.BNGReference` object.
"""
return _ring_or_disc(bng_ref, k, False, return_relations)
[docs]
@_validate_bngreferences
def bng_kdisc(
bng_ref: BNGReference, k: int, *, return_relations: bool = False
) -> list[BNGReference] | list[tuple[BNGReference, int, int]]:
"""Returns a filled disc of BNGReference objects around a ``BNGReference``.
Returns all :class:`~osbng.bng_reference.BNGReference` objects up to a grid
distance ``k``, including the given central
:class:`~osbng.bng_reference.BNGReference` object.
Notes:
Returned :class:`~osbng.bng_reference.BNGReference` objects are ordered North to
South then West to East.
Args:
bng_ref (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
k (int): Grid distance in units of grid squares.
Keyword Args:
return_relations (bool, optional): If True, returns a list of
(:class:`~osbng.bng_reference.BNGReference`, dx, dy) tuples where dx, dy are
integer offsets in grid units. If False (default), returns a list of
:class:`~osbng.bng_reference.BNGReference` objects. Keyword only.
Returns:
list[BNGReference]: If ``return_relations`` is False (default), returns all
:class:`~osbng.bng_reference.BNGReference` objects representing grid squares in
a square ring of radius k.
If ``return_relations`` is True, returns a list of
(:class:`~osbng.bng_reference.BNGReference`, dx, dy) tuples, where dx and dy are
the x and y offsets between ``bng_ref`` and each returned
:class:`~osbng.bng_reference.BNGReference` object in units of grid squares.
Examples:
>>> bng_kdisc(BNGReference("SU1234"), 1)
[
BNGReference(bng_ref_formatted=SU 11 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 33, resolution_label=1km)
]
>>> bng_kdisc(BNGReference("SU1234"), 1, return_relations=True)
[(BNGReference(bng_ref_formatted=SU 11 35, resolution_label=1km), -1, 1),
(BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km), 0, 1),
(BNGReference(bng_ref_formatted=SU 13 35, resolution_label=1km), 1, 1),
(BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km), -1, 0),
(BNGReference(bng_ref_formatted=SU 12 34, resolution_label=1km), 0, 0),
(BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km), 1, 0),
(BNGReference(bng_ref_formatted=SU 11 33, resolution_label=1km), -1, -1),
(BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km), 0, -1),
(BNGReference(bng_ref_formatted=SU 13 33, resolution_label=1km), 1, -1)]
>>> bng_kdisc(BNGReference("SU1234"), 3)
[list of 49 BNGReference objects]
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_kdisc` instance
method.
- The :func:`~osbng.traversal.bng_kring` function and
:meth:`osbng.bng_reference.BNGReference.bng_kring` instance method for
finding all :class:`~osbng.bng_reference.BNGReference` objects at a
specific distance d from an input
:class:`~osbng.bng_reference.BNGReference` object.
"""
return _ring_or_disc(bng_ref, k, True, return_relations)
[docs]
@_validate_bngreferences
def bng_distance(
bng_ref1: BNGReference, bng_ref2: BNGReference, *, edge_to_edge: bool = False
) -> float:
"""Returns the euclidean distance between two ``BNGReference`` objects.
When ``edge_to_edge`` is False, the distance is the centroid-to-centroid distance in
metres. When ``edge_to_edge`` is True, the distance is the shortest distance
between any two parts of the grid squares.
Notes:
Note that the two :class:`~osbng.bng_reference.BNGReference` objects do not
necessarily need to share a common resolution. When ``edge_to_edge`` = True and
``bng_ref1`` and ``bng_ref2`` have a parent-child relationship, the returned
distance is 0.
Args:
bng_ref1 (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
bng_ref2 (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
Keyword Args:
edge_to_edge (bool, optional): If False (default), distance will be
centroid-to-centroid distance. If True, distance will be the shortest
distance between any point in the grid squares. Keyword only.
Returns:
float: The euclidean distance between the two
:class:`~osbng.bng_reference.BNGReference` objects.
Raises:
TypeError: If the first or second argument is not a
:class:`~osbng.bng_reference.BNGReference` object.
Examples:
>>> bng_distance(BNGReference("SE1433"), BNGReference("SE1533"))
1000.0
>>> bng_distance(
... BNGReference("SE1433"), BNGReference("SE1533"), edge_to_edge=True
... )
0.0
>>> bng_distance(BNGReference("SE1433"), BNGReference("SE1631"))
2828.42712474619
>>> bng_distance(BNGReference("SE1433"), BNGReference("SE"))
39147.158262126766
>>> bng_distance(BNGReference("SE1433"), BNGReference("SENW"))
42807.709586007986
>>> bng_distance(BNGReference("SE"), BNGReference("OV"))
141421.35623730952
>>> bng_distance(BNGReference("SU"), BNGReference("SU2345"), edge_to_edge=True)
0.0
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_distance` instance
method.
- The :func:`~osbng.traversal.bng_dwithin` function and
:meth:`osbng.bng_reference.BNGReference.bng_dwithin` instance method for
obtaining all :class:`~osbng.bng_reference.BNGReference` objects within a
defined distance of an input :class:`~osbng.bng_reference.BNGReference`
object.
"""
# Catch the special case of parent-child relationship when using edge-to-edge
if (bng_ref1.resolution_metres != bng_ref2.resolution_metres) & edge_to_edge:
# Identify the possible parent and child
# Note this is required, to avoid errors by testing bng_to_parent on a
# BNGReference with resolution of 100km!
parent_candidate, child_candidate = (
(bng_ref1, bng_ref2)
if bng_ref1.resolution_metres > bng_ref2.resolution_metres
else (bng_ref2, bng_ref1)
)
# Return distance of 0 if one is a parent of the other
if (
bng_to_parent(
child_candidate, resolution=parent_candidate.resolution_metres
)
== parent_candidate
):
return 0.0
# Derive the centroid of the first BNGReference object
centroid1 = bng_to_xy(bng_ref1, position="centre")
# Derive the centroid of the second BNGReference object
centroid2 = bng_to_xy(bng_ref2, position="centre")
# Note this must be a new if-else logic to the above special case, to catch cases
# where bng_ref1 and bng_ref2 do not share a resolution but are not parents
if edge_to_edge:
# For edge-to-edge distances, the x-distance and y-distance are the
# centroid-to-centroid distance minus half the box width/height at either end
dx = (
0
if centroid1[0] == centroid2[0]
else abs(centroid1[0] - centroid2[0])
- 0.5 * (bng_ref1.resolution_metres + bng_ref2.resolution_metres)
)
dy = (
0
if centroid1[1] == centroid2[1]
else abs(centroid1[1] - centroid2[1])
- 0.5 * (bng_ref1.resolution_metres + bng_ref2.resolution_metres)
)
else:
dx = centroid1[0] - centroid2[0]
dy = centroid1[1] - centroid2[1]
return float(np.sqrt(dx**2 + dy**2))
[docs]
@_validate_bngreferences
def bng_neighbours(bng_ref: BNGReference) -> list[BNGReference]:
"""Returns the four ``BNGReference`` object neighbours of a ``BNGReference``.
The neighbours are defined as the grid squares immediately North, East, South and
West of the input grid square sharing an edge with the input
:class:`~osbng.bng_reference.BNGReference` object.
Args:
bng_ref (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
Returns:
list[BNGReference]: The grid squares immediately North, East, South and West
of bng_ref, in that order.
Examples:
>>> bng_neighbours(BNGReference("SU1234"))
[
BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km)
]
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_neighbours`
instance method.
- The :func:`~osbng.traversal.bng_is_neighbour` function and
:meth:`osbng.bng_reference.BNGReference.bng_is_neighbour` instance method for
testing whether two :class:`~osbng.bng_reference.BNGReference` objects are
neighbours.
"""
# Get the centroid of the bng square
x, y = bng_to_xy(bng_ref, position="centre")
# Initialise a neighbours list
neighbours_list = []
# Track whether we need to raise an extent warning
raise_extent_warning = False
# Iterate through N,E,S,W neighbours
for dx, dy in [[0, 1], [1, 0], [0, -1], [-1, 0]]:
try:
neighbour = xy_to_bng(
x + (dx * bng_ref.resolution_metres),
y + (dy * bng_ref.resolution_metres),
bng_ref.resolution_metres,
)
# Catch extent errors and track whether we need to warn
except BNGExtentError:
raise_extent_warning = True
else:
neighbours_list.append(neighbour)
# Raise an extent warning if an error has been caught
# Note: do this after the above, otherwise repeated warnings will be raised!
if raise_extent_warning:
warnings.warn(
"One or more of the requested grid squares falls outside of the BNG index "
+ "system extent and will not be returned."
)
return neighbours_list
[docs]
@_validate_bngreferences
def bng_is_neighbour(bng_ref1: BNGReference, bng_ref2: BNGReference) -> bool:
"""Tests whether two ``BNGReference`` objects are neighbours.
Returns True if the two :class:`~osbng.bng_reference.BNGReference` objects are
neighbours, otherwise False. Neighbours are defined as grid squares that share an
edge with the first :class:`~osbng.bng_reference.BNGReference` object.
Args:
bng_ref1 (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
bng_ref2 (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
Returns:
bool: True if the two :class:`~osbng.bng_reference.BNGReference` objects are
neighbours, otherwise False.
Raises:
TypeError: If the first or second argument is not a
:class:`~osbng.bng_reference.BNGReference` object.
BNGNeighbourError: If the two :class:`~osbng.bng_reference.BNGReference`
objects are not at the same resolution.
Examples:
>>> bng_is_neighbour(BNGReference("SE1921"), BNGReference("SE1821"))
True
>>> bng_is_neighbour(BNGReference("SE1922"), BNGReference("SE1821"))
False
>>> bng_is_neighbour(BNGReference("SU1234"), BNGReference("SU1234"))
False
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_is_neighbour`
instance method.
- The :func:`~osbng.traversal.bng_neighbours` function and
:meth:`osbng.bng_reference.BNGReference.bng_neighbours` instance method for
obtaining all neighbouring :class:`~osbng.bng_reference.BNGReference` objects
of an input :class:`~osbng.bng_reference.BNGReference` object.
"""
# Check if the two BNGReference objects are at the same resolution
if bng_ref1.resolution_metres != bng_ref2.resolution_metres:
raise BNGNeighbourError(
"The input BNGReference objects are not the same grid resolution."
"The inputBNGReference objects must be the same grid resolution."
)
# Otherwise check if the two BNGReference objects are neighbours
else:
return bng_ref2 in bng_neighbours(bng_ref1)
[docs]
@_validate_bngreferences
def bng_dwithin(bng_ref: BNGReference, d: int | float) -> list[BNGReference]:
"""Returns a list of ``BNGReference`` objects within an absolute distance ``d``.
All squares will be returned for which any part of its boundary is within distance
``d`` of any part of the input :class:`~osbng.bng_reference.BNGReference`'s
boundary.
Args:
bng_ref (BNGReference): A :class:`~osbng.bng_reference.BNGReference` object.
d (int or float): The absolute distance d in metres.
Returns:
list[BNGReference]: All grid squares which have any part of their geometry
within distance ``d`` of ``bng_ref``'s geometry
Examples:
>>> bng_dwithin(BNGReference("SU1234"), 1000)
[
BNGReference(bng_ref_formatted=SU 11 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 33, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 34, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 11 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 12 35, resolution_label=1km),
BNGReference(bng_ref_formatted=SU 13 35, resolution_label=1km)
]
>>> bng_dwithin(BNGReference("SU1234"), 1001)
[list of 21 BNGReference objects]
See Also:
- The equivalent :meth:`osbng.bng_reference.BNGReference.bng_dwithin` instance
method.
- The :func:`~osbng.traversal.bng_distance` function and
:meth:`osbng.bng_reference.BNGReference.bng_distance` instance method for
calculating the distance between two input
:class:`~osbng.bng_reference.BNGReference` objects.
"""
# Convert distance to units of k
k = int(np.ceil(d / bng_ref.resolution_metres))
# Get full kdisc
disc_refs = bng_kdisc(bng_ref, k)
# Return only those whose centroids are within distance
return [r for r in disc_refs if bng_distance(bng_ref, r, edge_to_edge=True) <= d]