From e655269143db2a7266d2c88d66a2b0677d906b6c Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 28 Mar 2024 10:50:09 -0400 Subject: [PATCH] PolygonLayer (#330) It looks like this is now working! image This does have the caveat that picking currently only works for _polygons_ and not their associated exteriors. See https://github.com/geoarrow/deck.gl-layers/pull/114 Todo: - [x] Use PolygonLayer as the default polygon rendering in `viz` - [x] Render stroke by default in `viz`. - [ ] Get input from designers on good default colors for the Polygon exterior - [x] Document difference between the two polygon layers - [x] Use published @geoarrow/deck.gl-layers beta, instead of local install. Closes https://github.com/developmentseed/lonboard/issues/197 ----- Old: It turns out that there are still some issues on the JS side of the PolygonLayer. I think I had only tested it with Polygon, not MultiPolygon, input, which is why I didn't catch these two. More info in https://github.com/geoarrow/deck.gl-layers/issues/102 --- README.md | 2 +- docs/api/layers/polygon-layer.md | 5 + lonboard/__init__.py | 1 + lonboard/_layer.py | 263 ++++++++++++++++++++++++++++++- lonboard/_viz.py | 80 +++++++--- lonboard/types/layer.py | 18 +++ mkdocs.yml | 1 + package-lock.json | 26 +-- package.json | 2 +- src/model/layer.ts | 113 ++++++++++++- tests/test_viz.py | 12 +- 11 files changed, 467 insertions(+), 56 deletions(-) create mode 100644 docs/api/layers/polygon-layer.md diff --git a/README.md b/README.md index a173aabc..c8ab668c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ gdf = gpd.GeoDataFrame(...) viz(gdf) ``` -Under the hood, this delegates to a [`ScatterplotLayer`](https://developmentseed.org/lonboard/latest/api/layers/scatterplot-layer/), [`PathLayer`](https://developmentseed.org/lonboard/latest/api/layers/path-layer/), or [`SolidPolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/solid-polygon-layer/). Refer to the [documentation](https://developmentseed.org/lonboard/) and [examples](https://developmentseed.org/lonboard/latest/examples/internet-speeds/) for more control over rendering. +Under the hood, this delegates to a [`ScatterplotLayer`](https://developmentseed.org/lonboard/latest/api/layers/scatterplot-layer/), [`PathLayer`](https://developmentseed.org/lonboard/latest/api/layers/path-layer/), or [`PolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/polygon-layer/). Refer to the [documentation](https://developmentseed.org/lonboard/) and [examples](https://developmentseed.org/lonboard/latest/examples/internet-speeds/) for more control over rendering. ## Documentation diff --git a/docs/api/layers/polygon-layer.md b/docs/api/layers/polygon-layer.md new file mode 100644 index 00000000..aceff289 --- /dev/null +++ b/docs/api/layers/polygon-layer.md @@ -0,0 +1,5 @@ +# PolygonLayer + +::: lonboard.PolygonLayer + options: + inherited_members: true diff --git a/lonboard/__init__.py b/lonboard/__init__.py index 70122287..e0669d1a 100644 --- a/lonboard/__init__.py +++ b/lonboard/__init__.py @@ -11,6 +11,7 @@ HeatmapLayer, PathLayer, PointCloudLayer, + PolygonLayer, ScatterplotLayer, SolidPolygonLayer, ) diff --git a/lonboard/_layer.py b/lonboard/_layer.py index 30d83862..1476e991 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -47,6 +47,7 @@ HeatmapLayerKwargs, PathLayerKwargs, PointCloudLayerKwargs, + PolygonLayerKwargs, ScatterplotLayerKwargs, SolidPolygonLayerKwargs, ) @@ -606,6 +607,249 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs): """ +class PolygonLayer(BaseArrowLayer): + """The `PolygonLayer` renders filled, stroked and/or extruded polygons. + + !!! note + + This layer is essentially a combination of a [`PathLayer`][lonboard.PathLayer] + and a [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]. This has some overhead + beyond a `SolidPolygonLayer`, so if you're looking for the maximum performance + with large data, you may want to use a `SolidPolygonLayer` directly. + + **Example:** + + From GeoPandas: + + ```py + import geopandas as gpd + from lonboard import Map, PolygonLayer + + # A GeoDataFrame with Polygon or MultiPolygon geometries + gdf = gpd.GeoDataFrame() + layer = PolygonLayer.from_geopandas( + gdf, + get_fill_color=[255, 0, 0], + get_line_color=[0, 100, 100, 150], + ) + m = Map(layer) + ``` + + From [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest): + + ```py + from geoarrow.rust.core import read_parquet + from lonboard import Map, PolygonLayer + + # Example: A GeoParquet file with Polygon or MultiPolygon geometries + table = read_parquet("path/to/file.parquet") + layer = PolygonLayer( + table=table, + get_fill_color=[255, 0, 0], + get_line_color=[0, 100, 100, 150], + ) + m = Map(layer) + ``` + """ + + def __init__( + self, + *, + table: pa.Table, + _rows_per_chunk: Optional[int] = None, + **kwargs: Unpack[PolygonLayerKwargs], + ): + super().__init__(table=table, _rows_per_chunk=_rows_per_chunk, **kwargs) + + @classmethod + def from_geopandas( + cls, + gdf: gpd.GeoDataFrame, + *, + auto_downcast: bool = True, + **kwargs: Unpack[PolygonLayerKwargs], + ) -> Self: + return super().from_geopandas(gdf=gdf, auto_downcast=auto_downcast, **kwargs) + + _layer_type = traitlets.Unicode("polygon").tag(sync=True) + + table = PyarrowTableTrait( + allowed_geometry_types={EXTENSION_NAME.POLYGON, EXTENSION_NAME.MULTIPOLYGON} + ) + """A GeoArrow table with a Polygon or MultiPolygon column. + + This is the fastest way to plot data from an existing GeoArrow source, such as + [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest) or + [geoarrow-pyarrow](https://geoarrow.github.io/geoarrow-python/main/index.html). + + If you have a GeoPandas `GeoDataFrame`, use + [`from_geopandas`][lonboard.PolygonLayer.from_geopandas] instead. + """ + + stroked = traitlets.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 = traitlets.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 = traitlets.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 = traitlets.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 = traitlets.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 = traitlets.Unicode(None, allow_none=True).tag(sync=True) + """ + The units of the line 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 = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The line 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 = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The minimum line width in pixels. This can be used to prevent the line from getting + too small when zoomed out. + + - Type: `float`, optional + - Default: `0` + """ + + line_width_max_pixels = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) + """ + The maximum line width in pixels. This can be used to prevent the line from getting + too big when zoomed in. + + - Type: `float`, optional + - Default: `None` + """ + + line_joint_rounded = traitlets.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 = traitlets.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 line 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 `extruded=True`. + + - Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional + - If a single `list` or `tuple` is provided, it is used as the line color for + all polygons. + - If a numpy or pyarrow array is provided, each value in the array will be used + as the line 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 ScatterplotLayer(BaseArrowLayer): """The `ScatterplotLayer` renders circles at given coordinates. @@ -1115,6 +1359,13 @@ class SolidPolygonLayer(BaseArrowLayer): """ The `SolidPolygonLayer` renders filled and/or extruded polygons. + !!! note + + This layer is similar to the [`PolygonLayer`][lonboard.PolygonLayer] but will + not render an outline around polygons. In most cases, you'll want to use the + `PolygonLayer` directly, but for very large datasets not drawing the outline can + significantly improve performance, in which case you may want to use this layer. + **Example:** From GeoPandas: @@ -1210,6 +1461,12 @@ def from_geopandas( - 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 = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) @@ -1220,12 +1477,6 @@ def from_geopandas( - Type: `float`, optional - Default: `1` - - **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. """ get_elevation = FloatAccessor(None, allow_none=True) diff --git a/lonboard/_viz.py b/lonboard/_viz.py index 9bcb14fd..6a10fa54 100644 --- a/lonboard/_viz.py +++ b/lonboard/_viz.py @@ -28,14 +28,14 @@ from lonboard._geoarrow.geopandas_interop import geopandas_to_geoarrow from lonboard._geoarrow.parse_wkb import parse_wkb_table from lonboard._geoarrow.sanitize import remove_extension_classes -from lonboard._layer import PathLayer, ScatterplotLayer, SolidPolygonLayer +from lonboard._layer import PathLayer, PolygonLayer, ScatterplotLayer from lonboard._map import Map from lonboard._utils import get_geometry_column_index from lonboard.basemap import CartoBasemap from lonboard.types.layer import ( PathLayerKwargs, + PolygonLayerKwargs, ScatterplotLayerKwargs, - SolidPolygonLayerKwargs, ) from lonboard.types.map import MapKwargs @@ -84,6 +84,7 @@ def __arrow_c_stream__( "#FFFF66", # yellow "#00FFFF", # turquoise ] +DEFAULT_POLYGON_LINE_COLOR = [0, 0, 0, 200] def viz( @@ -91,7 +92,7 @@ def viz( *, scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, - solid_polygon_kwargs: Optional[SolidPolygonLayerKwargs] = None, + polygon_kwargs: Optional[PolygonLayerKwargs] = None, map_kwargs: Optional[MapKwargs] = None, ) -> Map: """A high-level function to plot your data easily. @@ -120,8 +121,8 @@ def viz( [`ScatterplotLayer`][lonboard.ScatterplotLayer]s. path_kwargs: a `dict` of parameters to pass down to all generated [`PathLayer`][lonboard.PathLayer]s. - solid_polygon_kwargs: a `dict` of parameters to pass down to all generated - [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]s. + polygon_kwargs: a `dict` of parameters to pass down to all generated + [`PolygonLayer`][lonboard.PolygonLayer]s. map_kwargs: a `dict` of parameters to pass down to the generated [`Map`][lonboard.Map]. @@ -140,7 +141,7 @@ def viz( _viz_color=color_ordering[i % len(color_ordering)], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, - solid_polygon_kwargs=solid_polygon_kwargs, + polygon_kwargs=polygon_kwargs, ) for i, item in enumerate(data) ] @@ -151,7 +152,7 @@ def viz( _viz_color=color_ordering[0], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, - solid_polygon_kwargs=solid_polygon_kwargs, + polygon_kwargs=polygon_kwargs, ) ] @@ -165,7 +166,7 @@ def viz( def create_layer_from_data_input( data: VizDataInput, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: # geopandas GeoDataFrame if ( data.__class__.__module__.startswith("geopandas") @@ -232,14 +233,14 @@ def create_layer_from_data_input( def _viz_geopandas_geodataframe( data: gpd.GeoDataFrame, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: table = geopandas_to_geoarrow(data) return _viz_geoarrow_table(table, **kwargs) def _viz_geopandas_geoseries( data: gpd.GeoSeries, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: import geopandas as gpd gdf = gpd.GeoDataFrame(geometry=data) @@ -249,13 +250,13 @@ def _viz_geopandas_geoseries( def _viz_shapely_scalar( data: shapely.geometry.base.BaseGeometry, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: return _viz_shapely_array(np.array([data]), **kwargs) def _viz_shapely_array( data: NDArray[np.object_], **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: # TODO: pass include_z? field, geom_arr = construct_geometry_array(data) schema = pa.schema([field]) @@ -265,7 +266,7 @@ def _viz_shapely_array( def _viz_geo_interface( data: dict, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: if data["type"] in [ "Point", "LineString", @@ -309,7 +310,7 @@ def _viz_geo_interface( def _viz_geoarrow_array( data: ArrowArrayExportable, **kwargs, -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: schema_capsule, array_capsule = data.__arrow_c_array__() # If the user doesn't have pyarrow extension types registered for geoarrow types, @@ -355,11 +356,11 @@ def __arrow_c_array__(self, requested_schema): def _viz_geoarrow_table( table: pa.Table, *, - _viz_color: Optional[str] = None, + _viz_color: str, scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, - solid_polygon_kwargs: Optional[SolidPolygonLayerKwargs] = None, -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: + polygon_kwargs: Optional[PolygonLayerKwargs] = None, +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: table = remove_extension_classes(table) table = parse_wkb_table(table) @@ -383,6 +384,14 @@ def _viz_geoarrow_table( else: scatterplot_kwargs["radius_min_pixels"] = 0.2 + if "opacity" not in scatterplot_kwargs.keys(): + if len(table) <= 10_000: + scatterplot_kwargs["opacity"] = 0.9 + elif len(table) <= 100_000: + scatterplot_kwargs["opacity"] = 0.7 + elif len(table) <= 1_000_000: + scatterplot_kwargs["opacity"] = 0.5 + return ScatterplotLayer(table=table, **scatterplot_kwargs) elif geometry_ext_type in [ @@ -404,17 +413,42 @@ def _viz_geoarrow_table( else: path_kwargs["width_min_pixels"] = 0.5 + if "opacity" not in path_kwargs.keys(): + if len(table) <= 1_000: + path_kwargs["opacity"] = 0.9 + elif len(table) <= 10_000: + path_kwargs["opacity"] = 0.7 + elif len(table) <= 100_000: + path_kwargs["opacity"] = 0.5 + return PathLayer(table=table, **path_kwargs) elif geometry_ext_type in [EXTENSION_NAME.POLYGON, EXTENSION_NAME.MULTIPOLYGON]: - solid_polygon_kwargs = {} if not solid_polygon_kwargs else solid_polygon_kwargs + polygon_kwargs = {} if not polygon_kwargs else polygon_kwargs + + if "get_fill_color" not in polygon_kwargs.keys(): + polygon_kwargs["get_fill_color"] = _viz_color - if "get_fill_color" not in solid_polygon_kwargs.keys(): - solid_polygon_kwargs["get_fill_color"] = _viz_color + if "get_line_color" not in polygon_kwargs.keys(): + polygon_kwargs["get_line_color"] = DEFAULT_POLYGON_LINE_COLOR - if "opacity" not in solid_polygon_kwargs.keys(): - solid_polygon_kwargs["opacity"] = 0.6 + if "opacity" not in polygon_kwargs.keys(): + polygon_kwargs["opacity"] = 0.5 + + if "line_width_min_pixels" not in polygon_kwargs.keys(): + if len(table) <= 100: + polygon_kwargs["line_width_min_pixels"] = 0.5 + if len(table) <= 1_000: + polygon_kwargs["line_width_min_pixels"] = 0.45 + if len(table) <= 5_000: + polygon_kwargs["line_width_min_pixels"] = 0.4 + elif len(table) <= 10_000: + polygon_kwargs["line_width_min_pixels"] = 0.3 + elif len(table) <= 100_000: + polygon_kwargs["line_width_min_pixels"] = 0.25 + else: + polygon_kwargs["line_width_min_pixels"] = 0.2 - return SolidPolygonLayer(table=table, **solid_polygon_kwargs) + return PolygonLayer(table=table, **polygon_kwargs) raise ValueError(f"Unsupported extension type: '{geometry_ext_type}'.") diff --git a/lonboard/types/layer.py b/lonboard/types/layer.py index 2417c496..49ec5190 100644 --- a/lonboard/types/layer.py +++ b/lonboard/types/layer.py @@ -131,6 +131,24 @@ class PointCloudLayerKwargs(BaseLayerKwargs, total=False): get_normal: NormalAccessorInput +class PolygonLayerKwargs(BaseLayerKwargs, total=False): + stroked: bool + filled: bool + extruded: bool + wireframe: bool + elevation_scale: IntFloat + line_width_units: Units + line_width_scale: IntFloat + line_width_min_pixels: IntFloat + line_width_max_pixels: IntFloat + line_joint_rounded: bool + line_miter_limit: IntFloat + get_fill_color: ColorAccessorInput + get_line_color: ColorAccessorInput + get_line_width: FloatAccessorInput + get_elevation: FloatAccessorInput + + class ScatterplotLayerKwargs(BaseLayerKwargs, total=False): radius_units: Units radius_scale: IntFloat diff --git a/mkdocs.yml b/mkdocs.yml index 997b7b31..609eb1b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ nav: - api/layers/heatmap-layer.md - api/layers/path-layer.md - api/layers/point-cloud-layer.md + - api/layers/polygon-layer.md - api/layers/scatterplot-layer.md - api/layers/solid-polygon-layer.md - api/layers/base-layer.md diff --git a/package-lock.json b/package-lock.json index 0d03ff73..8cd91403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@deck.gl/extensions": "^8.9.35", "@deck.gl/layers": "^8.9.35", "@deck.gl/react": "^8.9.35", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.15", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.16", "apache-arrow": "^15.0.2", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", @@ -596,9 +596,9 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.3.0-beta.15", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.15.tgz", - "integrity": "sha512-SQhPbSll7Ac5r+Cpiz2dp+mKEoZPQlL2t6g9AQn+rwRZFdNSdTgWjBE9iL4yG08E+dr/jRJCzKJNyEPKZUEyBQ==", + "version": "0.3.0-beta.16", + "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.16.tgz", + "integrity": "sha512-KvBlMhmcoHYvrP/YpXv9F9A4OZ5jjO3FoRSdX4bZM+z+4bN2xj6ZBfE4Wfp47cS6Q3dotUACO+VwY5dZDixl6A==", "dependencies": { "@geoarrow/geoarrow-js": "^0.3.0", "threads": "^1.7.0" @@ -1591,9 +1591,9 @@ "dev": true }, "node_modules/@swc/helpers": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", - "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", "dependencies": { "tslib": "^2.4.0" } @@ -1669,9 +1669,9 @@ "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==" }, "node_modules/@types/command-line-usage": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.2.tgz", - "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==" + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==" }, "node_modules/@types/estree": { "version": "1.0.5", @@ -1726,9 +1726,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "version": "20.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", + "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", "dependencies": { "undici-types": "~5.26.4" } diff --git a/package.json b/package.json index d9823935..7b9fecba 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@deck.gl/extensions": "^8.9.35", "@deck.gl/layers": "^8.9.35", "@deck.gl/react": "^8.9.35", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.15", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.16", "apache-arrow": "^15.0.2", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", diff --git a/src/model/layer.ts b/src/model/layer.ts index 0ab3c02e..b08e4e5a 100644 --- a/src/model/layer.ts +++ b/src/model/layer.ts @@ -1,19 +1,23 @@ import { GeoArrowArcLayer, - GeoArrowArcLayerProps, GeoArrowColumnLayer, - GeoArrowColumnLayerProps, GeoArrowHeatmapLayer, - GeoArrowHeatmapLayerProps, GeoArrowPathLayer, - GeoArrowPathLayerProps, + GeoArrowPolygonLayer, GeoArrowPointCloudLayer, GeoArrowPointCloudLayerProps, GeoArrowScatterplotLayer, - GeoArrowScatterplotLayerProps, GeoArrowSolidPolygonLayer, - GeoArrowSolidPolygonLayerProps, _GeoArrowTextLayer as GeoArrowTextLayer, +} from "@geoarrow/deck.gl-layers"; +import type { + GeoArrowArcLayerProps, + GeoArrowColumnLayerProps, + GeoArrowHeatmapLayerProps, + GeoArrowPathLayerProps, + GeoArrowPolygonLayerProps, + GeoArrowScatterplotLayerProps, + GeoArrowSolidPolygonLayerProps, _GeoArrowTextLayerProps as GeoArrowTextLayerProps, } from "@geoarrow/deck.gl-layers"; import type { WidgetModel } from "@jupyter-widgets/base"; @@ -532,6 +536,99 @@ export class PointCloudModel extends BaseArrowLayerModel { } } +export class PolygonModel extends BaseArrowLayerModel { + static layerType = "polygon"; + + protected stroked: GeoArrowPolygonLayerProps["stroked"] | null; + protected filled: GeoArrowPolygonLayerProps["filled"] | null; + protected extruded: GeoArrowPolygonLayerProps["extruded"] | null; + protected wireframe: GeoArrowPolygonLayerProps["wireframe"] | null; + protected elevationScale: GeoArrowPolygonLayerProps["elevationScale"] | null; + protected lineWidthUnits: GeoArrowPolygonLayerProps["lineWidthUnits"] | null; + protected lineWidthScale: GeoArrowPolygonLayerProps["lineWidthScale"] | null; + protected lineWidthMinPixels: + | GeoArrowPolygonLayerProps["lineWidthMinPixels"] + | null; + protected lineWidthMaxPixels: + | GeoArrowPolygonLayerProps["lineWidthMaxPixels"] + | null; + protected lineJointRounded: + | GeoArrowPolygonLayerProps["lineJointRounded"] + | null; + protected lineMiterLimit: GeoArrowPolygonLayerProps["lineMiterLimit"] | null; + + protected getFillColor: GeoArrowPolygonLayerProps["getFillColor"] | null; + protected getLineColor: GeoArrowPolygonLayerProps["getLineColor"] | null; + protected getLineWidth: GeoArrowPolygonLayerProps["getLineWidth"] | null; + protected getElevation: GeoArrowPolygonLayerProps["getElevation"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("stroked", "stroked"); + this.initRegularAttribute("filled", "filled"); + this.initRegularAttribute("extruded", "extruded"); + this.initRegularAttribute("wireframe", "wireframe"); + this.initRegularAttribute("elevation_scale", "elevationScale"); + this.initRegularAttribute("line_width_units", "lineWidthUnits"); + this.initRegularAttribute("line_width_scale", "lineWidthScale"); + this.initRegularAttribute("line_width_min_pixels", "lineWidthMinPixels"); + this.initRegularAttribute("line_width_max_pixels", "lineWidthMaxPixels"); + this.initRegularAttribute("line_joint_rounded", "lineJointRounded"); + this.initRegularAttribute("line_miter_limit", "lineMiterLimit"); + + this.initVectorizedAccessor("get_fill_color", "getFillColor"); + this.initVectorizedAccessor("get_line_color", "getLineColor"); + this.initVectorizedAccessor("get_line_width", "getLineWidth"); + this.initVectorizedAccessor("get_elevation", "getElevation"); + } + + layerProps(): Omit { + console.log("table", this.table); + console.log("filled", this.filled); + + return { + data: this.table, + ...(isDefined(this.stroked) && { stroked: this.stroked }), + ...(isDefined(this.filled) && { filled: this.filled }), + ...(isDefined(this.extruded) && { extruded: this.extruded }), + ...(isDefined(this.wireframe) && { wireframe: this.wireframe }), + ...(isDefined(this.elevationScale) && { + elevationScale: this.elevationScale, + }), + ...(isDefined(this.lineWidthUnits) && { + lineWidthUnits: this.lineWidthUnits, + }), + ...(isDefined(this.lineWidthScale) && { + lineWidthScale: this.lineWidthScale, + }), + ...(isDefined(this.lineWidthMinPixels) && { + lineWidthMinPixels: this.lineWidthMinPixels, + }), + ...(isDefined(this.lineWidthMaxPixels) && { + lineWidthMaxPixels: this.lineWidthMaxPixels, + }), + ...(isDefined(this.lineJointRounded) && { + lineJointRounded: this.lineJointRounded, + }), + ...(isDefined(this.lineMiterLimit) && { + lineMiterLimit: this.lineMiterLimit, + }), + ...(isDefined(this.getFillColor) && { getFillColor: this.getFillColor }), + ...(isDefined(this.getLineColor) && { getLineColor: this.getLineColor }), + ...(isDefined(this.getLineWidth) && { getLineWidth: this.getLineWidth }), + ...(isDefined(this.getElevation) && { getElevation: this.getElevation }), + }; + } + + render(): GeoArrowPolygonLayer { + return new GeoArrowPolygonLayer({ + ...this.baseLayerProps(), + ...this.layerProps(), + }); + } +} + export class ScatterplotModel extends BaseArrowLayerModel { static layerType = "scatterplot"; @@ -846,6 +943,10 @@ export async function initializeLayer( layerModel = new PointCloudModel(model, updateStateCallback); break; + case PolygonModel.layerType: + layerModel = new PolygonModel(model, updateStateCallback); + break; + case ScatterplotModel.layerType: layerModel = new ScatterplotModel(model, updateStateCallback); break; diff --git a/tests/test_viz.py b/tests/test_viz.py index 9a547b04..a3f58777 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -3,14 +3,14 @@ from geoarrow.rust.core import read_pyogrio from pyogrio.raw import read_arrow -from lonboard import SolidPolygonLayer, viz +from lonboard import PolygonLayer, viz def test_viz_wkb_pyarrow(): path = geodatasets.get_path("naturalearth.land") meta, table = read_arrow(path) map_ = viz(table) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_reproject(): @@ -37,19 +37,19 @@ def __geo_interface__(self): geo_interface_obj = GeoInterfaceHolder(gdf.geometry[0]) map_ = viz(geo_interface_obj) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_table(): table = read_pyogrio(geodatasets.get_path("naturalearth.land")) map_ = viz(table) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_array(): table = read_pyogrio(geodatasets.get_path("naturalearth.land")) map_ = viz(table.geometry.chunk(0)) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_wkb_array(): @@ -57,4 +57,4 @@ def test_viz_geoarrow_rust_wkb_array(): arr = table.geometry.chunk(0) wkb_arr = arr.to_wkb() map_ = viz(wkb_arr) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer)