Metadata-Version: 2.4
Name: pygeospace
Version: 0.8.1
Summary: Powerful, honest, open-source geospatial visualization with true 3D.
Author: PyGeoSpace contributors
License: MIT
Project-URL: Homepage, https://github.com/pygeospace/pygeospace
Project-URL: Documentation, https://github.com/pygeospace/pygeospace#readme
Project-URL: Roadmap, https://github.com/pygeospace/pygeospace/blob/main/ROADMAP.md
Keywords: gis,geospatial,visualization,deck.gl,mapping
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: geopandas>=1.0
Requires-Dist: shapely>=2.0
Requires-Dist: pyproj>=3.5
Requires-Dist: pydeck>=0.9
Requires-Dist: matplotlib>=3.7
Requires-Dist: pillow>=10.0
Requires-Dist: scikit-learn>=1.3
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn>=0.27
Requires-Dist: click>=8.1
Requires-Dist: pydantic>=2.0
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scipy>=1.10
Requires-Dist: affine>=2.4
Requires-Dist: httpx2>=2.0
Provides-Extra: raster
Requires-Dist: rasterio>=1.3; extra == "raster"
Provides-Extra: 3d
Requires-Dist: pyvista>=0.45; extra == "3d"
Requires-Dist: meshio>=5.3; extra == "3d"
Requires-Dist: trame>=3.0; extra == "3d"
Requires-Dist: trame-vtk>=2.8; extra == "3d"
Requires-Dist: trame-vuetify>=2.7; extra == "3d"
Provides-Extra: pointcloud
Requires-Dist: laspy[lazrs]>=2.5; extra == "pointcloud"
Provides-Extra: streaming
Requires-Dist: websockets>=12.0; extra == "streaming"
Requires-Dist: paho-mqtt>=2.0; extra == "streaming"
Provides-Extra: clustering-h3
Requires-Dist: h3>=4.0; extra == "clustering-h3"
Provides-Extra: network
Requires-Dist: osmnx>=2.0; extra == "network"
Provides-Extra: threed
Requires-Dist: pyvista>=0.45; extra == "threed"
Requires-Dist: meshio>=5.3; extra == "threed"
Requires-Dist: trame>=3.0; extra == "threed"
Requires-Dist: trame-vtk>=2.8; extra == "threed"
Requires-Dist: trame-vuetify>=2.7; extra == "threed"
Provides-Extra: distributed
Requires-Dist: dask[complete]>=2024.1; extra == "distributed"
Provides-Extra: folium
Requires-Dist: folium>=0.16; extra == "folium"
Requires-Dist: branca>=0.7; extra == "folium"
Requires-Dist: jinja2>=3.0; extra == "folium"
Provides-Extra: export
Requires-Dist: selenium>=4.0; extra == "export"
Requires-Dist: pillow>=10.0; extra == "export"
Provides-Extra: classify
Requires-Dist: jenkspy>=0.4; extra == "classify"
Provides-Extra: stac
Requires-Dist: pystac-client>=0.7; extra == "stac"
Requires-Dist: planetary-computer>=1.0; extra == "stac"
Provides-Extra: overture
Requires-Dist: overturemaps>=0.6; extra == "overture"
Provides-Extra: earthengine
Requires-Dist: earthengine-api>=0.1; extra == "earthengine"
Requires-Dist: geemap>=0.30; extra == "earthengine"
Provides-Extra: osm
Requires-Dist: osmnx>=2.0; extra == "osm"
Provides-Extra: all
Requires-Dist: rasterio>=1.3; extra == "all"
Requires-Dist: laspy[lazrs]>=2.5; extra == "all"
Requires-Dist: websockets>=12.0; extra == "all"
Requires-Dist: paho-mqtt>=2.0; extra == "all"
Requires-Dist: h3>=4.0; extra == "all"
Requires-Dist: pyvista>=0.45; extra == "all"
Requires-Dist: meshio>=5.3; extra == "all"
Requires-Dist: trame>=3.0; extra == "all"
Requires-Dist: trame-vtk>=2.8; extra == "all"
Requires-Dist: trame-vuetify>=2.7; extra == "all"
Requires-Dist: folium>=0.16; extra == "all"
Requires-Dist: branca>=0.7; extra == "all"
Requires-Dist: jinja2>=3.0; extra == "all"
Requires-Dist: jenkspy>=0.4; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: nbclient>=0.9; extra == "dev"
Requires-Dist: nbformat>=5.9; extra == "dev"
Requires-Dist: ipykernel>=6.0; extra == "dev"
Dynamic: license-file

