189 lines
4.7 KiB
Python
189 lines
4.7 KiB
Python
import numpy as np
|
|
|
|
|
|
def _hilbert_distance(geoms, total_bounds=None, level=16):
|
|
"""
|
|
Calculate the distance along a Hilbert curve.
|
|
|
|
The distances are calculated for the midpoints of the geometries in the
|
|
GeoDataFrame.
|
|
|
|
Parameters
|
|
----------
|
|
geoms : GeometryArray
|
|
total_bounds : 4-element array
|
|
Total bounds of geometries - array
|
|
level : int (1 - 16), default 16
|
|
Determines the precision of the curve (points on the curve will
|
|
have coordinates in the range [0, 2^level - 1]).
|
|
|
|
Returns
|
|
-------
|
|
np.ndarray
|
|
Array containing distances along the Hilbert curve
|
|
|
|
"""
|
|
if geoms.is_empty.any() | geoms.isna().any():
|
|
raise ValueError(
|
|
"Hilbert distance cannot be computed on a GeoSeries with empty or "
|
|
"missing geometries.",
|
|
)
|
|
# Calculate bounds as numpy array
|
|
bounds = geoms.bounds
|
|
|
|
# Calculate discrete coords based on total bounds and bounds
|
|
x, y = _continuous_to_discrete_coords(bounds, level, total_bounds)
|
|
# Compute distance along hilbert curve
|
|
distances = _encode(level, x, y)
|
|
|
|
return distances
|
|
|
|
|
|
def _continuous_to_discrete_coords(bounds, level, total_bounds):
|
|
"""
|
|
Calculates mid points & ranges of geoms and returns
|
|
as discrete coords
|
|
|
|
Parameters
|
|
----------
|
|
|
|
bounds : Bounds of each geometry - array
|
|
|
|
p : The number of iterations used in constructing the Hilbert curve
|
|
|
|
total_bounds : Total bounds of geometries - array
|
|
|
|
Returns
|
|
-------
|
|
Discrete two-dimensional numpy array
|
|
Two-dimensional array Array of hilbert distances for each geom
|
|
|
|
"""
|
|
# Hilbert Side length
|
|
side_length = (2**level) - 1
|
|
|
|
# Calculate mid points for x and y bound coords - returns array
|
|
x_mids = (bounds[:, 0] + bounds[:, 2]) / 2.0
|
|
y_mids = (bounds[:, 1] + bounds[:, 3]) / 2.0
|
|
|
|
# Calculate x and y range of total bound coords - returns array
|
|
if total_bounds is None:
|
|
total_bounds = (
|
|
np.nanmin(x_mids),
|
|
np.nanmin(y_mids),
|
|
np.nanmax(x_mids),
|
|
np.nanmax(y_mids),
|
|
)
|
|
|
|
xmin, ymin, xmax, ymax = total_bounds
|
|
|
|
# Transform continuous value to discrete integer for each dimension
|
|
x_int = _continuous_to_discrete(x_mids, (xmin, xmax), side_length)
|
|
y_int = _continuous_to_discrete(y_mids, (ymin, ymax), side_length)
|
|
|
|
return x_int, y_int
|
|
|
|
|
|
def _continuous_to_discrete(vals, val_range, n):
|
|
"""
|
|
Convert a continuous one-dimensional array to discrete integer values
|
|
based their ranges
|
|
|
|
Parameters
|
|
----------
|
|
vals : Array of continuous values
|
|
|
|
val_range : Tuple containing range of continuous values
|
|
|
|
n : Number of discrete values
|
|
|
|
Returns
|
|
-------
|
|
One-dimensional array of discrete ints
|
|
|
|
"""
|
|
width = val_range[1] - val_range[0]
|
|
if width == 0:
|
|
return np.zeros_like(vals, dtype=np.uint32)
|
|
res = (vals - val_range[0]) * (n / width)
|
|
|
|
np.clip(res, 0, n, out=res)
|
|
return res.astype(np.uint32)
|
|
|
|
|
|
# Fast Hilbert curve algorithm by http://threadlocalmutex.com/
|
|
# From C++ https://github.com/rawrunprotected/hilbert_curves
|
|
# (public domain)
|
|
|
|
|
|
MAX_LEVEL = 16
|
|
|
|
|
|
def _interleave(x):
|
|
x = (x | (x << 8)) & 0x00FF00FF
|
|
x = (x | (x << 4)) & 0x0F0F0F0F
|
|
x = (x | (x << 2)) & 0x33333333
|
|
x = (x | (x << 1)) & 0x55555555
|
|
return x
|
|
|
|
|
|
def _encode(level, x, y):
|
|
x = np.asarray(x, dtype="uint32")
|
|
y = np.asarray(y, dtype="uint32")
|
|
|
|
if level > MAX_LEVEL:
|
|
raise ValueError("Level out of range")
|
|
|
|
x = x << (16 - level)
|
|
y = y << (16 - level)
|
|
|
|
# Initial prefix scan round, prime with x and y
|
|
a = x ^ y
|
|
b = 0xFFFF ^ a
|
|
c = 0xFFFF ^ (x | y)
|
|
d = x & (y ^ 0xFFFF)
|
|
|
|
A = a | (b >> 1)
|
|
B = (a >> 1) ^ a
|
|
C = ((c >> 1) ^ (b & (d >> 1))) ^ c
|
|
D = ((a & (c >> 1)) ^ (d >> 1)) ^ d
|
|
|
|
a = A.copy()
|
|
b = B.copy()
|
|
c = C.copy()
|
|
d = D.copy()
|
|
|
|
A = (a & (a >> 2)) ^ (b & (b >> 2))
|
|
B = (a & (b >> 2)) ^ (b & ((a ^ b) >> 2))
|
|
C ^= (a & (c >> 2)) ^ (b & (d >> 2))
|
|
D ^= (b & (c >> 2)) ^ ((a ^ b) & (d >> 2))
|
|
|
|
a = A.copy()
|
|
b = B.copy()
|
|
c = C.copy()
|
|
d = D.copy()
|
|
|
|
A = (a & (a >> 4)) ^ (b & (b >> 4))
|
|
B = (a & (b >> 4)) ^ (b & ((a ^ b) >> 4))
|
|
C ^= (a & (c >> 4)) ^ (b & (d >> 4))
|
|
D ^= (b & (c >> 4)) ^ ((a ^ b) & (d >> 4))
|
|
|
|
# Final round and projection
|
|
a = A.copy()
|
|
b = B.copy()
|
|
c = C.copy()
|
|
d = D.copy()
|
|
|
|
C ^= (a & (c >> 8)) ^ (b & (d >> 8))
|
|
D ^= (b & (c >> 8)) ^ ((a ^ b) & (d >> 8))
|
|
|
|
# Undo transformation prefix scan
|
|
a = C ^ (C >> 1)
|
|
b = D ^ (D >> 1)
|
|
|
|
# Recover index bits
|
|
i0 = x ^ y
|
|
i1 = b | (0xFFFF ^ (i0 | a))
|
|
|
|
return ((_interleave(i1) << 1) | _interleave(i0)) >> (32 - 2 * level)
|