diff --git a/quickstarts/lonboard.ipynb b/quickstarts/lonboard.ipynb new file mode 100644 index 0000000..58b4cba --- /dev/null +++ b/quickstarts/lonboard.ipynb @@ -0,0 +1,350 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b90cec6c", + "metadata": {}, + "source": [ + "# Visualizing Planetary Computer data with Lonboard\n", + "\n", + "This notebook walks through interactive geospatial visualization with [Lonboard](https://developmentseed.org/lonboard/). Lonboard renders large vector datasets on a GPU-accelerated WebGL map directly in Jupyter. Key benefits:\n", + "\n", + "1. **GPU rendering**: pan and zoom through *millions* of features without breaking interactivity.\n", + "2. **No tile server**: geometry streams to the browser as [Apache Arrow](https://arrow.apache.org/); there's no intermediate vector-tile service to stand up.\n", + "3. **Cloud-native vector**: read a STAC GeoParquet partition straight off Azure Blob into a `GeoDataFrame`.\n", + "4. **Composable**: stack multiple vector layers in one `Map`.\n", + "5. **Data-driven styling**: color features by an attribute and mutate the layer in place.\n", + "\n", + "We'll render [Microsoft Building Footprints](https://planetarycomputer.microsoft.com/dataset/ms-buildings) over Portland, Oregon: hundreds of thousands of polygons in a single layer.\n", + "\n", + "The companion [Lonboard tutorial](../overview/lonboard.md) has the full narrative." + ] + }, + { + "cell_type": "markdown", + "id": "a75e986e", + "metadata": {}, + "source": [ + "## Install" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2719b6d3", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:15.849722Z", + "iopub.status.busy": "2026-06-01T23:09:15.849298Z", + "iopub.status.idle": "2026-06-01T23:09:16.915041Z", + "shell.execute_reply": "2026-06-01T23:09:16.913769Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.1.2\u001b[0m\r\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpython -m pip install --upgrade pip\u001b[0m\r\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install --quiet lonboard pystac-client planetary-computer dask-geopandas adlfs" + ] + }, + { + "cell_type": "markdown", + "id": "dd178afe", + "metadata": {}, + "source": [ + "## Open the Planetary Computer STAC catalog\n", + "\n", + "`modifier=planetary_computer.sign_inplace` signs every asset as the search returns, so the GeoParquet partition can be read directly.\n", + "\n", + "**Expected result:** working `catalog` client, no output printed." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6db7f010", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:16.917910Z", + "iopub.status.busy": "2026-06-01T23:09:16.917592Z", + "iopub.status.idle": "2026-06-01T23:09:17.905144Z", + "shell.execute_reply": "2026-06-01T23:09:17.903296Z" + } + }, + "outputs": [], + "source": [ + "import pystac_client\n", + "import planetary_computer\n", + "\n", + "catalog = pystac_client.Client.open(\n", + " \"https://planetarycomputer.microsoft.com/api/stac/v1\",\n", + " modifier=planetary_computer.sign_inplace,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "626369d5", + "metadata": {}, + "source": [ + "## Find the building-footprints partition for Portland\n", + "\n", + "The `ms-buildings` collection is partitioned by [quadkey](https://learn.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system). Compute the zoom-9 quadkey for a Portland coordinate and fetch the STAC item whose partition covers it.\n", + "\n", + "**Expected result:** one matching item and its GeoParquet `data` asset." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2e668d74", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:17.908410Z", + "iopub.status.busy": "2026-06-01T23:09:17.908088Z", + "iopub.status.idle": "2026-06-01T23:09:19.140611Z", + "shell.execute_reply": "2026-06-01T23:09:19.138497Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "('021230223', 'UnitedStates_21230223_2023-04-25')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import math\n", + "\n", + "\n", + "def quadkey(lat, lon, zoom):\n", + " n = 2 ** zoom\n", + " x = int((lon + 180.0) / 360.0 * n)\n", + " y = int((1.0 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2.0 * n)\n", + " digits = []\n", + " for i in range(zoom, 0, -1):\n", + " bit = 1 << (i - 1)\n", + " digits.append(str((1 if x & bit else 0) + (2 if y & bit else 0)))\n", + " return \"\".join(digits)\n", + "\n", + "\n", + "qk = quadkey(45.52, -122.66, 9)\n", + "item = next(catalog.search(\n", + " collections=[\"ms-buildings\"],\n", + " query={\n", + " \"msbuildings:region\": {\"eq\": \"UnitedStates\"},\n", + " \"msbuildings:quadkey\": {\"eq\": int(qk)},\n", + " },\n", + ").items())\n", + "asset = item.assets[\"data\"]\n", + "qk, item.id" + ] + }, + { + "cell_type": "markdown", + "id": "1bc86af0", + "metadata": {}, + "source": [ + "## Load the footprints into a GeoDataFrame\n", + "\n", + "The asset is a Delta/Parquet partition on Azure Blob. `dask_geopandas.read_parquet` reads it with the asset's `table:storage_options` (account + SAS), then `.compute()` materializes a GeoDataFrame. Clip to the Portland metro for a focused view.\n", + "\n", + "**Expected result:** a few hundred thousand building polygons." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8078f767", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:19.143991Z", + "iopub.status.busy": "2026-06-01T23:09:19.143563Z", + "iopub.status.idle": "2026-06-01T23:09:34.562416Z", + "shell.execute_reply": "2026-06-01T23:09:34.561197Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "324600" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import dask_geopandas\n", + "\n", + "gdf = dask_geopandas.read_parquet(\n", + " asset.href,\n", + " storage_options=asset.extra_fields[\"table:storage_options\"],\n", + ").compute()\n", + "gdf = gdf.cx[-122.85:-122.45, 45.42:45.62]\n", + "\n", + "len(gdf)" + ] + }, + { + "cell_type": "markdown", + "id": "54ac8f51", + "metadata": {}, + "source": [ + "## Render the footprints\n", + "\n", + "`PolygonLayer.from_geopandas()` uploads the geometry to the GPU as Arrow. The map below is fully interactive. Pan and zoom through every building with no tile server in the loop.\n", + "\n", + "**Expected result:** an interactive map of Portland's building footprints." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fc44a442", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:34.564879Z", + "iopub.status.busy": "2026-06-01T23:09:34.564469Z", + "iopub.status.idle": "2026-06-01T23:09:35.658328Z", + "shell.execute_reply": "2026-06-01T23:09:35.657485Z" + } + }, + "outputs": [], + "source": [ + "from lonboard import Map, PolygonLayer\n", + "\n", + "layer = PolygonLayer.from_geopandas(\n", + " gdf,\n", + " get_fill_color=[255, 140, 0, 170],\n", + " get_line_color=[90, 40, 0],\n", + " line_width_min_pixels=0.5,\n", + ")\n", + "m = Map(layer, view_state={\"longitude\": -122.66, \"latitude\": 45.52, \"zoom\": 12})\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "90f0fe81", + "metadata": {}, + "source": [ + "## Color by building height\n", + "\n", + "Each footprint carries a `meanHeight`. Map it through a continuous colormap to shade every polygon: data-driven styling across the whole layer, evaluated on the GPU.\n", + "\n", + "**Expected result:** the same footprints, now colored by height (`plasma`: purple = low, yellow = tall)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "08ea2dc5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:35.752527Z", + "iopub.status.busy": "2026-06-01T23:09:35.752195Z", + "iopub.status.idle": "2026-06-01T23:09:35.932090Z", + "shell.execute_reply": "2026-06-01T23:09:35.930838Z" + } + }, + "outputs": [], + "source": [ + "import matplotlib as mpl\n", + "from lonboard.colormap import apply_continuous_cmap\n", + "\n", + "heights = gdf[\"meanHeight\"].clip(0, 30)\n", + "normalized = (heights - heights.min()) / (heights.max() - heights.min())\n", + "\n", + "layer.get_fill_color = apply_continuous_cmap(\n", + " normalized.to_numpy(), mpl.colormaps[\"plasma\"], alpha=0.8\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "49ee64a4", + "metadata": {}, + "source": [ + "## Mutate in place\n", + "\n", + "Changing a layer property updates the existing map without re-uploading geometry.\n", + "\n", + "**Expected result:** the rendered footprints redraw at 50% opacity." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dc9d1f2a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-01T23:09:35.934563Z", + "iopub.status.busy": "2026-06-01T23:09:35.934163Z", + "iopub.status.idle": "2026-06-01T23:09:35.938547Z", + "shell.execute_reply": "2026-06-01T23:09:35.937594Z" + } + }, + "outputs": [], + "source": [ + "layer.opacity = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "7c643f03", + "metadata": {}, + "source": [ + "## You're done\n", + "\n", + "If every cell above rendered a map, the stack is wired up end-to-end: STAC search → cloud GeoParquet → Arrow upload → GPU-rendered vector → data-driven styling, all with no tile server.\n", + "\n", + "Swap in your own bbox, collection, or `GeoDataFrame` and the same pattern applies. For pixel-level *raster* analysis (window reads, overview traversal), see the [async-geotiff tutorial](../overview/async-geotiff.md). For a standalone web app rather than a notebook, the [deck.gl-raster tutorial](../overview/deckgl-raster.md) builds a raster renderer in TypeScript." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}