# PyGeoSpace

**Powerful, honest, open-source geospatial visualization for Python.**

PyGeoSpace reads the common vector and raster formats, renders **interactive 2D
maps, true 3D scenes, and an interactive 3D globe**, runs spatial + spectral
analytics, and exports interactive HTML, static images, or real 3D model files
(glTF / GLB / STL / PLY / OBJ). It pairs with the PyGeoFetch / PyGeoVision
ecosystem through a dedicated integration layer.

This is **0.8.0 (Beta)** — "leafmap-style Notebook API". It is deliberately
honest about its boundaries: features listed under **What works today** are
implemented, and anything not yet built either says so here or raises a clear,
actionable error instead of silently stubbing. A full, per-component
verification record ships in [`PACKAGE_STATUS.md`](PACKAGE_STATUS.md).

---

## Two engines, one API

| Engine | Import | Renders | Best for |
|---|---|---|---|
| **deck.gl / PyVista / Cesium** (default) | `import pygeospace as pgs` → `pgs.Map()` | 2D WebGL maps, **true 3D** (PyVista), **3D globe** (CesiumJS) | imagery, terrain, large data, 3D |
| **folium** (Leaflet) | `from pygeospace.folium import Map` | 2D Leaflet maps + plugins | classic web maps, choropleths, clustering, draw/measure |

The default engine is deck.gl-based. The folium engine is additive and selected
explicitly. The **integration adapters are engine-agnostic** — they produce a
`Map` on whichever engine you use.

---

## Install

```bash
pip install pygeospace                 # core: vector IO, analytics, deck.gl, CLI
pip install "pygeospace[raster]"       # + GeoTIFF / COG / JPEG2000 (rasterio)
pip install "pygeospace[3d]"           # + true 3D (PyVista, trame, meshio)
pip install "pygeospace[folium]"       # + folium backend (folium, branca, jinja2)
pip install "pygeospace[classify]"     # + true Fisher-Jenks breaks (jenkspy)
pip install "pygeospace[osm]"          # + OpenStreetMap extraction (osmnx)
pip install "pygeospace[stac]"         # + Planetary Computer (pystac-client, planetary-computer)
pip install "pygeospace[overture]"     # + Overture Maps (overturemaps)
pip install "pygeospace[earthengine]"  # + Google Earth Engine (earthengine-api, geemap)
pip install "pygeospace[all]"          # everything pip-installable without accounts
```

Python 3.10+.

---

## Quickstart (2D, under 5 minutes)

```python
import pygeospace as pgs

m = pgs.Map(title="Cities")
m.add_basemap("carto-light")                      # registry name or raw XYZ URL
m.add_layer("cities.geojson").style(get_fill_color=[255, 90, 0, 200], get_radius=3000)

m.fit().save("map.html")                          # interactive deck.gl page
m.save("map.png", dpi=300)                        # static export

# A Map behaves like a container of its layers:
print(m)                                          # <Map mode=2d · 2 layers: [basemap, cities]>
print(m.layer_names, len(m), m["cities"])
```

Command line:

```bash
pygeospace visualize cities.geojson -o map.html --style choropleth --attribute pop --method jenks
pygeospace export cities.geojson -o map.png --dpi 300
pygeospace serve cities.geojson --port 8000       # live preview
pygeospace info data.gpkg
```

## Satellite imagery — one call per scene

`read_bands` folds the whole discover / window / reproject / scale / stack
pipeline into a single call, returning a stacked `RasterLayer` in
`[blue, green, red, nir, swir]` order.

```python
import pygeospace as pgs
from pygeospace.analytics.spectral import spectral_index

stack = pgs.read_bands("data/satellite/", bbox=(-74.05, 40.68, -73.90, 40.82))

m = pgs.Map().add_basemap()
m.add_composite(stack, "false_color")             # true_color / false_color / agriculture
m.save("scene.html")

ndvi = spectral_index(stack, "ndvi")              # band order already correct
# one-liner: m.add_bands("data/satellite/", "true_color", bbox=...)
```

