diff --git a/lonboard/__init__.py b/lonboard/__init__.py index 2e0dddc2..148ccb4f 100644 --- a/lonboard/__init__.py +++ b/lonboard/__init__.py @@ -12,10 +12,13 @@ BitmapLayer, BitmapTileLayer, ColumnLayer, + GeohashLayer, + H3HexagonLayer, HeatmapLayer, PathLayer, PointCloudLayer, PolygonLayer, + S2Layer, ScatterplotLayer, SolidPolygonLayer, TripsLayer, @@ -29,11 +32,14 @@ "BitmapLayer", "BitmapTileLayer", "ColumnLayer", + "GeohashLayer", + "H3HexagonLayer", "HeatmapLayer", "Map", "PathLayer", "PointCloudLayer", "PolygonLayer", + "S2Layer", "ScatterplotLayer", "SolidPolygonLayer", "TripsLayer", diff --git a/lonboard/layer/__init__.py b/lonboard/layer/__init__.py index 56f5a1c6..353bc836 100644 --- a/lonboard/layer/__init__.py +++ b/lonboard/layer/__init__.py @@ -12,11 +12,13 @@ from ._base import BaseArrowLayer, BaseLayer from ._bitmap import BitmapLayer, BitmapTileLayer from ._column import ColumnLayer +from ._geohash import GeohashLayer from ._h3 import H3HexagonLayer from ._heatmap import HeatmapLayer from ._path import PathLayer from ._point_cloud import PointCloudLayer from ._polygon import PolygonLayer, SolidPolygonLayer +from ._s2 import S2Layer from ._scatterplot import ScatterplotLayer from ._trips import TripsLayer @@ -28,11 +30,13 @@ "BitmapLayer", "BitmapTileLayer", "ColumnLayer", + "GeohashLayer", "H3HexagonLayer", "HeatmapLayer", "PathLayer", "PointCloudLayer", "PolygonLayer", + "S2Layer", "ScatterplotLayer", "SolidPolygonLayer", "TripsLayer", diff --git a/lonboard/layer/_a5.py b/lonboard/layer/_a5.py index 6737a8c3..72664c88 100644 --- a/lonboard/layer/_a5.py +++ b/lonboard/layer/_a5.py @@ -30,7 +30,12 @@ class A5Layer(PolygonLayer): - """The `A5Layer` renders filled and/or stroked polygons based on the [A5](https://a5geo.org) geospatial indexing system.""" + """The `A5Layer` renders filled and/or stroked polygons based on the [A5](https://a5geo.org) geospatial indexing system. + + !!! warning + This layer does not currently support auto-centering the map view based on the + extent of the input. + """ def __init__( self, diff --git a/lonboard/layer/_geohash.py b/lonboard/layer/_geohash.py new file mode 100644 index 00000000..6c9a1bda --- /dev/null +++ b/lonboard/layer/_geohash.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import traitlets as t + +from lonboard._utils import auto_downcast as _auto_downcast + +# Important to import from ._polygon to avoid circular imports +from lonboard.layer._polygon import PolygonLayer +from lonboard.traits import ArrowTableTrait, TextAccessor + +if TYPE_CHECKING: + import sys + + import pandas as pd + from arro3.core.types import ArrowStreamExportable + + from lonboard.types.layer import GeohashLayerKwargs, TextAccessorInput + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + + if sys.version_info >= (3, 12): + from typing import Unpack + else: + from typing_extensions import Unpack + + +class GeohashLayer(PolygonLayer): + """The `GeohashLayer` renders filled and/or stroked polygons based on the [Geohash](https://en.wikipedia.org/wiki/Geohash) geospatial indexing system. + + !!! warning + This layer does not currently support auto-centering the map view based on the + extent of the input. + """ + + def __init__( + self, + table: ArrowStreamExportable, + *, + get_geohash: TextAccessorInput, + _rows_per_chunk: int | None = None, + **kwargs: Unpack[GeohashLayerKwargs], + ) -> None: + """Create a new GeohashLayer. + + Args: + table: An Arrow table with properties to associate with the geohashes. + + Keyword Args: + get_geohash: The identifier of each geohash. + kwargs: Extra args passed down as GeohashLayer attributes. + + """ + super().__init__( + table=table, + get_geohash=get_geohash, + _rows_per_chunk=_rows_per_chunk, + **kwargs, + ) + + @classmethod + def from_pandas( + cls, + df: pd.DataFrame, + *, + get_geohash: TextAccessorInput, + auto_downcast: bool = True, + **kwargs: Unpack[GeohashLayerKwargs], + ) -> Self: + """Create a new GeohashLayer from a pandas DataFrame. + + Args: + df: a Pandas DataFrame with properties to associate with geohashes. + + Keyword Args: + get_geohash: geohash identifiers. + auto_downcast: Whether to save memory on input by casting to smaller types. Defaults to True. + kwargs: Extra args passed down as GeohashLayer attributes. + + """ + try: + import pyarrow as pa + except ImportError as e: + raise ImportError( + "pyarrow required for converting GeoPandas to arrow.\n" + "Run `pip install pyarrow`.", + ) from e + + if auto_downcast: + # Note: we don't deep copy because we don't need to clone geometries + df = _auto_downcast(df.copy()) # type: ignore + + table = pa.Table.from_pandas(df) + return cls(table, get_geohash=get_geohash, **kwargs) + + _layer_type = t.Unicode("geohash").tag(sync=True) + + table = ArrowTableTrait(geometry_required=False) + """An Arrow table with properties to associate with the geohashes. + + If you have a Pandas `DataFrame`, use + [`from_pandas`][lonboard.GeohashLayer.from_pandas] instead. + """ + + get_geohash = TextAccessor() + """The cell identifier of each geohash. + + Accepts either an array of strings or uint64 integers representing geohash IDs. + + - Type: [TextAccessor][lonboard.traits.TextAccessor] + """ diff --git a/lonboard/layer/_s2.py b/lonboard/layer/_s2.py new file mode 100644 index 00000000..74006626 --- /dev/null +++ b/lonboard/layer/_s2.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import traitlets as t + +from lonboard._utils import auto_downcast as _auto_downcast + +# Important to import from ._polygon to avoid circular imports +from lonboard.layer._polygon import PolygonLayer +from lonboard.traits import ArrowTableTrait, TextAccessor + +if TYPE_CHECKING: + import sys + + import pandas as pd + from arro3.core.types import ArrowStreamExportable + + from lonboard.types.layer import S2LayerKwargs, TextAccessorInput + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + + if sys.version_info >= (3, 12): + from typing import Unpack + else: + from typing_extensions import Unpack + + +class S2Layer(PolygonLayer): + """The `S2Layer` renders filled and/or stroked polygons based on the [S2](http://s2geometry.io/) geospatial indexing system. + + !!! warning + This layer does not currently support auto-centering the map view based on the + extent of the input. + """ + + def __init__( + self, + table: ArrowStreamExportable, + *, + get_s2_token: TextAccessorInput, + _rows_per_chunk: int | None = None, + **kwargs: Unpack[S2LayerKwargs], + ) -> None: + """Create a new S2Layer. + + Args: + table: An Arrow table with properties to associate with the S2 cells. + + Keyword Args: + get_s2_token: The identifier of each S2 cell. + kwargs: Extra args passed down as S2Layer attributes. + + """ + super().__init__( + table=table, + get_s2_token=get_s2_token, + _rows_per_chunk=_rows_per_chunk, + **kwargs, + ) + + @classmethod + def from_pandas( + cls, + df: pd.DataFrame, + *, + get_s2_token: TextAccessorInput, + auto_downcast: bool = True, + **kwargs: Unpack[S2LayerKwargs], + ) -> Self: + """Create a new S2Layer from a pandas DataFrame. + + Args: + df: a Pandas DataFrame with properties to associate with S2 cells. + + Keyword Args: + get_s2_token: S2 cell identifier of each S2 hexagon. + auto_downcast: Whether to save memory on input by casting to smaller types. Defaults to True. + kwargs: Extra args passed down as S2Layer attributes. + + """ + try: + import pyarrow as pa + except ImportError as e: + raise ImportError( + "pyarrow required for converting GeoPandas to arrow.\n" + "Run `pip install pyarrow`.", + ) from e + + if auto_downcast: + # Note: we don't deep copy because we don't need to clone geometries + df = _auto_downcast(df.copy()) # type: ignore + + table = pa.Table.from_pandas(df) + return cls(table, get_s2_token=get_s2_token, **kwargs) + + _layer_type = t.Unicode("s2").tag(sync=True) + + table = ArrowTableTrait(geometry_required=False) + """An Arrow table with properties to associate with the S2 cells. + + If you have a Pandas `DataFrame`, use + [`from_pandas`][lonboard.S2Layer.from_pandas] instead. + """ + + get_s2_token = TextAccessor() + """The cell identifier of each S2 cell. + + Accepts either an array of strings or uint64 integers representing S2 cell IDs. + + - Type: [TextAccessor][lonboard.traits.TextAccessor] + """ diff --git a/lonboard/types/layer.py b/lonboard/types/layer.py index c545a778..54ffe99d 100644 --- a/lonboard/types/layer.py +++ b/lonboard/types/layer.py @@ -216,6 +216,14 @@ class A5LayerKwargs(PolygonLayerKwargs, total=False): pass +class S2LayerKwargs(PolygonLayerKwargs, total=False): + pass + + +class GeohashLayerKwargs(PolygonLayerKwargs, total=False): + pass + + class ScatterplotLayerKwargs(BaseLayerKwargs, total=False): radius_units: Units radius_scale: IntFloat diff --git a/package-lock.json b/package-lock.json index 1c987bfb..68885d19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@deck.gl/mapbox": "^9.2.2", "@deck.gl/mesh-layers": "^9.2.2", "@deck.gl/react": "^9.2.2", - "@geoarrow/deck.gl-layers": "^0.4.0-beta.5", + "@geoarrow/deck.gl-layers": "^0.4.0-beta.6", "@geoarrow/geoarrow-js": "^0.3.2", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", @@ -2166,9 +2166,9 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.4.0-beta.5", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.4.0-beta.5.tgz", - "integrity": "sha512-9Loxmr7rqLlWiJBaCMt5qoO9d3+CWh0UlHQ0GIk5Zku5rvVEYYrYAQLlKVPIEkKPF0EM+qum3BgfsWfNB4jY9A==", + "version": "0.4.0-beta.6", + "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.4.0-beta.6.tgz", + "integrity": "sha512-figzvJOeH2zND7lnvCBZkSk+PvfNpyKTRmLqNdbT55Ju+hjf4CWorDbpSeQNYUnPKm+iAidx2yFFABHXzii7uA==", "license": "MIT", "dependencies": { "@geoarrow/geoarrow-js": "^0.3.0", diff --git a/package.json b/package.json index b3fe2a55..104fa3f9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@deck.gl/mapbox": "^9.2.2", "@deck.gl/mesh-layers": "^9.2.2", "@deck.gl/react": "^9.2.2", - "@geoarrow/deck.gl-layers": "^0.4.0-beta.5", + "@geoarrow/deck.gl-layers": "^0.4.0-beta.6", "@geoarrow/geoarrow-js": "^0.3.2", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", diff --git a/src/model/layer/index.ts b/src/model/layer/index.ts index 3632ccf1..0c5c62c9 100644 --- a/src/model/layer/index.ts +++ b/src/model/layer/index.ts @@ -9,8 +9,10 @@ import { PathModel } from "./path.js"; import { PointCloudModel } from "./point-cloud.js"; import { A5Model, + GeohashModel, H3HexagonModel, PolygonModel, + S2Model, SolidPolygonModel, } from "./polygon.js"; import { ScatterplotModel } from "./scatterplot.js"; @@ -62,6 +64,10 @@ export async function initializeLayer( layerModel = new ColumnModel(model, updateStateCallback); break; + case GeohashModel.layerType: + layerModel = new GeohashModel(model, updateStateCallback); + break; + case H3HexagonModel.layerType: layerModel = new H3HexagonModel(model, updateStateCallback); break; @@ -82,6 +88,10 @@ export async function initializeLayer( layerModel = new PolygonModel(model, updateStateCallback); break; + case S2Model.layerType: + layerModel = new S2Model(model, updateStateCallback); + break; + case ScatterplotModel.layerType: layerModel = new ScatterplotModel(model, updateStateCallback); break; diff --git a/src/model/layer/polygon.ts b/src/model/layer/polygon.ts index a19592db..748816a9 100644 --- a/src/model/layer/polygon.ts +++ b/src/model/layer/polygon.ts @@ -1,19 +1,22 @@ -import { - GeoArrowPolygonLayer, - GeoArrowSolidPolygonLayer, - GeoArrowA5Layer, - GeoArrowH3HexagonLayer, -} from "@geoarrow/deck.gl-layers"; import type { GeoArrowA5LayerProps, + GeoArrowGeohashLayerProps, GeoArrowH3HexagonLayerProps, GeoArrowPolygonLayerProps, + GeoArrowS2LayerProps, GeoArrowSolidPolygonLayerProps, } from "@geoarrow/deck.gl-layers"; +import { + GeoArrowA5Layer, + GeoArrowGeohashLayer, + GeoArrowH3HexagonLayer, + GeoArrowPolygonLayer, + GeoArrowS2Layer, + GeoArrowSolidPolygonLayer, +} from "@geoarrow/deck.gl-layers"; import type { WidgetModel } from "@jupyter-widgets/base"; import * as arrow from "apache-arrow"; -import { BaseArrowLayerModel } from "./base.js"; import { isDefined } from "../../util.js"; import { EARCUT_WORKER_POOL } from "../earcut-pool.js"; import { @@ -22,6 +25,7 @@ import { accessColorData, accessFloatData, } from "../types.js"; +import { BaseArrowLayerModel } from "./base.js"; export class SolidPolygonModel extends BaseArrowLayerModel { static layerType = "solid-polygon"; @@ -278,3 +282,67 @@ export class A5Model extends BasePolygonModel { return layers; } } + +export class GeohashModel extends BasePolygonModel { + static layerType = "geohash"; + + protected getGeohash!: arrow.Vector; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initVectorizedAccessor("get_geohash", "getGeohash"); + } + + layerProps(batchIndex: number): GeoArrowGeohashLayerProps { + return { + getGeohash: this.getGeohash.data[batchIndex], + ...this.basePolygonLayerProps(batchIndex), + }; + } + + render(): GeoArrowGeohashLayer[] { + const layers: GeoArrowGeohashLayer[] = []; + for (let batchIdx = 0; batchIdx < this.table.batches.length; batchIdx++) { + layers.push( + new GeoArrowGeohashLayer({ + ...this.baseLayerProps(), + ...this.layerProps(batchIdx), + }), + ); + } + return layers; + } +} + +export class S2Model extends BasePolygonModel { + static layerType = "s2"; + + protected getS2Token!: arrow.Vector; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initVectorizedAccessor("get_s2_token", "getS2Token"); + } + + layerProps(batchIndex: number): GeoArrowS2LayerProps { + return { + getS2Token: this.getS2Token.data[batchIndex], + ...this.basePolygonLayerProps(batchIndex), + }; + } + + render(): GeoArrowS2Layer[] { + const layers: GeoArrowS2Layer[] = []; + for (let batchIdx = 0; batchIdx < this.table.batches.length; batchIdx++) { + layers.push( + new GeoArrowS2Layer({ + ...this.baseLayerProps(), + ...this.layerProps(batchIdx), + }), + ); + } + return layers; + } +}