diff --git a/README.md b/README.md index 51a1468..0f05317 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,34 @@ # dpg-map -`dpg-map` is a Dear PyGui map widget for XYZ raster tiles and geographic overlays. - -The rebuilt beta exposes a stable public import: +`dpg-map` is a Dear PyGui widget for interactive XYZ raster maps, tile caching, and +geographic overlays. ```python import dpg_map as dpgm ``` +The widget is built for telemetry dashboards and internal tools that need maps inside normal +Dear PyGui layouts. Runtime updates are thread-safe: marker, trajectory, view, provider, and +cache calls update logical state or enqueue renderer work, while Dear PyGui draw calls stay on +the GUI thread. + ## Install -Use `uv` for development and dependency management: +For local development: ```bash uv sync uv run pytest ``` -From another local project, add this package as an editable dependency: +From another local project: ```bash uv add --editable ../dpg-map uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())" ``` -## Basic Map +## Minimal Example ```python from typing import Any @@ -37,7 +41,7 @@ dpg: Any = _dpg dpgm.configure(user_agent="my-app/0.1 contact@example.com") dpg.create_context() -dpg.create_viewport(title="Map", width=1000, height=700) +dpg.create_viewport(title="Map", width=900, height=600) with dpg.window(label="Map", width=-1, height=-1): with dpgm.map_widget(tag="map", center=(47.9029, 1.9093), zoom=15, width=-1, height=-1): @@ -49,128 +53,36 @@ dpg.start_dearpygui() dpg.destroy_context() ``` -## Sizing - -The widget is a Dear PyGui `child_window` containing a measured drawlist. The child keeps the requested Dear PyGui sizing intent while the drawlist uses concrete measured pixels. - -Supported sizing modes: - -- `width=0` and `height=0` keep Dear PyGui default sizing. -- `width=-1` and `height=-1` fill available space where Dear PyGui supports it. -- Positive dimensions request fixed sizes. -- `autosize_x` and `autosize_y` are passed to the child window. -- Hidden layouts preserve the last non-zero measured size until visible again. - -Examples: - -```bash -uv run python examples/sizing_window.py -uv run python examples/sizing_child.py -uv run python examples/sizing_table.py -uv run python examples/hidden_tab.py -``` - -## Live Updates - -Runtime overlay updates are safe to call from background threads. They update logical state and enqueue renderer commands; Dear PyGui drawing, texture, handler, and viewport calls stay on the GUI thread. - -Live marker update: - -```python -dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093, map_tag="map") -dpgm.update_marker("vehicle", lat=current_lat, lon=current_lon, map_tag="map") -``` - -Live trajectory update: - -```python -dpgm.add_trajectory("track", points=[], map_tag="map") -dpgm.update_trajectory("track", points=tuple(points), map_tag="map") -``` - -Stress examples: - -```bash -uv run python examples/markers_live_thread.py -uv run python examples/trajectory_live_thread.py -``` - -## Providers - -OpenStreetMap is registered as `osm` by default. Custom XYZ providers can be registered and selected at runtime: - -```python -provider = dpgm.TileProvider( - name="custom", - url_template="https://example.com/{z}/{x}/{y}.png", - attribution="Tiles (c) Example", -) -dpgm.register_provider(provider) -dpgm.set_provider("custom", map_tag="map") -``` - -Provider switching preserves overlays and center, clamps zoom to the new provider range, increments the tile generation, and ignores stale tile results from the previous provider. - -Example: - -```bash -uv run python examples/custom_provider.py -``` - -## Cache - -Tiles use an in-memory cache and a persistent provider-namespaced disk cache. - -```python -dpgm.configure( - user_agent="my-app/0.1 contact@example.com", - cache_dir=".tile-cache", - memory_cache_max_tiles=512, - disk_cache_max_bytes=2_000_000_000, -) - -stats = dpgm.get_cache_stats(map_tag="map") -dpgm.clear_memory_cache(map_tag="map") -dpgm.clear_disk_cache(provider="osm") -``` - -`disk_cache_max_bytes=None` disables the disk size limit. Memory cache clears are routed through the renderer command queue so texture deletion happens on the GUI thread. Disk cache clears can target all providers or a single provider namespace. - -Cache example: - -```bash -uv run python examples/cache_stress.py -``` - -## OpenStreetMap Usage - -The default OpenStreetMap provider requires attribution and should use an application-specific `User-Agent`. - -```python -dpgm.configure(user_agent="my-product/1.0 contact@example.com") -``` - -If no user agent is configured, `dpg-map` emits a runtime warning and falls back to a package user agent. Applications are responsible for displaying provider attribution in accordance with provider terms; the renderer draws the provider attribution text on the map. - -## Thread-Safety Contract - -Public runtime functions are intended to be callable from non-GUI threads unless explicitly documented otherwise. They acquire map locks briefly, update logical state, and/or enqueue commands. - -Thread-safe runtime areas include: - -- view updates: `set_center`, `set_zoom`, `set_view`, `fit_bounds` -- overlay updates: `add_marker`, `update_marker`, `update_trajectory`, `delete_overlay` -- layer updates: `add_layer`, `show_layer`, `hide_layer`, `clear_layer` -- provider/cache updates: `set_provider`, `clear_memory_cache`, `clear_disk_cache` - -`map_widget(...)` creates Dear PyGui items and must be used on the GUI thread inside an active Dear PyGui context. - -## Examples +Run the bundled example: ```bash uv run python examples/basic_map.py -uv run python examples/cache_stress.py -uv run python examples/custom_provider.py -uv run python examples/markers_live_thread.py -uv run python examples/trajectory_live_thread.py +``` + +## Documentation + +- [Getting Started](docs/GETTING_STARTED.md) +- [Examples](docs/EXAMPLES.md) +- [API Reference](docs/API_REFERENCE.md) + +Internal rebuild notes, implementation plans, and agent logs live in [codex/](codex/). + +## Highlights + +- OpenStreetMap provider registered by default as `osm` +- Custom XYZ tile providers +- Pan and cursor-centered zoom +- Markers, polylines, and live trajectories +- Overlay layers with visibility and z-order controls +- Memory tile cache and provider-namespaced disk cache +- Multiple independent map widgets in one Dear PyGui app +- Background-thread runtime updates for telemetry workloads + +## Project Commands + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright ``` diff --git a/AGENTS.md b/codex/AGENTS.md similarity index 99% rename from AGENTS.md rename to codex/AGENTS.md index 30064e8..5e3ee39 100644 --- a/AGENTS.md +++ b/codex/AGENTS.md @@ -36,7 +36,7 @@ None yet. ## Commands used -- Read `STEPS.md`, `FEATURES.md`, and `ARCHITECTURE.md`. +- Read `codex/STEPS.md`, `codex/FEATURES.md`, and `codex/ARCHITECTURE.md`. - Created initial package, examples, tests, and agent-log structure. - Implemented public exports, exceptions, common types, tile provider registry, projection helpers, cache dataclasses, and GUI-dependent API stubs. - Added Step 1 tests for imports, providers, projection, and cache dataclasses. diff --git a/ARCHITECTURE.md b/codex/ARCHITECTURE.md similarity index 99% rename from ARCHITECTURE.md rename to codex/ARCHITECTURE.md index 31ff363..c03a4e3 100644 --- a/ARCHITECTURE.md +++ b/codex/ARCHITECTURE.md @@ -684,14 +684,14 @@ Create: src/dpg_map/ examples/ tests/ -FEATURES.md -ARCHITECTURE.md -STEPS.md -AGENTS.md +codex/FEATURES.md +codex/ARCHITECTURE.md +codex/STEPS.md +codex/AGENTS.md README.md ``` -Use `AGENTS.md` as the rolling implementation log. After every step: +Use `codex/AGENTS.md` as the rolling implementation log. After every step: ```bash uv run pytest diff --git a/FEATURES.md b/codex/FEATURES.md similarity index 100% rename from FEATURES.md rename to codex/FEATURES.md diff --git a/codex/README.md b/codex/README.md new file mode 100644 index 0000000..87fca60 --- /dev/null +++ b/codex/README.md @@ -0,0 +1,18 @@ +# Codex Build Notes + +This folder contains internal rebuild instructions, implementation architecture notes, and the +rolling agent log used while preparing the beta. + +These files are not user-facing package documentation: + +- `AGENTS.md`: rolling implementation log and current build status +- `ARCHITECTURE.md`: internal architecture and invariants +- `FEATURES.md`: rebuild feature contract +- `STEPS.md`: historical rebuild step plan + +User-facing documentation lives in: + +- `../README.md` +- `../docs/GETTING_STARTED.md` +- `../docs/EXAMPLES.md` +- `../docs/API_REFERENCE.md` diff --git a/STEPS.md b/codex/STEPS.md similarity index 97% rename from STEPS.md rename to codex/STEPS.md index 890ecb7..05365a9 100644 --- a/STEPS.md +++ b/codex/STEPS.md @@ -7,11 +7,11 @@ There is no Step 0. Initial setup is listed separately, then implementation star ## Workflow rules 1. Use `uv` for all Python package and dependency management. -2. Always read `FEATURES.md`, `ARCHITECTURE.md`, and `AGENTS.md` before making code changes. -3. Keep `AGENTS.md` as a rolling log of what has been done, what is broken, and what comes next. +2. Always read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, and `codex/AGENTS.md` before making code changes. +3. Keep `codex/AGENTS.md` as a rolling log of what has been done, what is broken, and what comes next. 4. Update `README.md` whenever public behaviour or examples change. 5. After every step: - - update `AGENTS.md` + - update `codex/AGENTS.md` - run relevant checks - commit to git 6. Do not casually change public API once introduced. @@ -55,10 +55,10 @@ src/dpg_map/ exceptions.py examples/ tests/ -FEATURES.md -ARCHITECTURE.md -STEPS.md -AGENTS.md +codex/FEATURES.md +codex/ARCHITECTURE.md +codex/STEPS.md +codex/AGENTS.md README.md ``` @@ -76,7 +76,7 @@ select = ["E", "F", "I", "UP", "B", "SIM"] typeCheckingMode = "basic" ``` -Create `AGENTS.md` with: +Create `codex/AGENTS.md` with: ```markdown # AGENTS.md @@ -753,7 +753,7 @@ Acceptance criteria: - disk cache limit works - sizing examples work - README accurately documents thread-safety and cache behaviour -- AGENTS.md accurately describes status +- codex/AGENTS.md accurately describes status Commit: diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..1082e33 --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,411 @@ +# API Reference + +Public imports are available from: + +```python +import dpg_map as dpgm +``` + +Unless stated otherwise, runtime public functions are safe to call from non-GUI threads. They +update logical state and enqueue GUI-thread renderer work. `map_widget(...)` is the main exception: +it creates Dear PyGui items and must run on the GUI thread. + +## Common Types + +- `Tag`: `str | int` +- `LatLon`: `tuple[float, float]` as `(lat, lon)` +- `Point`: `tuple[float, float]` as `(x, y)` +- `Bounds`: `tuple[LatLon, LatLon]` as `((south, west), (north, east))` +- `Color`: RGB or RGBA integer tuple, usually `(r, g, b, a)` + +## Configuration + +### `configure(...) -> None` + +Configure package-wide defaults used by subsequently created maps. + +```python +dpgm.configure( + user_agent=None, + cache_dir=None, + default_provider="osm", + memory_cache_max_tiles=512, + disk_cache_max_bytes=2_000_000_000, + prefetch_margin_tiles=1, + tile_worker_count=4, + overlay_update_policy="coalesce", + debug=False, +) +``` + +Parameters: + +- `user_agent`: HTTP user agent for tile requests. +- `cache_dir`: persistent tile cache root. Defaults to a platform user cache directory. +- `default_provider`: provider name or `TileProvider` used by new maps. +- `memory_cache_max_tiles`: maximum decoded runtime tile count per map. +- `disk_cache_max_bytes`: maximum disk cache size. Use `None` for no size limit. +- `prefetch_margin_tiles`: number of tiles to request around the visible viewport. +- `tile_worker_count`: background worker count for disk/network/decode work. +- `overlay_update_policy`: currently only `"coalesce"` is supported. +- `debug`: store debug mode flag in global config. + +Raises `ValueError` for invalid cache, worker, prefetch, or update-policy settings. + +## Widget + +### `map_widget(...) -> Iterator[Tag | None]` + +Create a Dear PyGui child-window map shell and logical map context. + +```python +with dpgm.map_widget( + tag="map", + center=(47.9029, 1.9093), + zoom=15, + provider="osm", + width=-1, + height=-1, +): + dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093) +``` + +Parameters: + +- `tag`: map tag. If omitted, a generated tag is used. +- `center`: initial `(lat, lon)`. +- `zoom`: initial zoom, clamped to provider range. +- `provider`: provider name or `TileProvider`; defaults to configured default provider. +- `width`, `height`: Dear PyGui child-window size arguments. +- `autosize_x`, `autosize_y`: Dear PyGui child-window autosize flags. +- `cache_dir`: optional per-map cache directory passed as keyword argument. +- `user_agent`: optional per-map tile user agent passed as keyword argument. +- Other keyword arguments are forwarded to `dpg.add_child_window(...)`. + +GUI-thread only. + +## View + +### `set_center(lat, lon, *, map_tag=None) -> None` + +Set the map center without changing zoom. + +### `get_center(*, map_tag=None) -> LatLon` + +Return the current logical map center. + +### `set_zoom(zoom, *, map_tag=None) -> None` + +Set the zoom, clamped to the current provider range. + +### `get_zoom(*, map_tag=None) -> int` + +Return the current logical zoom. + +### `set_view(*, center=None, zoom=None, map_tag=None) -> None` + +Set center and/or zoom as one logical view update. + +```python +dpgm.set_view(center=(47.9029, 1.9093), zoom=15, map_tag="map") +``` + +### `fit_bounds(bounds, *, map_tag=None) -> None` + +Set center and zoom so bounds fit the current draw area. + +```python +dpgm.fit_bounds(((47.89, 1.89), (47.92, 1.93)), map_tag="map") +``` + +### `screen_to_latlon(x, y, *, map_tag=None) -> LatLon` + +Convert map-local screen coordinates to latitude/longitude. + +### `latlon_to_screen(lat, lon, *, map_tag=None) -> Point` + +Convert latitude/longitude to map-local screen coordinates. + +## Markers + +### `add_marker(tag, *, lat, lon, label=None, layer="default", show=True, map_tag=None, **kwargs) -> Tag` + +Add or replace a marker overlay. + +Keyword options: + +- `color`: marker fill color, default `(255, 80, 80, 255)`. +- `radius`: marker radius in pixels, default `5.0`. +- `show_label`: draw `label` next to the marker, default `False`. +- `user_data`: stored with the overlay model. +- `callback`: stored with the overlay model for application use. + +### `update_marker(tag, *, lat=None, lon=None, label=None, map_tag=None, **kwargs) -> None` + +Update marker properties without changing map view. + +Supported keyword updates: + +- `show` +- `color` +- `radius` + +### `set_marker_position(tag, lat, lon, *, map_tag=None) -> None` + +Shortcut for `update_marker(..., lat=lat, lon=lon)`. + +### `set_marker_label(tag, label, *, map_tag=None) -> None` + +Shortcut for `update_marker(..., label=label)`. + +## Polylines + +### `add_polyline(tag, *, points=None, lats=None, lons=None, layer="default", show=True, map_tag=None, **kwargs) -> Tag` + +Add or replace a polyline overlay. + +Provide either: + +- `points=[(lat, lon), ...]` +- or `lats=[...], lons=[...]` + +Keyword options: + +- `color`: line color, default `(80, 180, 255, 255)`. +- `thickness`: line thickness, default `2.0`. +- `closed`: close the polyline, default `False`. +- `simplify`: stored with the overlay model, default `True`. +- `user_data`: stored with the overlay model. + +### `update_polyline(tag, *, points=None, lats=None, lons=None, map_tag=None, **kwargs) -> None` + +Update polyline points or properties. + +Supported keyword updates: + +- `show` +- `color` +- `thickness` + +### `set_polyline_points(tag, points, *, map_tag=None) -> None` + +Shortcut for replacing a polyline point sequence. + +## Trajectories + +### `add_trajectory(tag, *, points=None, lats=None, lons=None, layer="default", show=True, map_tag=None, **kwargs) -> Tag` + +Add or replace a trajectory overlay. + +Provide either `points` or `lats`/`lons`. Empty trajectories are valid for live updates. + +Keyword options: + +- `timestamps`: optional sequence with the same length as points. +- `color`: trajectory color, default `(255, 180, 60, 255)`. +- `thickness`: line thickness, default `2.0`. +- `show_points`: draw point markers along the trajectory, default `False`. +- `point_stride`: draw every Nth point when `show_points=True`, default `1`. +- `user_data`: stored with the overlay model. + +### `update_trajectory(tag, *, points=None, lats=None, lons=None, map_tag=None, **kwargs) -> None` + +Update trajectory points or properties. + +Supported keyword updates: + +- `timestamps` +- `show` +- `color` +- `thickness` + +## Generic Overlay Control + +### `set_overlay_show(tag, show, *, map_tag=None) -> None` + +Show or hide an overlay without deleting it. + +### `delete_overlay(tag, *, map_tag=None) -> None` + +Delete an overlay from its map and layer. + +When `map_tag` is omitted, the API can resolve an overlay only if the tag is unambiguous across +registered maps. In multi-map apps, pass `map_tag`. + +## Layers + +### `add_layer(name, *, z_index=None, show=True, map_tag=None) -> None` + +Create or update a logical overlay layer. + +### `show_layer(name, *, map_tag=None) -> None` + +Show all overlays assigned to a layer. + +### `hide_layer(name, *, map_tag=None) -> None` + +Hide all overlays assigned to a layer. + +### `clear_layer(name, *, map_tag=None) -> None` + +Delete all overlays assigned to a layer. + +### `clear_map(*, map_tag=None) -> None` + +Delete all overlays and invalidate map tile resources. + +## Tile Providers + +### `TileProvider` + +Immutable tile provider definition. + +```python +dpgm.TileProvider( + name="custom", + url_template="https://example.com/{z}/{x}/{y}.png", + min_zoom=0, + max_zoom=19, + tile_size=256, + attribution="Tiles (c) Example", + headers={}, + subdomains=(), + retina=False, + file_extension=None, +) +``` + +Fields: + +- `name`: provider registry name. +- `url_template`: XYZ URL template with `{z}`, `{x}`, and `{y}`. +- `min_zoom`, `max_zoom`: valid zoom range. +- `tile_size`: tile size in pixels. +- `attribution`: text drawn on the map. +- `headers`: request headers. +- `subdomains`: values for `{s}`. +- `retina`: if true, `{r}` expands to `"@2x"`. +- `file_extension`: value for `{ext}` and disk cache extension. + +### `register_provider(provider, *, replace=False) -> None` + +Register a provider by name. + +### `unregister_provider(name) -> None` + +Remove a registered provider. + +### `get_provider(name) -> TileProvider` + +Return a registered provider. + +### `list_providers() -> list[str]` + +Return registered provider names in sorted order. + +### `set_provider(provider, *, map_tag=None) -> None` + +Switch a map to another provider while preserving overlays and center. + +`provider` may be a provider name or a `TileProvider`. Provider switching clamps zoom to the new +provider range and invalidates stale tile loads. + +## Cache + +### `CacheStats` + +Public cache statistics snapshot. + +Fields: + +- `memory_tiles` +- `memory_max_tiles` +- `memory_hits` +- `memory_misses` +- `disk_bytes` +- `disk_max_bytes` +- `disk_hits` +- `disk_misses` +- `disk_path` + +### `get_cache_stats(*, map_tag=None) -> CacheStats` + +Return memory and disk cache diagnostics. With `map_tag=None`, returns global disk-cache stats and +configured memory limit. + +### `clear_memory_cache(*, map_tag=None) -> None` + +Clear decoded in-memory tile data. If `map_tag` is omitted outside a map context, all registered +maps are targeted. + +### `clear_disk_cache(provider=None, *, map_tag=None) -> None` + +Clear persistent tile cache data. + +- `provider=None`: clear all provider namespaces. +- `provider="osm"`: clear only one provider namespace. +- `map_tag="map"`: enqueue a map-scoped clear through the renderer. + +## Diagnostics + +### `get_map_debug_state(*, map_tag=None) -> dict[str, Any]` + +Return a diagnostic snapshot for a map. Keys include: + +- `tag` +- `center` +- `zoom` +- `requested_size` +- `measured_size` +- `visible` +- `provider` +- `overlay_count` +- `layers` +- `dirty_flags` +- `pending_command_count` +- `generation` +- `active_drag` +- `last_mouse_position` +- `tiles` + +This output is for debugging and logging. It is not guaranteed to be a stable serialization +format. + +## Exceptions + +Public exception classes are defined in `dpg_map.exceptions`: + +- `DpgMapError` +- `ProviderError` +- `ProviderExistsError` +- `ProviderNotFoundError` +- `InvalidProviderError` +- `ProjectionError` +- `MapNotFoundError` +- `OverlayNotFoundError` +- `CoordinateError` +- `ThreadingError` +- `CacheError` + +Common error cases: + +- Invalid latitude/longitude raises `CoordinateError`. +- Unknown map tags raise `MapNotFoundError`. +- Unknown overlay tags raise `OverlayNotFoundError`. +- Unknown or invalid providers raise provider-specific errors. + +## Threading Contract + +GUI-thread only: + +- `map_widget(...)` +- Dear PyGui setup and application code that creates widgets + +Runtime-safe public areas: + +- View updates: `set_center`, `set_zoom`, `set_view`, `fit_bounds` +- Overlay updates: `add_marker`, `update_marker`, `update_trajectory`, `delete_overlay` +- Layer updates: `add_layer`, `show_layer`, `hide_layer`, `clear_layer` +- Provider and cache updates: `set_provider`, `clear_memory_cache`, `clear_disk_cache` + +The renderer owns Dear PyGui draw commands, textures, input handlers, and frame callbacks. diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md new file mode 100644 index 0000000..822dd02 --- /dev/null +++ b/docs/EXAMPLES.md @@ -0,0 +1,146 @@ +# Examples + +The `examples/` directory contains runnable Dear PyGui programs: + +```bash +uv run python examples/basic_map.py +uv run python examples/custom_provider.py +uv run python examples/cache_stress.py +uv run python examples/markers_live_thread.py +uv run python examples/trajectory_live_thread.py +uv run python examples/sizing_window.py +uv run python examples/sizing_child.py +uv run python examples/sizing_table.py +uv run python examples/hidden_tab.py +``` + +## Basic Map + +Creates one map and one marker. + +```python +with dpg.window(label="Map", width=-1, height=-1): + with dpgm.map_widget(tag="map", center=(47.9029, 1.9093), zoom=15, width=-1, height=-1): + dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093) +``` + +Run: + +```bash +uv run python examples/basic_map.py +``` + +## Live Marker Updates + +Use explicit `map_tag` from background threads or callbacks. + +```python +dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093, map_tag="map") + +def update_from_telemetry(lat: float, lon: float) -> None: + dpgm.update_marker("vehicle", lat=lat, lon=lon, map_tag="map") +``` + +Run a threaded stress example: + +```bash +uv run python examples/markers_live_thread.py +``` + +## Live Trajectory + +Keep an application-owned point buffer and pass immutable snapshots to `update_trajectory(...)`. + +```python +points: list[tuple[float, float]] = [] +dpgm.add_trajectory("track", points=[], show_points=True, point_stride=12, map_tag="map") + +def push_point(lat: float, lon: float) -> None: + points.append((lat, lon)) + dpgm.update_trajectory("track", points=tuple(points), map_tag="map") +``` + +Run: + +```bash +uv run python examples/trajectory_live_thread.py +``` + +## Multiple Maps + +Each map is independent. Reusing overlay tags across different maps is valid, but runtime updates +should pass `map_tag` to avoid ambiguity. + +```python +with dpgm.map_widget(tag="live-map", center=(47.9, 1.9), zoom=15, width=-1, height=300): + dpgm.add_marker("vehicle", lat=47.9, lon=1.9) + +with dpgm.map_widget(tag="recap-map", center=(47.9, 1.9), zoom=13, width=-1, height=300): + dpgm.add_trajectory("vehicle", points=[]) + +dpgm.update_marker("vehicle", lat=47.901, lon=1.902, map_tag="live-map") +dpgm.update_trajectory("vehicle", points=lap_points, map_tag="recap-map") +``` + +## Custom Provider Switch + +Register a provider, create the map with it, and switch at runtime. + +```python +carto = dpgm.TileProvider( + name="carto-light", + url_template="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", + subdomains=("a", "b", "c", "d"), + attribution="(c) OpenStreetMap contributors (c) CARTO", +) + +dpgm.register_provider(carto) +dpgm.set_provider("carto-light", map_tag="map") +dpgm.set_provider("osm", map_tag="map") +``` + +Run: + +```bash +uv run python examples/custom_provider.py +``` + +## Cache Controls + +Configure a local cache and expose buttons in your UI. + +```python +dpgm.configure(cache_dir=".tile-cache", memory_cache_max_tiles=128) + +def clear_memory() -> None: + dpgm.clear_memory_cache(map_tag="map") + +def clear_disk() -> None: + dpgm.clear_disk_cache(map_tag="map") + +def refresh_stats() -> str: + stats = dpgm.get_cache_stats(map_tag="map") + return f"{stats.memory_tiles} memory tiles, {stats.disk_bytes} disk bytes" +``` + +Run: + +```bash +uv run python examples/cache_stress.py +``` + +## Layout And Sizing + +The widget works inside windows, child windows, tables, and hidden tabs. + +```python +with dpg.child_window(width=-1, height=420): + with dpgm.map_widget(tag="map-child", width=-1, height=-1): + dpgm.add_marker("inside-child", lat=47.0, lon=2.0) +``` + +Run: + +```bash +uv run python examples/sizing_child.py +``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..effcae7 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,286 @@ +# Getting Started + +This guide walks through installing `dpg-map`, creating a map widget, adding overlays, updating +the map from runtime code, and configuring tile providers and caches. + +## Requirements + +`dpg-map` currently targets Python `>=3.14` and uses: + +- Dear PyGui for the UI +- Pillow for decoding tile images +- Requests for tile HTTP requests +- Platformdirs for the default disk cache location + +The project is managed with `uv`. + +## Install From This Repository + +For development inside this repository: + +```bash +uv sync +uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())" +``` + +For another local app using this checkout: + +```bash +cd /path/to/your-app +uv add --editable /home/hector/projects/dpg-map +``` + +Then import it as: + +```python +import dpg_map as dpgm +``` + +## Configure The Package + +Configure package-wide defaults before creating maps: + +```python +dpgm.configure( + user_agent="my-product/1.0 contact@example.com", + cache_dir=".tile-cache", + memory_cache_max_tiles=512, + disk_cache_max_bytes=2_000_000_000, +) +``` + +Set an application-specific `user_agent` when using OpenStreetMap tiles. If no user agent is +configured, `dpg-map` emits a runtime warning and uses a fallback package user agent. + +## Create A Map + +`map_widget(...)` is a Dear PyGui context manager. It creates a child window containing the map +drawlist and installs internal render and input handlers. + +```python +from typing import Any + +import dearpygui.dearpygui as _dpg +import dpg_map as dpgm + +dpg: Any = _dpg + +dpgm.configure(user_agent="getting-started/0.1 contact@example.com") + +dpg.create_context() +dpg.create_viewport(title="dpg-map", width=900, height=600) + +with dpg.window(label="Map", width=-1, height=-1): + with dpgm.map_widget(tag="map", center=(47.9029, 1.9093), zoom=15, width=-1, height=-1): + dpgm.add_marker("start", lat=47.9029, lon=1.9093, label="Start") + +dpg.setup_dearpygui() +dpg.show_viewport() +dpg.start_dearpygui() +dpg.destroy_context() +``` + +`map_widget(...)` must be called on the GUI thread inside an active Dear PyGui context. + +## Tags And Map Scope + +Each map has a `tag`. Runtime calls should pass `map_tag` when there may be more than one map: + +```python +dpgm.set_center(47.9029, 1.9093, map_tag="live-map") +dpgm.update_marker("vehicle", lat=47.9030, lon=1.9098, map_tag="live-map") +``` + +Inside a `with dpgm.map_widget(...)` block, overlay creation can omit `map_tag` because the map is +the current map context: + +```python +with dpgm.map_widget(tag="live-map"): + dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093) +``` + +Use explicit `map_tag` in callbacks, worker threads, and apps with multiple maps. + +## Add Overlays + +Markers: + +```python +dpgm.add_marker( + "vehicle", + lat=47.9029, + lon=1.9093, + label="Vehicle", + show_label=True, + color=(255, 80, 80, 255), + radius=6, + map_tag="map", +) +``` + +Polylines: + +```python +dpgm.add_polyline( + "route", + points=[(47.9029, 1.9093), (47.9050, 1.9150), (47.9080, 1.9180)], + color=(80, 180, 255, 255), + thickness=3, + map_tag="map", +) +``` + +Trajectories: + +```python +dpgm.add_trajectory( + "track", + points=[], + color=(255, 180, 60, 255), + thickness=3, + show_points=True, + point_stride=10, + map_tag="map", +) +``` + +Coordinates are `(lat, lon)` pairs in degrees. Latitude must be between `-90` and `90`; +longitude must be between `-180` and `180`. + +## Runtime Updates + +Runtime public calls are intended to be safe from background threads. They update logical state +and enqueue GUI-thread work instead of calling Dear PyGui directly. + +```python +dpgm.update_marker("vehicle", lat=current_lat, lon=current_lon, map_tag="map") +dpgm.update_trajectory("track", points=tuple(track_points), map_tag="map") +``` + +Overlay updates do not reset map center or zoom. + +## View Control + +```python +dpgm.set_center(47.9029, 1.9093, map_tag="map") +dpgm.set_zoom(15, map_tag="map") +dpgm.set_view(center=(47.9029, 1.9093), zoom=15, map_tag="map") +dpgm.fit_bounds(((47.89, 1.89), (47.92, 1.93)), map_tag="map") +``` + +`set_zoom(...)` and `set_view(...)` clamp zoom to the active provider range. + +Coordinate conversion helpers use map-local screen coordinates: + +```python +lat, lon = dpgm.screen_to_latlon(120, 80, map_tag="map") +x, y = dpgm.latlon_to_screen(47.9029, 1.9093, map_tag="map") +``` + +## Layers + +Overlays belong to named layers. Layers can be shown, hidden, cleared, and ordered with +`z_index`. + +```python +dpgm.add_layer("vehicles", z_index=80, map_tag="map") +dpgm.add_marker("car-1", lat=47.9, lon=1.9, layer="vehicles", map_tag="map") + +dpgm.hide_layer("vehicles", map_tag="map") +dpgm.show_layer("vehicles", map_tag="map") +dpgm.clear_layer("vehicles", map_tag="map") +``` + +Default layers include `default`, `markers`, `lines`, and `trajectories`. + +## Tile Providers + +OpenStreetMap is registered by default as `osm`. + +Custom providers use XYZ URL templates. Required template fields are `{z}`, `{x}`, and `{y}`. +Optional fields are `{s}` for subdomain, `{r}` for retina suffix, and `{ext}` for file extension. + +```python +provider = dpgm.TileProvider( + name="carto-light", + url_template="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", + subdomains=("a", "b", "c", "d"), + attribution="(c) OpenStreetMap contributors (c) CARTO", + file_extension="png", +) + +dpgm.register_provider(provider) +dpgm.set_provider("carto-light", map_tag="map") +``` + +Provider switching preserves overlays and center, clamps zoom to the new provider range, and +invalidates stale tile loads from the previous provider. + +## Cache + +`dpg-map` uses: + +- A memory cache for decoded runtime tiles +- A persistent provider-namespaced disk cache + +```python +stats = dpgm.get_cache_stats(map_tag="map") +print(stats.memory_tiles, stats.disk_bytes, stats.disk_path) + +dpgm.clear_memory_cache(map_tag="map") +dpgm.clear_disk_cache(provider="osm") +``` + +`clear_memory_cache(...)` is routed through the renderer command queue so Dear PyGui textures are +deleted on the GUI thread. `clear_disk_cache(...)` can clear all providers, one provider, or a +map-scoped cache directory. + +## Sizing + +The map widget is a Dear PyGui child window containing a measured drawlist. + +Common sizing modes: + +- `width=-1`, `height=-1`: fill available space where Dear PyGui supports it +- Positive `width` and `height`: fixed requested size +- `autosize_x=True` or `autosize_y=True`: pass Dear PyGui autosize flags to the child +- Hidden layouts preserve the last non-zero measured size until visible again + +Examples: + +```bash +uv run python examples/sizing_window.py +uv run python examples/sizing_child.py +uv run python examples/sizing_table.py +uv run python examples/hidden_tab.py +``` + +## Multiple Maps + +Multiple maps can be used in the same Dear PyGui app. Give each map a distinct tag and pass +`map_tag` for runtime updates. + +```python +with dpgm.map_widget(tag="live-map", center=(47.9, 1.9), zoom=15, width=-1, height=300): + dpgm.add_marker("vehicle", lat=47.9, lon=1.9) + +with dpgm.map_widget(tag="recap-map", center=(47.9, 1.9), zoom=13, width=-1, height=300): + dpgm.add_trajectory("lap", points=[]) + +dpgm.update_marker("vehicle", lat=47.901, lon=1.902, map_tag="live-map") +dpgm.update_trajectory("lap", points=lap_points, map_tag="recap-map") +``` + +Pan, zoom, tile rendering, overlays, providers, and caches are isolated per map unless you choose +to share global configuration. + +## Diagnostics + +Use `get_map_debug_state(...)` and `get_cache_stats(...)` while developing or troubleshooting: + +```python +debug = dpgm.get_map_debug_state(map_tag="map") +stats = dpgm.get_cache_stats(map_tag="map") +``` + +These snapshots are intended for diagnostics and logging, not as a stable serialization format. diff --git a/pyproject.toml b/pyproject.toml index 51836e2..45a340e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dpg-map" -version = "0.3.0b1" +version = "1.0.0" description = "Dear PyGui map widget for XYZ raster tiles and geographic overlays" readme = "README.md" authors = [ diff --git a/src/dpg_map/tiles.py b/src/dpg_map/tiles.py index e90738d..e38cc6d 100644 --- a/src/dpg_map/tiles.py +++ b/src/dpg_map/tiles.py @@ -316,7 +316,7 @@ class TileManager: RuntimeWarning, stacklevel=3, ) - headers["User-Agent"] = "dpg-map/0.3.0b1" + headers["User-Agent"] = "dpg-map/1.0.0" return headers def _visible_disk_paths(self, cache_dir: str | Path | None) -> list[Path]: diff --git a/uv.lock b/uv.lock index 8cf8684..e4933ea 100644 --- a/uv.lock +++ b/uv.lock @@ -74,7 +74,7 @@ wheels = [ [[package]] name = "dpg-map" -version = "0.3.0b1" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "dearpygui" },