## True 3D

```python
import pygeospace as pgs

m = pgs.Map(mode="3d")
m.add_terrain("srtm.tif", exaggeration=2.0, cmap="gist_earth")
m.export_3d("terrain.html")                       # interactive
m.render_3d("terrain.png")                        # static
m.export_3d("terrain.gltf")                       # model for Blender / Unity / printing

cam = pgs.Camera3D(position=(0, 0, 5000), focal_point=(0, 0, 0))
cam.fly_to(4000, 4000, 2000, duration=3.0).orbit(45).tilt(20)
m.render_3d("flyover.png", camera=cam)
```

## 3D globe (CesiumJS)

The same map on an interactive 3D globe, with a built-in 2D / 2.5D / 3D scene
toggle. **Token-free by default** (open imagery); a Cesium Ion token is optional
for world terrain.

```python
import pygeospace as pgs

m = pgs.Map(title="NYC")
m.add_layer(gdf, name="Landmarks")
m.add_bands("data/satellite/", "true_color", bbox=(-74.05, 40.68, -73.90, 40.82))
m.save("globe.html", engine="cesium")             # no token required
# premium terrain (optional): m.to_cesium_html(ion_token="…", terrain=True)
```

## folium backend (notebook-first)

Renders inline in Jupyter / Colab and shows **both raster and vector** on one map.

```python
from pygeospace.folium import (Map, VectorLayer, RasterLayer, ChoroplethLayer,
                               HeatmapLayer, ClusterLayer, SplitMapLayer, TimeSeriesLayer)

m = Map(location=(5.6, -0.2), zoom_start=11, basemap="carto-dark", title="Accra")
m.add_layer(RasterLayer(ndvi_array, bounds=(-0.3, 5.5, -0.1, 5.7), cmap="RdYlGn"))  # raster overlay
m.add_layer(VectorLayer("cities.geojson", tooltip="name", popup=["name", "pop"], fill_color="#e53e3e"))
m.add_layer(ChoroplethLayer("districts.geojson", attribute="pop", classification="quantile"))
m.add_layer(HeatmapLayer("events.csv"))
m.add_layer(ClusterLayer("points.geojson"))

m.add_controls().fit()
m.show()                                            # inline in a notebook cell
# m.save("accra.html")  /  m.to_png("accra.png")    # to_png needs [export] + headless Chrome
```

Notebook layers: `RasterLayer` (numpy array / pygeospace raster / GeoTIFF / XYZ-COG
overlay), `SplitMapLayer` (draggable before/after divider), `TimeSeriesLayer`
(animated slider), `CustomLayer` (HTML overlay). Classification methods:
`quantile`, `equal_interval`, `natural_breaks` (Fisher-Jenks via `jenkspy` if
installed, else a dependency-free k-means fallback), `std_mean`.

### Controls & panels

The folium `Map` carries a full set of custom controls — real Leaflet controls
anchored in the map corners — chainable in one expression:

```python
(m.add_title("Vegetation index", subtitle="NDVI · 2024")
  .add_legend({"Dense": "#1a9850", "Sparse": "#fee08b", "Built-up": "#d73027"})
  .add_colorbar(-0.4, 0.9, cmap="RdYlGn", caption="NDVI")   # continuous scale for rasters
  .add_info_panel("<p>How to read this map…</p>", title="About", collapsed=True)
  .add_search()             # Nominatim geocoder box
  .add_locate()             # geolocation button
  .add_scale()              # scale bar
  .add_basemap_switcher(["carto-light", "carto-dark", "esri-satellite"]))

m.add_full_ui()             # layer control + fullscreen + minimap + coords + search + locate + scale
```

Panels: `add_title`, `add_legend`, `add_colorbar`, `add_info_panel` (collapsible),
`add_panel` (arbitrary HTML). Controls: `add_search`, `add_locate`, `add_scale`,
`add_basemap_switcher`, plus `add_layer_control`/`add_fullscreen`/`add_measure`/
`add_draw`/`add_mouse_position`/`add_minimap`.

### leafmap-style methods

If you know leafmap, the folium `Map` speaks the same dialect:

