diff --git a/lonboard/__init__.py b/lonboard/__init__.py index 2b59fe8e..894fe46a 100644 --- a/lonboard/__init__.py +++ b/lonboard/__init__.py @@ -7,6 +7,7 @@ BitmapLayer, BitmapTileLayer, ColumnLayer, + H3HexagonLayer, HeatmapLayer, PathLayer, PointCloudLayer, diff --git a/lonboard/_h3/__init__.py b/lonboard/_h3/__init__.py new file mode 100644 index 00000000..5aa804ab --- /dev/null +++ b/lonboard/_h3/__init__.py @@ -0,0 +1,3 @@ +from ._h3_to_str import h3_to_str +from ._str_to_h3 import str_to_h3 +from ._validate_h3_cell import validate_h3_indices diff --git a/lonboard/_h3/_h3_to_str.py b/lonboard/_h3/_h3_to_str.py new file mode 100644 index 00000000..438ed96c --- /dev/null +++ b/lonboard/_h3/_h3_to_str.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +if TYPE_CHECKING: + from numpy.typing import NDArray + + +def h3_to_str(h3_indices: NDArray[np.uint64]) -> NDArray[np.str_]: + """Convert an array of H3 indices (uint64) to their hexadecimal string representations. + + Returns a numpy array of type S15 (fixed-length ASCII strings of length 15). + """ + # Ensure input is a numpy array of uint64 + hex_chars = np.empty((h3_indices.size, 15), dtype="S1") + + # Prepare hex digits lookup + hex_digits = np.array(list("0123456789ABCDEF"), dtype="S1") + + # Fill each digit + for i in range(15): + shift = (15 - 1 - i) * 4 + hex_chars[:, i] = hex_digits[(h3_indices >> shift) & 0xF] + + return hex_chars.view(" NDArray[np.uint64]: + return np.array([int(h, 16) for h in hex_arr]) + + +# # Convert ASCII bytes to numeric nibble values +# vals = np.frombuffer(hex_arr, dtype=np.uint8).reshape(len(hex_arr), -1) + +# # Convert ASCII hex chars to 0-15 +# nibbles = (vals - ord("0")).astype(np.int8) +# nibbles[nibbles > 9] -= 39 # 'a' - '0' = 49, adjust so 'a'→10, 'f'→15 + +# # Accumulate nibbles into integer +# # Each hex digit represents 4 bits +# ints = nibbles[:, 0] << 60 +# for i in range(1, 16): +# ints |= nibbles[:, i].astype(np.uint64) << (60 - 4 * i) + +# pass + + +# print(ints) +# # [10, 255, 3735928559] + + +# pass +# # Example 2D S1 array of hex chars (shape: n x 15) +# arr_s1 = np.array([list(b"00000000000000A"), list(b"0000000000000FF")], dtype="S1") + +# n_rows, n_cols = arr_s1.shape +# assert n_cols == 15, "Each hex string must be exactly 15 characters" + +# # Step 1: convert ASCII bytes to numeric values 0-15 +# arr_int = arr_s1.view("uint8") # get ASCII code +# # '0'-'9' -> 0-9, 'A'-'F' -> 10-15 +# arr_val = arr_int.copy() +# arr_val = np.where(arr_val >= ord("0"), arr_val - ord("0"), arr_val) +# arr_val = np.where(arr_val >= 10, arr_val - 7, arr_val) # adjust 'A'-'F' + +# # Step 2: create powers of 16 for each position +# powers = 16 ** np.arange(n_cols - 1, -1, -1, dtype=np.uint64) + +# # Step 3: compute dot product along each row +# uint64_arr = np.dot(arr_val, powers) + +# print(uint64_arr) +# # Output: [10 255] diff --git a/lonboard/_h3/_validate_h3_cell.py b/lonboard/_h3/_validate_h3_cell.py new file mode 100644 index 00000000..8954f221 --- /dev/null +++ b/lonboard/_h3/_validate_h3_cell.py @@ -0,0 +1,219 @@ +"""Implement h3 cell validation in pure numpy. + +It's hard to surface errors from deck.gl back to Python, so it's a bad user experience +if the JS console errors and silently nothing renders. But also I don't want to depend +on the h3 library for this because the h3 library isn't vectorized (arghhhh!) and I +don't want to require the dependency. + +So instead, I spend my time porting code into Numpy 😄. + +Ported from Rust code in h3o: + +https://github.com/HydroniumLabs/h3o/blob/07dcb85d9cb539f685ec63050ef0954b1d9f3864/src/index/cell.rs#L1897-L1962 +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +if TYPE_CHECKING: + from numpy.typing import NDArray + +__all__ = ["validate_h3_indices"] + +MODE_OFFSET = 59 +"""Offset (in bits) of the mode in an H3 index.""" + +MODE_MASK = 0b1111 << MODE_OFFSET + +EDGE_OFFSET = 56 +"""Offset (in bits) of the cell edge in an H3 index.""" + +EDGE_MASK = 0b111 << EDGE_OFFSET + +VERTEX_OFFSET = 56 +"""Offset (in bits) of the cell vertex in an H3 index.""" + +VERTEX_MASK = 0b111 << VERTEX_OFFSET + +DIRECTIONS_MASK = 0x0000_1FFF_FFFF_FFFF +"""Bitmask to select the directions bits in an H3 index.""" + +INDEX_MODE_CELL = 1 +"""H3 index mode for cells.""" + +BASE_CELL_OFFSET = 45 +"""Offset (in bits) of the base cell in an H3 index.""" + +BASE_CELL_MASK = 0b111_1111 << BASE_CELL_OFFSET +"""Bitmask to select the base cell bits in an H3 index.""" + +MAX_BASE_CELL = 121 +"""Maximum value for a base cell.""" + +RESOLUTION_OFFSET = 52 +"""The bit offset of the resolution in an H3 index.""" + +RESOLUTION_MASK = 0b1111 << RESOLUTION_OFFSET +"""Bitmask to select the resolution bits in an H3 index.""" + +MAX_RESOLUTION = 15 +"""Maximum supported H3 resolution.""" + +DIRECTION_BITSIZE = 3 +"""Size, in bits, of a direction (range [0; 6]).""" + +BASE_PENTAGONS_HI = 0x0020_0802_0008_0100 +"""Bitmap where a bit's position represents a base cell value (high part). + +Refactored from upstream 128 bit integer +https://github.com/HydroniumLabs/h3o/blob/3b40550291a57552117c48c19841557a3b0431e1/src/base_cell.rs#L12 +""" + +BASE_PENTAGONS_LO = 0x8402_0040_0100_4010 +"""Bitmap where a bit's position represents a base cell value (low part). + +Refactored from upstream 128 bit integer +https://github.com/HydroniumLabs/h3o/blob/3b40550291a57552117c48c19841557a3b0431e1/src/base_cell.rs#L12 +""" + +PENTAGON_BASE_CELLS = np.array( + [4, 14, 24, 33, 38, 49, 58, 63, 72, 83, 97, 107], + dtype=np.uint8, +) +"""Set of pentagon base cells.""" + + +def validate_h3_indices(h3_indices: NDArray[np.uint64]) -> None: + """Validate an array of uint64 H3 indices. + + Raises ValueError if any index is invalid. + """ + invalid_reserved_bits = h3_indices >> 56 & 0b1000_0111 != 0 + bad_indices = np.where(invalid_reserved_bits)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Tainted reserved bits in indices: {bad_indices.tolist()}\n" + f"with values {h3_indices[bad_indices].tolist()}", + ) + + invalid_mode = get_mode(h3_indices) != INDEX_MODE_CELL + bad_indices = np.where(invalid_mode)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Invalid index mode in indices: {bad_indices.tolist()}", + f"with values {h3_indices[bad_indices].tolist()}", + ) + + base = get_base_cell(h3_indices) + invalid_base_cell = base > MAX_BASE_CELL + bad_indices = np.where(invalid_base_cell)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Invalid base cell in indices: {bad_indices.tolist()}", + f"with values {h3_indices[bad_indices].tolist()}", + ) + + # Resolution is always valid: coded on 4 bits, valid range is [0; 15]. + resolution = get_resolution(h3_indices) + + # Check that we have a tail of unused cells after `resolution` cells. + # + # We expect every bit to be 1 in the tail (because unused cells are + # represented by `0b111`), i.e. every bit set to 0 after a NOT. + unused_count = MAX_RESOLUTION - resolution + unused_bitsize = unused_count * DIRECTION_BITSIZE + unused_mask = (1 << unused_bitsize.astype(np.uint64)) - 1 + invalid_unused_direction_pattern = (~h3_indices) & unused_mask != 0 + bad_indices = np.where(invalid_unused_direction_pattern)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Invalid unused direction pattern in indices: {bad_indices.tolist()}", + f"with values {h3_indices[bad_indices].tolist()}", + ) + + # Check that we have `resolution` valid cells (no unused ones). + dirs_mask = (1 << (resolution * DIRECTION_BITSIZE).astype(np.uint64)) - 1 + dirs = (h3_indices >> unused_bitsize) & dirs_mask + invalid_unused_direction = has_unused_direction(dirs) + bad_indices = np.where(invalid_unused_direction)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Unexpected unused direction in indices: {bad_indices.tolist()}", + f"with values {h3_indices[bad_indices].tolist()}", + ) + + # Check for pentagons with deleted subsequence. + has_pentagon_base = np.logical_and(is_pentagon(base), resolution != 0) + pentagon_base_indices = np.where(has_pentagon_base)[0] + if len(pentagon_base_indices) > 0: + pentagons = h3_indices[pentagon_base_indices] + pentagon_resolutions = resolution[pentagon_base_indices] + pentagon_dirs = dirs[pentagon_base_indices] + + # Move directions to the front, so that we can count leading zeroes. + pentagon_offset = 64 - (pentagon_resolutions * DIRECTION_BITSIZE) + + # NOTE: The following was ported via GPT from Rust `leading_zeros` + # https://github.com/HydroniumLabs/h3o/blob/07dcb85d9cb539f685ec63050ef0954b1d9f3864/src/index/cell.rs#L1951 + + # Find the position of the first bit set, if it's a multiple of 3 + # that means we have a K axe as the first non-center direction, + # which is forbidden. + shifted = pentagon_dirs << pentagon_offset + + # Compute leading zeros for each element (assuming 64-bit unsigned integers) + # where `leading_zeros = 64 - shifted.bit_length()` + # numpy doesn't have bit_length, so use log2 and handle zeros + bitlen = np.where(shifted == 0, 0, np.floor(np.log2(shifted)).astype(int) + 1) + leading_zeros = 64 - bitlen + + # Add 1 and check if multiple of 3 + is_multiple_of_3 = ((leading_zeros + 1) % 3) == 0 + bad_indices = np.where(is_multiple_of_3)[0] + if len(bad_indices) > 0: + raise ValueError( + f"Pentagonal cell index with a deleted subsequence: {bad_indices.tolist()}", + f"with values {pentagons[bad_indices].tolist()}", + ) + + +def get_mode(bits: NDArray[np.uint64]) -> NDArray[np.uint8]: + """Return the H3 index mode bits.""" + return ((bits & MODE_MASK) >> MODE_OFFSET).astype(np.uint8) + + +def get_base_cell(bits: NDArray[np.uint64]) -> NDArray[np.uint8]: + """Return the H3 index base cell bits.""" + return ((bits & BASE_CELL_MASK) >> BASE_CELL_OFFSET).astype(np.uint8) + + +def get_resolution(bits: NDArray[np.uint64]) -> NDArray[np.uint8]: + """Return the H3 index resolution.""" + return ((bits & RESOLUTION_MASK) >> RESOLUTION_OFFSET).astype(np.uint8) + + +def has_unused_direction(dirs: NDArray) -> NDArray[np.bool_]: + """Check if there is at least one unused direction in the given directions. + + Copied from upstream + https://github.com/HydroniumLabs/h3o/blob/07dcb85d9cb539f685ec63050ef0954b1d9f3864/src/index/cell.rs#L2056-L2107 + """ + LO_MAGIC = 0b001_001_001_001_001_001_001_001_001_001_001_001_001_001_001 # noqa: N806 + HI_MAGIC = 0b100_100_100_100_100_100_100_100_100_100_100_100_100_100_100 # noqa: N806 + + return ((~dirs - LO_MAGIC) & (dirs & HI_MAGIC)) != 0 + + +def is_pentagon(cell: NDArray[np.uint8]) -> NDArray[np.bool_]: + """Return true if the base cell is pentagonal. + + Note that this is **not** copied from the upstream: + https://github.com/HydroniumLabs/h3o/blob/3b40550291a57552117c48c19841557a3b0431e1/src/base_cell.rs#L33-L47 + + Because they use a 128 bit integer as a bitmap, which is not available in + numpy. Instead we use a simple lookup in a static array. + """ + return np.isin(cell, PENTAGON_BASE_CELLS) diff --git a/lonboard/_layer.py b/lonboard/_layer.py index cc3453c2..8a7a9754 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -39,6 +39,7 @@ ColorAccessor, FloatAccessor, NormalAccessor, + TextAccessor, VariableLengthTuple, ) @@ -1989,6 +1990,246 @@ def from_duckdb( """ +class H3HexagonLayer(BaseArrowLayer): + """The `H3HexagonLayer` renders H3 hexagons. + + **Example:** + + From GeoPandas: + + ```py + import geopandas as gpd + from lonboard import Map, H3HexagonLayer + + # A GeoDataFrame with Polygon or MultiPolygon geometries + gdf = gpd.GeoDataFrame() + layer = H3HexagonLayer.from_geopandas( + gdf, + get_fill_color=[255, 0, 0], + ) + m = Map(layer) + ``` + + From an Arrow-compatible source like [pyogrio][pyogrio] or [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest): + + ```py + from geoarrow.rust.io import read_flatgeobuf + from lonboard import Map, H3HexagonLayer + + # Example: A FlatGeobuf file with Polygon or MultiPolygon geometries + table = read_flatgeobuf("path/to/file.fgb") + layer = H3HexagonLayer( + table, + get_fill_color=[255, 0, 0], + ) + m = Map(layer) + ``` + """ + + def __init__( + self, + table: ArrowStreamExportable, + *, + get_hexagon=None, + _rows_per_chunk: int | None = None, + **kwargs: Unpack[H3HexagonLayerKwargs], + ) -> None: + super().__init__( + table=table, + get_hexagon=get_hexagon, + _rows_per_chunk=_rows_per_chunk, + **kwargs, + ) + + @classmethod + def from_geopandas( + cls, + gdf: gpd.GeoDataFrame, + *, + get_hexagon=None, + auto_downcast: bool = True, + **kwargs: Unpack[H3HexagonLayerKwargs], + ) -> Self: + return super().from_geopandas( + gdf=gdf, + get_hexagon=get_hexagon, + auto_downcast=auto_downcast, + **kwargs, + ) + + _layer_type = t.Unicode("h3-hexagon").tag(sync=True) + + table = ArrowTableTrait(geometry_required=False) + + # TODO: create custom H3 accessor that validates hexagons and converts them to + # uint64 + get_hexagon = TextAccessor() + """Label text accessor""" + + stroked = t.Bool(None, allow_none=True).tag(sync=True) + """Whether to draw an outline around the polygon (solid fill). + + Note that both the outer polygon as well the outlines of any holes will be drawn. + + - Type: `bool`, optional + - Default: `True` + """ + + filled = t.Bool(None, allow_none=True).tag(sync=True) + """Whether to draw a filled polygon (solid fill). + + Note that only the area between the outer polygon and any holes will be filled. + + - Type: `bool`, optional + - Default: `True` + """ + + extruded = t.Bool(None, allow_none=True).tag(sync=True) + """Whether to extrude the polygons. + + Based on the elevations provided by the `getElevation` accessor. + + If set to `false`, all polygons will be flat, this generates less geometry and is + faster than simply returning 0 from getElevation. + + - Type: `bool`, optional + - Default: `False` + """ + + wireframe = t.Bool(None, allow_none=True).tag(sync=True) + """ + Whether to generate a line wireframe of the polygon. The outline will have + "horizontal" lines closing the top and bottom polygons and a vertical line + (a "strut") for each vertex on the polygon. + + - Type: `bool`, optional + - Default: `False` + + **Remarks:** + + - These lines are rendered with `GL.LINE` and will thus always be 1 pixel wide. + - Wireframe and solid extrusions are exclusive, you'll need to create two layers + with the same data if you want a combined rendering effect. + """ + + elevation_scale = t.Float(None, allow_none=True, min=0).tag(sync=True) + """Elevation multiplier. + + The final elevation is calculated by `elevationScale * getElevation(d)`. + `elevationScale` is a handy property to scale all elevation without updating the + data. + + - Type: `float`, optional + - Default: `1` + """ + + line_width_units = t.Unicode(None, allow_none=True).tag(sync=True) + """ + The units of the outline width, one of `'meters'`, `'common'`, and `'pixels'`. See + [unit + system](https://deck.gl/docs/developer-guide/coordinate-systems#supported-units). + + - Type: `str`, optional + - Default: `'meters'` + """ + + line_width_scale = t.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The outline width multiplier that multiplied to all outlines of `Polygon` and + `MultiPolygon` features if the `stroked` attribute is true. + + - Type: `float`, optional + - Default: `1` + """ + + line_width_min_pixels = t.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The minimum outline width in pixels. This can be used to prevent the outline from + getting too small when zoomed out. + + - Type: `float`, optional + - Default: `0` + """ + + line_width_max_pixels = t.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The maximum outline width in pixels. This can be used to prevent the outline from + getting too big when zoomed in. + + - Type: `float`, optional + - Default: `None` + """ + + line_joint_rounded = t.Bool(None, allow_none=True).tag(sync=True) + """Type of joint. If `true`, draw round joints. Otherwise draw miter joints. + + - Type: `bool`, optional + - Default: `False` + """ + + line_miter_limit = t.Float(None, allow_none=True, min=0).tag(sync=True) + """The maximum extent of a joint in ratio to the stroke width. + + Only works if `line_joint_rounded` is false. + + - Type: `float`, optional + - Default: `4` + """ + + get_fill_color = ColorAccessor(None, allow_none=True) + """ + The fill color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a + number between 0-255 and `a` is 255 if not supplied. + + - Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional + - If a single `list` or `tuple` is provided, it is used as the fill color for + all polygons. + - If a numpy or pyarrow array is provided, each value in the array will be used + as the fill color for the polygon at the same row index. + - Default: `[0, 0, 0, 255]`. + """ + + get_line_color = ColorAccessor(None, allow_none=True) + """ + The outline color of each polygon in the format of `[r, g, b, [a]]`. Each channel is + a number between 0-255 and `a` is 255 if not supplied. + + Only applies if `stroked=True`. + + - Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional + - If a single `list` or `tuple` is provided, it is used as the outline color for + all polygons. + - If a numpy or pyarrow array is provided, each value in the array will be used + as the outline color for the polygon at the same row index. + - Default: `[0, 0, 0, 255]`. + """ + + get_line_width = FloatAccessor(None, allow_none=True) + """ + The width of the outline of each polygon, in units specified by `line_width_units` + (default `'meters'`). + + - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional + - If a number is provided, it is used as the outline width for all polygons. + - If an array is provided, each value in the array will be used as the outline + width for the polygon at the same row index. + - Default: `1`. + """ + + get_elevation = FloatAccessor(None, allow_none=True) + """ + The elevation to extrude each polygon with, in meters. + + Only applies if `extruded=True`. + + - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional + - If a number is provided, it is used as the width for all polygons. + - If an array is provided, each value in the array will be used as the width for + the polygon at the same row index. + - Default: `1000`. + """ + + class HeatmapLayer(BaseArrowLayer): """The `HeatmapLayer` visualizes the spatial distribution of data. diff --git a/package-lock.json b/package-lock.json index 158ad258..f0f1fd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@deck.gl/layers": "^9.2.1", "@deck.gl/mapbox": "^9.2.1", "@deck.gl/react": "^9.2.1", - "@geoarrow/deck.gl-layers": "^0.4.0-beta.1", + "@geoarrow/deck.gl-layers": "^0.4.0-beta.3", "@geoarrow/geoarrow-js": "^0.3.2", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", @@ -1286,6 +1286,7 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.1.tgz", "integrity": "sha512-YAb8WZ1Htne9w7yKNw0DsD9fVtHY4GiCTLib+F0nQC9yNBP1xhA6l3myPf/ZfCxVkJLJ58cjEoqcgGMmQ4EvaQ==", + "license": "MIT", "peer": true, "dependencies": { "@luma.gl/constants": "^9.2.2", @@ -1346,6 +1347,7 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/@deck.gl/geo-layers/-/geo-layers-9.2.1.tgz", "integrity": "sha512-Cq7TZa15c2+XE7uci65AqPSOY/yDicmPjYJxNX/DgvMkgGOYlEO9TmBu8CCqNaBCxJlK9Ztz6mwoKZbltUBGQw==", + "license": "MIT", "peer": true, "dependencies": { "@loaders.gl/3d-tiles": "^4.2.0", @@ -1418,6 +1420,7 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.1.tgz", "integrity": "sha512-uSr0HgkFO8A7g6CDhEvWPieoZQiLUVGmumqVUXj1FqzbiKI5yBsbt839xMxS2ZKZWBdZI7FUp3G+NML1KBGdxw==", + "license": "MIT", "peer": true, "dependencies": { "@loaders.gl/gltf": "^4.2.0", @@ -2060,19 +2063,19 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.4.0-beta.1", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.4.0-beta.1.tgz", - "integrity": "sha512-SYS2DTWjWMPEfDphfPGPuRORIXg9Nwk1XiHmMrsw2pQzlufE9yVLWAQ4ppQXTpjDl5jqUhRbu3jiNOlTePpWpA==", + "version": "0.4.0-beta.3", + "resolved": "file:../../geoarrow/deck.gl-layers/packages/deck.gl-layers/geoarrow-deck.gl-layers-0.4.0-beta.3.tgz", + "integrity": "sha512-n137V/qf6tpg/g/Uw39Cf9hKFwDREBnb+G4DERux+XzgrIyPW3ny/tUCsMOY1XNCWd81eg7D/+v11hnjt4xh4w==", "license": "MIT", "dependencies": { "@geoarrow/geoarrow-js": "^0.3.0", "threads": "^1.7.0" }, "peerDependencies": { - "@deck.gl/aggregation-layers": "^9.2.1", - "@deck.gl/core": "^9.2.1", - "@deck.gl/geo-layers": "^9.2.1", - "@deck.gl/layers": "^9.2.1", + "@deck.gl/aggregation-layers": "^9.0.0", + "@deck.gl/core": "^9.0.0", + "@deck.gl/geo-layers": "^9.0.0", + "@deck.gl/layers": "^9.0.0", "@math.gl/polygon": "^4.1.0", "apache-arrow": ">=15" } @@ -2328,6 +2331,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-4.3.4.tgz", "integrity": "sha512-JQ3y3p/KlZP7lfobwON5t7H9WinXEYTvuo3SRQM8TBKhM+koEYZhvI2GwzoXx54MbBbY+s3fm1dq5UAAmaTsZw==", + "license": "MIT", "dependencies": { "@loaders.gl/compression": "4.3.4", "@loaders.gl/crypto": "4.3.4", @@ -2351,12 +2355,14 @@ "node_modules/@loaders.gl/3d-tiles/node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/@loaders.gl/compression": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/compression/-/compression-4.3.4.tgz", "integrity": "sha512-+o+5JqL9Sx8UCwdc2MTtjQiUHYQGJALHbYY/3CT+b9g/Emzwzez2Ggk9U9waRfdHiBCzEgRBivpWZEOAtkimXQ==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/worker-utils": "4.3.4", @@ -2392,6 +2398,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/crypto/-/crypto-4.3.4.tgz", "integrity": "sha512-3VS5FgB44nLOlAB9Q82VOQnT1IltwfRa1miE0mpHCe1prYu1M/dMnEyynusbrsp+eDs3EKbxpguIS9HUsFu5dQ==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/worker-utils": "4.3.4", @@ -2405,6 +2412,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/draco/-/draco-4.3.4.tgz", "integrity": "sha512-4Lx0rKmYENGspvcgV5XDpFD9o+NamXoazSSl9Oa3pjVVjo+HJuzCgrxTQYD/3JvRrolW/QRehZeWD/L/cEC6mw==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2419,6 +2427,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/gis/-/gis-4.3.4.tgz", "integrity": "sha512-8xub38lSWW7+ZXWuUcggk7agRHJUy6RdipLNKZ90eE0ZzLNGDstGD1qiBwkvqH0AkG+uz4B7Kkiptyl7w2Oa6g==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2434,6 +2443,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/gltf/-/gltf-4.3.4.tgz", "integrity": "sha512-EiUTiLGMfukLd9W98wMpKmw+hVRhQ0dJ37wdlXK98XPeGGB+zTQxCcQY+/BaMhsSpYt/OOJleHhTfwNr8RgzRg==", + "license": "MIT", "dependencies": { "@loaders.gl/draco": "4.3.4", "@loaders.gl/images": "4.3.4", @@ -2461,6 +2471,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.4.tgz", "integrity": "sha512-tjMZvlKQSaMl2qmYTAxg+ySR6zd6hQn5n3XaU8+Ehp90TD3WzxvDKOMNDqOa72fFmIV+KgPhcmIJTpq4lAdC4Q==", + "license": "MIT", "dependencies": { "@loaders.gl/schema": "4.3.4", "@loaders.gl/worker-utils": "4.3.4", @@ -2475,6 +2486,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/math/-/math-4.3.4.tgz", "integrity": "sha512-UJrlHys1fp9EUO4UMnqTCqvKvUjJVCbYZ2qAKD7tdGzHJYT8w/nsP7f/ZOYFc//JlfC3nq+5ogvmdpq2pyu3TA==", + "license": "MIT", "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2488,6 +2500,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/mvt/-/mvt-4.3.4.tgz", "integrity": "sha512-9DrJX8RQf14htNtxsPIYvTso5dUce9WaJCWCIY/79KYE80Be6dhcEYMknxBS4w3+PAuImaAe66S5xo9B7Erm5A==", + "license": "MIT", "dependencies": { "@loaders.gl/gis": "4.3.4", "@loaders.gl/images": "4.3.4", @@ -2505,6 +2518,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.4.tgz", "integrity": "sha512-1YTYoatgzr/6JTxqBLwDiD3AVGwQZheYiQwAimWdRBVB0JAzych7s1yBuE0CVEzj4JDPKOzVAz8KnU1TiBvJGw==", + "license": "MIT", "dependencies": { "@types/geojson": "^7946.0.7" }, @@ -2516,6 +2530,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/terrain/-/terrain-4.3.4.tgz", "integrity": "sha512-JszbRJGnxL5Fh82uA2U8HgjlsIpzYoCNNjy3cFsgCaxi4/dvjz3BkLlBilR7JlbX8Ka+zlb4GAbDDChiXLMJ/g==", + "license": "MIT", "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2530,6 +2545,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/textures/-/textures-4.3.4.tgz", "integrity": "sha512-arWIDjlE7JaDS6v9by7juLfxPGGnjT9JjleaXx3wq/PTp+psLOpGUywHXm38BNECos3MFEQK3/GFShWI+/dWPw==", + "license": "MIT", "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2547,6 +2563,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/tiles/-/tiles-4.3.4.tgz", "integrity": "sha512-oC0zJfyvGox6Ag9ABF8fxOkx9yEFVyzTa9ryHXl2BqLiQoR1v3p+0tIJcEbh5cnzHfoTZzUis1TEAZluPRsHBQ==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/math": "4.3.4", @@ -2564,6 +2581,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/wms/-/wms-4.3.4.tgz", "integrity": "sha512-yXF0wuYzJUdzAJQrhLIua6DnjOiBJusaY1j8gpvuH1VYs3mzvWlIRuZKeUd9mduQZKK88H2IzHZbj2RGOauq4w==", + "license": "MIT", "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2580,6 +2598,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.4.tgz", "integrity": "sha512-EbsszrASgT85GH3B7jkx7YXfQyIYo/rlobwMx6V3ewETapPUwdSAInv+89flnk5n2eu2Lpdeh+2zS6PvqbL2RA==", + "license": "MIT", "peerDependencies": { "@loaders.gl/core": "^4.3.0" } @@ -2588,6 +2607,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/xml/-/xml-4.3.4.tgz", "integrity": "sha512-p+y/KskajsvyM3a01BwUgjons/j/dUhniqd5y1p6keLOuwoHlY/TfTKd+XluqfyP14vFrdAHCZTnFCWLblN10w==", + "license": "MIT", "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2601,6 +2621,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/zip/-/zip-4.3.4.tgz", "integrity": "sha512-bHY4XdKYJm3vl9087GMoxnUqSURwTxPPh6DlAGOmz6X9Mp3JyWuA2gk3tQ1UIuInfjXKph3WAUfGe6XRIs1sfw==", + "license": "MIT", "dependencies": { "@loaders.gl/compression": "4.3.4", "@loaders.gl/crypto": "4.3.4", @@ -2651,6 +2672,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/@luma.gl/gltf/-/gltf-9.2.2.tgz", "integrity": "sha512-OL5dZlgq99QZCmOAPq4J3wlEmVkDbBdpXlzUukyHqI/1UV1jonb6e41JykXn1WuOYcrtYM8PnygG3MQ8afRjvA==", + "license": "MIT", "dependencies": { "@loaders.gl/core": "^4.2.0", "@loaders.gl/gltf": "^4.2.0", @@ -2849,12 +2871,14 @@ "node_modules/@mapbox/martini": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@mapbox/martini/-/martini-0.2.0.tgz", - "integrity": "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==" + "integrity": "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==", + "license": "ISC" }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { "version": "2.0.7", @@ -2870,6 +2894,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "~0.1.0" } @@ -2953,6 +2978,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/@math.gl/culling/-/culling-4.1.0.tgz", "integrity": "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==", + "license": "MIT", "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" @@ -2962,6 +2988,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/@math.gl/geospatial/-/geospatial-4.1.0.tgz", "integrity": "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==", + "license": "MIT", "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" @@ -6953,6 +6980,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-5.1.5.tgz", "integrity": "sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==", + "license": "MIT", "dependencies": { "@turf/helpers": "^5.1.5", "@turf/invariant": "^5.1.5" @@ -6962,6 +6990,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz", "integrity": "sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg==", + "license": "MIT", "dependencies": { "@turf/helpers": "^5.1.5" } @@ -6969,12 +6998,14 @@ "node_modules/@turf/helpers": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", - "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==" + "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", + "license": "MIT" }, "node_modules/@turf/invariant": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", + "license": "MIT", "dependencies": { "@turf/helpers": "^5.1.5" } @@ -6983,6 +7014,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", "integrity": "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==", + "license": "MIT", "dependencies": { "@turf/helpers": "^5.1.5" } @@ -6991,6 +7023,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-5.1.5.tgz", "integrity": "sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw==", + "license": "MIT", "dependencies": { "@turf/boolean-clockwise": "^5.1.5", "@turf/clone": "^5.1.5", @@ -7013,6 +7046,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz", "integrity": "sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7030,7 +7064,8 @@ "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", - "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==" + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -7104,7 +7139,8 @@ "node_modules/@types/pako": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", - "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==" + "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==", + "license": "MIT" }, "node_modules/@types/react": { "version": "19.1.13", @@ -7592,6 +7628,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/a5-js/-/a5-js-0.5.0.tgz", "integrity": "sha512-VAw19sWdYadhdovb0ViOIi1SdKx6H6LwcGMRFKwMfgL5gcmL/1fKJHfgsNgNaJ7xC/eEyjs6VK+VVd4N0a+peg==", + "license": "Apache-2.0", "dependencies": { "gl-matrix": "^3.4.3" } @@ -7964,6 +8001,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "optional": true }, "node_modules/baseline-browser-mapping": { @@ -8011,6 +8049,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", "optional": true, "dependencies": { "base64-js": "^1.1.2" @@ -8054,6 +8093,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", "integrity": "sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8221,6 +8261,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -8420,6 +8461,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", "integrity": "sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw==", + "license": "MIT", "dependencies": { "buf-compare": "^1.0.0", "is-error": "^2.2.0" @@ -8431,7 +8473,8 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -8450,6 +8493,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -8563,7 +8607,8 @@ "node_modules/d3-hexbin": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", - "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==" + "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==", + "license": "BSD-3-Clause" }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -8656,6 +8701,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/deep-strict-equal/-/deep-strict-equal-0.2.0.tgz", "integrity": "sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==", + "license": "MIT", "dependencies": { "core-assert": "^0.2.0" }, @@ -8754,7 +8800,8 @@ "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", - "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -9402,6 +9449,7 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], + "license": "MIT", "dependencies": { "strnum": "^1.1.1" }, @@ -9420,7 +9468,8 @@ "node_modules/fflate": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -9823,6 +9872,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.3.0.tgz", "integrity": "sha512-zgvyHZz5bEKeuyYGh0bF9/kYSxJ2SqroopkXHqKnD3lfjaZawcxulcI9nWbNC54gakl/2eObRLHWueTf1iLSaA==", + "license": "Apache-2.0", "engines": { "node": ">=4", "npm": ">=3", @@ -9943,7 +9993,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -9964,6 +10015,7 @@ "version": "0.7.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "license": "MIT", "bin": { "image-size": "bin/image-size.js" }, @@ -9974,7 +10026,8 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" }, "node_modules/immutable": { "version": "5.1.3", @@ -10009,7 +10062,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/input-otp": { "version": "1.4.1", @@ -10131,7 +10185,8 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" }, "node_modules/is-callable": { "version": "1.2.7", @@ -10195,7 +10250,8 @@ "node_modules/is-error": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz", - "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==" + "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==", + "license": "MIT" }, "node_modules/is-extendable": { "version": "0.1.1", @@ -10486,7 +10542,8 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -10677,6 +10734,7 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -10701,7 +10759,8 @@ "node_modules/ktx-parse": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz", - "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==" + "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==", + "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", @@ -10741,6 +10800,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -10822,6 +10882,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", + "license": "Apache-2.0", "engines": { "node": ">=0.6" } @@ -10853,12 +10914,14 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==", + "license": "ISC", "optional": true }, "node_modules/lzo-wasm": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/lzo-wasm/-/lzo-wasm-0.0.4.tgz", - "integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==" + "integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==", + "license": "BSD-2-Clause" }, "node_modules/magic-string": { "version": "0.30.19", @@ -10949,6 +11012,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -11349,7 +11413,8 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", @@ -11439,6 +11504,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -12488,7 +12554,8 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" }, "node_modules/proj4": { "version": "2.19.10", @@ -12647,6 +12714,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -12874,7 +12942,8 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, "node_modules/safe-identifier": { "version": "0.4.2", @@ -13385,7 +13454,8 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -13530,7 +13600,8 @@ "node_modules/snappyjs": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", - "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==" + "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", + "license": "MIT" }, "node_modules/sort-asc": { "version": "0.2.0", @@ -13609,7 +13680,8 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" }, "node_modules/stackback": { "version": "0.0.2", @@ -13640,6 +13712,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -13852,7 +13925,8 @@ "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ] + ], + "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.0", @@ -14033,6 +14107,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/texture-compressor/-/texture-compressor-1.0.2.tgz", "integrity": "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==", + "license": "MIT", "dependencies": { "argparse": "^1.0.10", "image-size": "^0.7.4" @@ -14045,6 +14120,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -15410,6 +15486,7 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/zstd-codec/-/zstd-codec-0.1.5.tgz", "integrity": "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==", + "license": "MIT", "optional": true } } diff --git a/package.json b/package.json index 031c3317..37f1312c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@deck.gl/layers": "^9.2.1", "@deck.gl/mapbox": "^9.2.1", "@deck.gl/react": "^9.2.1", - "@geoarrow/deck.gl-layers": "^0.4.0-beta.1", + "@geoarrow/deck.gl-layers": "^0.4.0-beta.3", "@geoarrow/geoarrow-js": "^0.3.2", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", @@ -22,9 +22,9 @@ "maplibre-gl": "^5.9.0", "memoize-one": "^6.0.0", "parquet-wasm": "0.7.1", + "react": "^19.2.0", "react-dom": "^19.2.0", "react-map-gl": "^8.1.0", - "react": "^19.2.0", "uuid": "^13.0.0", "xstate": "^5.22.0" }, @@ -35,26 +35,26 @@ "@jupyter-widgets/base": "^6.0.10", "@playwright/test": "^1.55.1", "@statelyai/inspect": "^0.4.0", + "@types/lodash": "^4.17.13", "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", - "@types/lodash": "^4.17.13", "@types/react": "^19.1.1", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", "dotenv": "^17.2.2", "esbuild": "^0.25.10", + "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.2", - "eslint": "^9.36.0", "globals": "^16.4.0", "nodemon": "^3.1.7", + "postcss": "^8.4.49", "postcss-modules": "^6.0.1", "postcss-preset-env": "^10.1.1", - "postcss": "^8.4.49", "prettier": "^3.4.1", "tailwindcss": "^3.4.15", - "typescript-eslint": "^8.16.0", "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", "vitest": "^2.1.6" }, "overrides": { diff --git a/pyproject.toml b/pyproject.toml index 6fe8e14d..17bae6a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ dev = [ "geoarrow-rust-core>=0.4.0", "geoarrow-rust-io>=0.4.1", "geodatasets>=2024.8.0", + "h3>=4.3.1", "jupyterlab>=4.3.3", "matplotlib>=3.7.5", "movingpandas>=0.20.0", @@ -118,11 +119,12 @@ artifacts = ["lonboard/static/*.js", "lonboard/static/*.css"] [[tool.mypy.overrides]] module = [ - "pyogrio.*", - "pyarrow.*", + "geoarrow.pyarrow.*", "geodatasets.*", + "h3.*", "ipywidgets.*", - "geoarrow.pyarrow.*", + "pyarrow.*", + "pyogrio.*", ] ignore_missing_imports = true diff --git a/src/model/layer.ts b/src/model/layer.ts index 98c8787e..a114ffb3 100644 --- a/src/model/layer.ts +++ b/src/model/layer.ts @@ -9,9 +9,11 @@ import { GeoArrowScatterplotLayer, GeoArrowSolidPolygonLayer, _GeoArrowTextLayer as GeoArrowTextLayer, + _GeoArrowH3HexagonLayer as GeoArrowH3HexagonLayer, GeoArrowTripsLayer, } from "@geoarrow/deck.gl-layers"; import type { + _GeoArrowH3HexagonLayerProps as GeoArrowH3HexagonLayerProps, GeoArrowArcLayerProps, GeoArrowColumnLayerProps, GeoArrowHeatmapLayerProps, @@ -424,6 +426,78 @@ export class ColumnModel extends BaseArrowLayerModel { } } +export class H3HexagonModel extends BaseArrowLayerModel { + static layerType = "h3-hexagon"; + + protected highPrecision?: GeoArrowH3HexagonLayerProps["highPrecision"] | null; + protected coverage?: GeoArrowH3HexagonLayerProps["coverage"] | null; + protected centerHexagon?: GeoArrowH3HexagonLayerProps["centerHexagon"] | null; + protected extruded?: GeoArrowH3HexagonLayerProps["extruded"] | null; + + protected getHexagon!: arrow.Vector; + protected getFillColor?: ColorAccessorInput | null; + protected getLineColor?: ColorAccessorInput | null; + protected getLineWidth?: FloatAccessorInput | null; + protected getElevation?: FloatAccessorInput | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("high_precision", "highPrecision"); + this.initRegularAttribute("coverage", "coverage"); + this.initRegularAttribute("center_hexagon", "centerHexagon"); + this.initRegularAttribute("extruded", "extruded"); + + this.initVectorizedAccessor("get_hexagon", "getHexagon"); + this.initVectorizedAccessor("get_fill_color", "getFillColor"); + this.initVectorizedAccessor("get_line_color", "getLineColor"); + this.initVectorizedAccessor("get_line_width", "getLineWidth"); + this.initVectorizedAccessor("get_elevation", "getElevation"); + } + + layerProps(batchIndex: number): GeoArrowH3HexagonLayerProps { + return { + id: `${this.model.model_id}-${batchIndex}`, + data: this.table.batches[batchIndex], + // Required argument + getHexagon: this.getHexagon.data[batchIndex], + ...(isDefined(this.highPrecision) && { + highPrecision: this.highPrecision, + }), + ...(isDefined(this.coverage) && { coverage: this.coverage }), + ...(isDefined(this.centerHexagon) && { + centerHexagon: this.centerHexagon, + }), + ...(isDefined(this.extruded) && { extruded: this.extruded }), + ...(isDefined(this.getFillColor) && { + getFillColor: accessColorData(this.getFillColor, batchIndex), + }), + ...(isDefined(this.getLineColor) && { + getLineColor: accessColorData(this.getLineColor, batchIndex), + }), + ...(isDefined(this.getLineWidth) && { + getLineWidth: accessFloatData(this.getLineWidth, batchIndex), + }), + ...(isDefined(this.getElevation) && { + getElevation: accessFloatData(this.getElevation, batchIndex), + }), + }; + } + + render(): GeoArrowH3HexagonLayer[] { + const layers: GeoArrowH3HexagonLayer[] = []; + for (let batchIdx = 0; batchIdx < this.table.batches.length; batchIdx++) { + layers.push( + new GeoArrowH3HexagonLayer({ + ...this.baseLayerProps(), + ...this.layerProps(batchIdx), + }), + ); + } + return layers; + } +} + export class HeatmapModel extends BaseArrowLayerModel { static layerType = "heatmap"; @@ -1171,6 +1245,10 @@ export async function initializeLayer( layerModel = new ColumnModel(model, updateStateCallback); break; + case H3HexagonModel.layerType: + layerModel = new H3HexagonModel(model, updateStateCallback); + break; + case HeatmapModel.layerType: layerModel = new HeatmapModel(model, updateStateCallback); break; diff --git a/tests/test_h3.py b/tests/test_h3.py new file mode 100644 index 00000000..5acb2d57 --- /dev/null +++ b/tests/test_h3.py @@ -0,0 +1,140 @@ +"""Vendor h3o cell index tests + +https://github.com/HydroniumLabs/h3o/blob/6918ea071cf2d65a20cbb103f32d984a01161819/tests/h3/is_valid_cell.rs +""" + +import h3.api.numpy_int as h3 +import numpy as np +import pytest + +from lonboard._h3 import validate_h3_indices + +VALID_INDICES = np.array( + [ + 0x8075FFFFFFFFFFF, + 0x81757FFFFFFFFFF, + 0x82754FFFFFFFFFF, + 0x83754EFFFFFFFFF, + 0x84754A9FFFFFFFF, + 0x85754E67FFFFFFF, + 0x86754E64FFFFFFF, + 0x87754E64DFFFFFF, + 0x88754E6499FFFFF, + 0x89754E64993FFFF, + 0x8A754E64992FFFF, + 0x8B754E649929FFF, + 0x8C754E649929DFF, + 0x8D754E64992D6FF, + 0x8E754E64992D6DF, + 0x8F754E64992D6D8, + ], + dtype=np.uint64, +) + + +def test_valid_indices(): + for cell in VALID_INDICES: + assert h3.is_valid_cell(cell) + validate_h3_indices(VALID_INDICES) + + +def test_invalid_high_bit_set(): + h3_indices = np.array([0x88C2BAE305336BFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Tainted reserved bits in indices"): + validate_h3_indices(h3_indices) + + +def test_invalid_mode(): + h3_indices = np.array([0x28C2BAE305336BFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Invalid index mode in indices"): + validate_h3_indices(h3_indices) + + +def test_tainted_reserved_bits(): + h3_indices = np.array([0xAC2BAE305336BFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Tainted reserved bits in indices"): + validate_h3_indices(h3_indices) + + +def test_invalid_base_cell(): + h3_indices = np.array([0x80FFFFFFFFFFFFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Invalid base cell in indices"): + validate_h3_indices(h3_indices) + + +def test_unexpected_unused_first(): + h3_indices = np.array([0x8C2BEE305336BFFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Tainted reserved bits in indices"): + validate_h3_indices(h3_indices) + + +def test_unexpected_unused_middle(): + h3_indices = np.array([0x8C2BAE33D336BFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Unexpected unused direction in indices"): + validate_h3_indices(h3_indices) + + +def test_unexpected_unused_last(): + h3_indices = np.array([0x8C2BAE305336FFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Unexpected unused direction in indices"): + validate_h3_indices(h3_indices) + + +def test_missing_unused_first(): + h3_indices = np.array([0x8C0FAE305336AFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Invalid unused direction pattern in indices"): + validate_h3_indices(h3_indices) + + +def test_missing_unused_middle(): + h3_indices = np.array([0x8C0FAE305336FEF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Invalid unused direction pattern in indices"): + validate_h3_indices(h3_indices) + + +def test_missing_unused_last(): + h3_indices = np.array([0x81757FFFFFFFFFE], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises(ValueError, match="Invalid unused direction pattern in indices"): + validate_h3_indices(h3_indices) + + +def test_deleted_subsequence_hexagon1(): + h3_indices = np.array([0x81887FFFFFFFFFF], dtype=np.uint64) + assert h3.is_valid_cell(h3_indices[0]) + validate_h3_indices(h3_indices) + + +def test_deleted_subsequence_pentagon1(): + h3_indices = np.array([0x81087FFFFFFFFFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises( + ValueError, + match="Pentagonal cell index with a deleted subsequence", + ): + validate_h3_indices(h3_indices) + + +def test_deleted_subsequence_hexagon2(): + h3_indices = np.array([0x8804000011FFFFF], dtype=np.uint64) + assert h3.is_valid_cell(h3_indices[0]) + validate_h3_indices(h3_indices) + + +def test_deleted_subsequence_pentagon2(): + h3_indices = np.array([0x8808000011FFFFF], dtype=np.uint64) + assert not h3.is_valid_cell(h3_indices[0]) + with pytest.raises( + ValueError, + match="Pentagonal cell index with a deleted subsequence", + ): + validate_h3_indices(h3_indices) diff --git a/uv.lock b/uv.lock index 97924ca0..1794fe35 100644 --- a/uv.lock +++ b/uv.lock @@ -1221,6 +1221,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h3" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/97/7c795fd4b7f7913cc001d73c5470ec278d705fdea7bb23b67b561e198426/h3-4.3.1.tar.gz", hash = "sha256:ecac67318538ecef1d893c019946d4cce58c1eef9349090b887ebfe8a59d4f31", size = 167964, upload-time = "2025-08-10T19:54:43.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/58/3de20e25db166f220050798c60dc58b0a44842607eb5d26ccbf01d0c3d32/h3-4.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce6033bdee8dd0179a99f818d6bdce01bf63821fc0ecee9ce5d013ec54cb1000", size = 859236, upload-time = "2025-08-10T19:53:48.859Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/2532b9ebf3deb64cb508e0ed3602cfa656f772c2bc2870724437b823cd99/h3-4.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6e0d81d60397fae0159e2f6f6a457f959f5b137b1db969e513aeeb26895798a", size = 802307, upload-time = "2025-08-10T19:53:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/5b/98/37360e0c43a0ed88ca4f057adc3218987f30d4b6c97bac70f5b31e437569/h3-4.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65662d46dc3786b961533231e5597aafe642b835a9f09d4fcb1e4081fd3161f9", size = 991394, upload-time = "2025-08-10T19:53:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/314aefe73af35adc1ee6bf757cf6fe8bb6c8d7d30df181a184ecccea136f/h3-4.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f0349364c2bcd7344880902d63fa2e3b5d9a96edbdf0d2d59d2d2e9ee65814", size = 1028103, upload-time = "2025-08-10T19:53:53.25Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/3b87a291ef209ff480a230403934e7710e01558b2a947d1a8d774d52bf78/h3-4.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e41ddbcc8d89e81a7a2b8de80d957f260802a47fe84fb12b4b1fdfacef93dea", size = 1039453, upload-time = "2025-08-10T19:53:54.689Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8b/4e626a127e85c23e492f96cf52b20b8023420ad32af94c98895dffbf2372/h3-4.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:5fc9fcf2f4a96b71b084a0a535f0a3728d43258624e0aad077304bf170f6d95c", size = 795954, upload-time = "2025-08-10T19:53:55.94Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c8/ae8aba6d2dd4c327b31339b478553fdde482e187899f79165c8e7c9ab621/h3-4.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:693f91e0819cd77b2037d7b8e8ef2b807243896a8bf9d542385067087c67b561", size = 859078, upload-time = "2025-08-10T19:53:57.136Z" }, + { url = "https://files.pythonhosted.org/packages/6f/46/68a542833bd3c0c10ffb9d9654eca76fc4e6a36a2439df61c56b9484f3f6/h3-4.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2eee0ce19777910187018d8878b2ba746a529c3cf54efa0fd1b79be95034c4b5", size = 800943, upload-time = "2025-08-10T19:53:58.587Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cc/dfe823ec29dd974449914fe59a181522b939fd7cbe0929df81310c128ef9/h3-4.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1fad090aa81eb6ac2e97cd06e3c17c2021b32afef55f202f4b733fecccfd51c", size = 994141, upload-time = "2025-08-10T19:54:00.08Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ca/e0a85dc6ac504d69cb2777e225c34c29b42f11f9d80fd70e58bbaec600da/h3-4.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd5d6893a3b81b6855c8343375f335b639de202559c69802c4739497cf0d6127", size = 1028418, upload-time = "2025-08-10T19:54:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/ff/da/8ea4dd1462b006da75b3e0d57c4f4fcd116f7c438c0ae4e74c6204f17a6a/h3-4.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e357001998db9babb4e8b23b617134819e5a2e8c3223c5b292ab05e4c36f19b0", size = 1040091, upload-time = "2025-08-10T19:54:02.419Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7d/05bcc6720fb0fb3e965deb5fd7de4c0b444935adcd32cc23c90f04d34cac/h3-4.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3b67b687f339c0bb9f656a8120dcf36714364aadb77c8641206ace9cf664850", size = 796274, upload-time = "2025-08-10T19:54:03.734Z" }, + { url = "https://files.pythonhosted.org/packages/9f/46/ddfb53cf1549808724186d3b50f77dd85d95c02e424668b8bd9b13fb85eb/h3-4.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:5961d986e77900e57b25ce0b0be362d2181bd3db9e1b8792f2b4a503f1d0857e", size = 696343, upload-time = "2025-08-10T19:54:04.91Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e4/37f37656f2f3aac6e057a4796f8acba9042ece471679d202614b2810d5fe/h3-4.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a838d17726baf573cde98338b8eba9c78f5fb0a60e076a4f038b1573c64959d", size = 865460, upload-time = "2025-08-10T19:54:06.305Z" }, + { url = "https://files.pythonhosted.org/packages/7b/63/22bca3886d0bc57e9b1b6d0385555ea69aec65f6aade0b80d89bd4db4a34/h3-4.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:63affe0b9d590e138aa3595300e8989d309480b650c3ba5f1219fa412e06147a", size = 798471, upload-time = "2025-08-10T19:54:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/4d/82/b4152f051322b098e94672a76350c4c21092e86973cd0e859dd438b8dd6c/h3-4.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:180c82873452aa3fef17b01bc1a5bea90cc508b5d7f78657566dc3e2cc5a0b87", size = 975788, upload-time = "2025-08-10T19:54:08.655Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/d5ee6d3fb0a11b754a565cf412e8b93b5543a278fa58c6e92d5e4a274be2/h3-4.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382fbc8904cdf1b606c374c32f1501c182157bb179ac2261eb5f9bf1f06613ad", size = 1017151, upload-time = "2025-08-10T19:54:10.071Z" }, + { url = "https://files.pythonhosted.org/packages/51/03/a6c392952d098e642309a6d71d68b57c9af70e9b987078cdba6474538b75/h3-4.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:089a83c3046887d762a702fa8bbd63508113ce91b702749a3ee260531da9d084", size = 1027707, upload-time = "2025-08-10T19:54:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/94/a4/d77cffe5f4e361ef7fb95ac266eb292acb88be2b8a09e2f8ddc07bda233c/h3-4.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbc9bed33915386cfa7fa29314e2cc2f7aa94384835d5e2f772c4f66133b34fa", size = 787032, upload-time = "2025-08-10T19:54:12.825Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1e/29c0e00fa57300e5b99095b572237d2914fc416cb038f9977ad69440742d/h3-4.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:87ac213063082a67698098c51e54443f465a239a11bf5e7aa4cf95071e3ea2f3", size = 698434, upload-time = "2025-08-10T19:54:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/df/2c/5194b5f27465c4ea65e1b779ac62551e2893bdfb26933d03e113310658f7/h3-4.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:836903c620114b67438cf589be108d974d72c96091e9f0d1461975114ce439a2", size = 858154, upload-time = "2025-08-10T19:54:14.773Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3c/7edad39214b7e6ec0fea4da241fda0ecdc4bc9c25d565483fe4eba89dd8f/h3-4.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f3f5ca6ac0f155a88889de788c13025f582cbde4cbc6281c472c4673784b6f54", size = 792498, upload-time = "2025-08-10T19:54:15.761Z" }, + { url = "https://files.pythonhosted.org/packages/44/bc/a11b200229591989426121590810fba43465c7bbedf3d7f235aee99e3d2d/h3-4.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a8dcb11f7b5b902521b6dd6955fe8710ab7aef33bccf21861182bc7ae02e04e", size = 971224, upload-time = "2025-08-10T19:54:16.726Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/5efd413072428bf1973c34d76a2f476a621848da86cecd8392ef59ba7640/h3-4.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b4885dd55ae5bc1848334981f1593eea098133c13bedc66bca1dac624cefe2c", size = 1013740, upload-time = "2025-08-10T19:54:17.758Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3a/93e7962d160cd2f8c4f2108a2606f7a4713edbe4dbbb39aef019bf2ebb08/h3-4.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ba48c0d64a5dc6843290441c2cf885a4912ccd861c3288e8f8be47a2d09e201", size = 1024968, upload-time = "2025-08-10T19:54:18.759Z" }, + { url = "https://files.pythonhosted.org/packages/90/bc/efe2bfc2c746bc7d092aef97ac6182c1e2f7b3759618ee55c21628fe1b80/h3-4.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:14f2012c8bbc6ec72e74ceeb46d617019bb3489c07d1c0dee516bc7765509850", size = 783789, upload-time = "2025-08-10T19:54:19.794Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1b/8ed8745a3a80a031201890cafa8a45780605005fafd3e460d75fb697fa00/h3-4.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:166fd7ecc8a384aad8272432ea482310dbe0022cb12dc84b9f8fd704a908890a", size = 695774, upload-time = "2025-08-10T19:54:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/5c/40/49b703d520189f2df139a72a9d899fec0a4382855e69241187272738887d/h3-4.3.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:adf011133912ec8346b7c9c2e396f8c4ad8395a3713d3ceac36c0fad6e66e7d5", size = 858473, upload-time = "2025-08-10T19:54:22.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/99/b2b8b3834fe1460da1f0ac5d6aaf0ae24a6c20d171605b694d364982c323/h3-4.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:348aa0a4f5899fcab48c5d0573c68b1190a3b3e574294524349428658587b7a3", size = 795377, upload-time = "2025-08-10T19:54:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3a/c85c9f345e76c4f94a0d2868052d7b52e2727fb9a16f06ba1a29265d9787/h3-4.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8b0ce8ac5b4227f511a56933f3da9587d31e4ad68e29ce82eb6214b949883fe", size = 1014140, upload-time = "2025-08-10T19:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/81/90/b498d83eba23fd428ddeeb4801f383ead0f86393607a749926363cd41b67/h3-4.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:b6e2dd52fd50546fb7be574025e7ac01c34b77c171583008cbcdbb2b9e918766", size = 799379, upload-time = "2025-08-10T19:54:25.444Z" }, + { url = "https://files.pythonhosted.org/packages/10/ff/c5bd98976a9904f314ed4edd960f115e865adc2e85205d9273bf3c8211a8/h3-4.3.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fc8a2c12d945ce840d20bbca68c2c4aae52db7b27a5c275c0171e9887ee902c0", size = 893161, upload-time = "2025-08-10T19:54:26.426Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e2/deffa7ce37527d380e11e0cdaa99065ede701d6243aa508625718aa2d1db/h3-4.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae63c00cedc0f499d83fb8eec9a84291c16ceb81d5909f75dd28fc6b0efe6d96", size = 839868, upload-time = "2025-08-10T19:54:27.414Z" }, + { url = "https://files.pythonhosted.org/packages/68/fa/956d6a8fe9cdcf761f65a732e6aa047e1c5dc6ed22d81ede868fb4d8bb83/h3-4.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aec62f93cae0e146766e6bfced32e5b0f9964aa33bf738d19be779d4a77328f0", size = 983850, upload-time = "2025-08-10T19:54:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b0/f9ae26739d77e846911a8ffebef13964116cd68df21b50e74ff5725ccd49/h3-4.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b005d38c4e91917b0b2e6053a47f07b123cc5eed794cb849a2d347b6b3888ea0", size = 893310, upload-time = "2025-08-10T19:54:29.796Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1805,6 +1848,7 @@ dev = [ { name = "geoarrow-rust-core" }, { name = "geoarrow-rust-io" }, { name = "geodatasets" }, + { name = "h3" }, { name = "jupyterlab" }, { name = "matplotlib" }, { name = "movingpandas" }, @@ -1862,6 +1906,7 @@ dev = [ { name = "geoarrow-rust-core", specifier = ">=0.4.0" }, { name = "geoarrow-rust-io", specifier = ">=0.4.1" }, { name = "geodatasets", specifier = ">=2024.8.0" }, + { name = "h3", specifier = ">=4.3.1" }, { name = "jupyterlab", specifier = ">=4.3.3" }, { name = "matplotlib", specifier = ">=3.7.5" }, { name = "movingpandas", specifier = ">=0.20.0" },