```python
from pygeospace.folium import Map, colormaps as cm

m = Map(center=(40, -100), zoom=4)
m.add_geojson("cities.geojson", layer_name="Cities", style_callback=lambda f: {"color": f["properties"]["c"]})
m.add_gdf(gdf); m.add_shp("states.zip"); m.add_vector("regions.gpkg")
m.add_csv("places.csv", x="lon", y="lat", popup=["name"])
m.add_raster("dem.tif", cmap="terrain")               # single band
m.add_raster("landsat.tif", band=[4, 3, 2], vmin=1, vmax=100)
m.add_cog_layer(cog_url); m.add_stac_layer(stac_url, bands=["B3", "B2", "B1"])
m.add_marker_cluster(pts); m.add_heatmap(pts); m.add_choropleth(geo, "population")
m.add_labels("states.geojson", "name", font_color="blue")
m.add_legend(builtin_legend="NLCD")                   # or legend_dict={...}
m.add_colorbar(0, 4000, cmap="terrain", caption="Elevation")
m.split_map("carto-light", "esri-satellite", left_label="A", right_label="B")
m.add_vector_tile_layer(mvt_url, attribution="Microsoft")

cm.get_palette("terrain", n_class=8)                  # colormaps module
```

`add_cog_layer` / `add_stac_layer` use a titiler endpoint and
`add_vector_tile_layer` uses Leaflet.VectorGrid, so those three need network at
view time; everything else renders offline.

## Basemap registry

46 attributed XYZ basemaps (27 key-free), shared by both engines.

```python
import pygeospace as pgs
from pygeospace.basemaps import basemap_url

pgs.list_basemaps(category="satellite", free_only=True)   # ['esri-satellite', 'usgs-imagery', ...]
pgs.get_basemap("carto-dark").attribution

# folium engine resolves registry names directly:
from pygeospace.folium import Map
Map(basemap="esri-satellite")
# deck.gl engine: pass a resolved URL
pgs.Map().add_basemap(basemap_url("esri-satellite"))
```

Key-required providers (Mapbox, Stadia/Stamen, Thunderforest, OpenWeatherMap)
are flagged and fail loudly without a key rather than serving dead tiles.

## Integrations

Import-light adapters that turn other tools' outputs into a PyGeoSpace map. Heavy
or account-bound dependencies load lazily with clear error messages.

```python
from pygeospace.integrations import geofetch, geovision, osm, pc, overture
from pygeospace.integrations import ee as earth_engine
from pygeospace.integrations import available

available()   # which integrations are usable right now

geofetch.visualize_search(results).save("search.html")        # PyGeoFetch footprints by provider
geofetch.visualize_downloads(downloaded).save("status.html")  # coloured by ok / failed / corrupt
geovision.visualize_predictions(preds).save("preds.html")     # model predictions, per-class layers

osm.from_place("Accra, Ghana", tags={"amenity": "hospital"})  # needs [osm]
pc.search("sentinel-2-l2a", bbox=bbox, datetime="2024-06")    # needs [stac]
overture.buildings((-0.21, 5.55, -0.17, 5.58))                # needs [overture]
earth_engine.tile_url(ee_image, {"min": 0, "max": 3000})      # needs [earthengine] + auth
```

---

## What works today (0.7.0)

| Area | Capability | Status |
|------|-----------|--------|
| **IO** | Shapefile, GeoJSON, GeoPackage, KML, GPX, GML; CSV with coords; PostGIS; format autodetect | ✅ |
| | GeoTIFF / COG / JPEG2000 read | ✅ `[raster]` |
| | LAS/LAZ point cloud read | ✅ `[pointcloud]` |
| | `read_bands()` — per-band satellite discover/window/reproject/scale/stack | ✅ `[raster]` |
| **2D viz** | deck.gl scatter / GeoJSON / heatmap / hexagon; rich interactive HTML (layer panel, basemap switch, measure, coords, legend) | ✅ |
| | Choropleth: quantile, equal-interval, **Jenks** | ✅ |
| | Georeferenced raster display + static PNG/PDF | ✅ |
| **3D globe** | CesiumJS globe, 2D/2.5D/3D toggle, token-free imagery | ✅ |
| **True 3D** | Terrain, point clouds (LAS classes), polygon extrusion, cut planes | ✅ `[3d]` |
| | Export glTF/GLB/STL/PLY/OBJ/VTK; `Camera3D` fly/orbit/tilt | ✅ `[3d]` |
| **Raster analytics** | Spectral indices NDVI/NDWI/NDBI/SAVI/EVI; band composites; slope; reproject; mosaic | ✅ |
| **Vector analytics** | Buffer (true meters via UTM), intersection, difference, dissolve; KMeans/DBSCAN; H3/grid binning; contours; O-D flow | ✅ |
| **Basemaps** | 46-entry registry with attribution + key-guarding | ✅ |
| **folium engine** | `Map`, Vector/Choropleth/Heatmap/Cluster; **Raster overlays (array/COG/tiles), Split, TimeSeries, Custom**; classification | ✅ `[folium]` |
| | Inline Jupyter/Colab display (`show`); panels: title, legend, colorbar, info; controls: search, locate, scale, basemap-switcher, draw, measure, fullscreen, minimap | ✅ `[folium]` |
| | leafmap-style API: add_geojson/gdf/shp/vector/csv, add_raster, add_cog_layer, add_stac_layer, add_labels, split_map, linked_maps, colormaps, built-in legends | ✅ `[folium]` |
| **Integrations** | geofetch, geovision (ecosystem); osm, pc, overture, ee (lazy) | ✅ (extras per backend) |
| **Streaming** | WebSocket & MQTT clients; trail buffer; alerts | ✅ `[streaming]` |
| **Publishing** | Interactive HTML, static PNG/PDF, Jupyter `_repr_html_`, FastAPI REST server | ✅ |
| **Extensibility** | Decorator plugin system | ✅ |

The deck.gl engine ships with a pytest suite (105 tests passing as of the 0.6.x
API work). The 0.7.0 additions (basemaps, folium engine, integrations) were
verified when this build was assembled — see [`PACKAGE_STATUS.md`](PACKAGE_STATUS.md)
for the exact matrix, including what was *not* re-verified in that environment.

## Not built yet / roadmap

Where the public API touches these, it raises a clear error rather than
pretending.

| Planned | Feature |
|---|---|
| **folium engine** | `to_png`/`to_pdf` (selenium/playwright, weasyprint) — *wired, needs a headless browser*; ipywidgets / Streamlit widgets; full Jinja2 theme system; raster `TimeSeriesLayer` frames |
| **More backends** | ipyleaflet, plotly |
| **Scale** | COG tile server with disk cache; vector tiles; WebGPU path for multi-million-point scenes |
| **Analytics** | Zonal statistics charts; Getis-Ord Gi\* hotspots; OSMnx routing |

## Design principles

- **Honesty over surface area.** A smaller set of things that genuinely work
  beats a large set that breaks on first use. Unimplemented features error
  clearly; they are never silently stubbed.
- **Real units.** Buffers reproject to the local UTM zone so "250 m" means 250
  meters, not 250 degrees.
- **Token-free by default.** Open imagery for both the 2D maps and the globe;
  paid keys are optional and, when required, fail loudly.
- **Lazy optional deps.** Heavy/optional libraries import only when used, with an
  install hint on failure.

## Examples & notebooks

Runnable notebooks live in [`examples/notebooks/`](examples/notebooks/) and
double as integration tests:

- `00_complete_tour.ipynb` — **the full tour**: every layer type, the leafmap-style API, and all advanced custom controls/panels in one notebook
- `01_quickstart.ipynb` — vectors, tooltips, basemaps
- `02_raster_and_vector.ipynb` — raster overlays + colorbar + vector, RGB composite, before/after split
- `03_controls_legends_colormaps.ipynb` — title/legend/colorbar/info panels, search/locate/scale, colormaps

They open directly in Jupyter or Colab (the folium engine renders inline).

## Development

```bash
pip install -e ".[folium,dev]"
pytest                       # core suite + notebook execution tests
pytest tests/test_notebooks.py   # just run the example notebooks end-to-end
ruff check .
```

The notebook tests execute every example in a fresh kernel and fail if any cell
raises — they skip automatically if the notebook stack isn't installed.

## License

MIT.
