Release v1.0.0
This is the first releas of dpg-map, an AI coded dearpygui map widget built due to a lack of time to build it myself. It features multi-widget abilities, with layers, overlays etc
This commit is contained in:
129
codex/AGENTS.md
Normal file
129
codex/AGENTS.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Current status
|
||||
|
||||
Step 8 complete.
|
||||
|
||||
## Completed steps
|
||||
|
||||
Step 1 - Public API contract and pure core.
|
||||
Step 2 - Thread-safe state, commands, overlays, and cache model.
|
||||
Step 3 - Widget shell, sizing system, and GUI-thread frame pump.
|
||||
Step 4 - Tile manager, persistent cache, and asynchronous loading.
|
||||
Step 5 - Interaction: pan, zoom, and view commands.
|
||||
Step 6 - Overlay rendering and runtime update stress tests.
|
||||
Step 7 - Layers, provider switching, and clearing APIs.
|
||||
Step 8 - Documentation, hardening, and internal release.
|
||||
|
||||
## Current step
|
||||
|
||||
Internal rebuilt beta prepared.
|
||||
|
||||
## Design decisions
|
||||
|
||||
- Package is managed with uv.
|
||||
- Public import is `import dpg_map as dpgm`.
|
||||
- Dear PyGui calls are GUI-thread-only.
|
||||
- Runtime public calls enqueue commands or update logical state.
|
||||
- Overlay updates must not reset center/zoom.
|
||||
- The widget uses child_window + measured-size drawlist.
|
||||
- Tiles use a memory cache and persistent disk cache.
|
||||
- Tile providers are interchangeable.
|
||||
|
||||
## Known issues
|
||||
|
||||
None yet.
|
||||
|
||||
## Commands used
|
||||
|
||||
- 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.
|
||||
- Implemented global configuration, logical MapState, map registry, and current map context stack.
|
||||
- Implemented DirtyFlags, MapCommand, CommandKind, and coalescing MapCommandQueue.
|
||||
- Implemented logical marker, polyline, trajectory, and layer state models.
|
||||
- Implemented public runtime overlay/view/layer/provider/cache/debug wrappers against logical state without Dear PyGui calls.
|
||||
- Implemented memory cache metadata, disk cache path generation, metadata read/write, disk size scanning, and prune planning.
|
||||
- Added Step 2 tests for command coalescing, overlay/view isolation, copied trajectory inputs, coordinate length validation, layer state, disk path generation, and prune ordering.
|
||||
- Implemented `map_widget(...)` as a Dear PyGui child-window plus drawlist shell.
|
||||
- Implemented GUI-thread renderer frame pump that schedules frame callbacks, drains command queues, measures size, resizes the drawlist, and draws a placeholder background/attribution.
|
||||
- Implemented sizing helpers for measured size, last non-zero size preservation, visibility transitions, effective draw size, and resize dirty flags.
|
||||
- Implemented map interaction hit-rectangle calculation.
|
||||
- Implemented TileID, TileStatus, Tile, visible tile calculation, and TileManager.
|
||||
- Implemented asynchronous tile worker queue for disk reads, HTTP fetches, and image decoding.
|
||||
- Implemented provider-namespaced persistent cache writes, access metadata updates, clearing, and LRU pruning.
|
||||
- Implemented memory tile cache with visible-tile protection and deferred GUI-thread texture deletion.
|
||||
- Integrated tile result processing, stale generation/provider rejection, texture creation, and tile drawing into the GUI-thread renderer.
|
||||
- Added OpenStreetMap User-Agent warning/fallback and configured examples with example user agents.
|
||||
- Added Step 3 examples for basic map, window sizing, child-window sizing, table sizing, and hidden-tab sizing.
|
||||
- Added Step 4 cache stress example.
|
||||
- Added Step 3 tests for sizing transitions, zero-size preservation, resize dirty flags, command drain ordering, and hit rectangles.
|
||||
- Added Step 4 tests for visible tile calculation, stale result rejection, protected memory eviction, and tile image decoding.
|
||||
- Ran a Dear PyGui context smoke check for `map_widget` child-window/drawlist creation.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Ran a Dear PyGui context smoke check for `map_widget` child-window/drawlist/texture-registry creation.
|
||||
- Implemented left mouse drag panning using the measured drawlist rectangle.
|
||||
- Implemented mouse wheel zoom around the cursor.
|
||||
- Implemented projection-backed `screen_to_latlon`, `latlon_to_screen`, and zoom-fitting `fit_bounds`.
|
||||
- Attached Dear PyGui mouse handlers through the map handler registry.
|
||||
- Added interaction debug state for active drag and last mouse position.
|
||||
- Added Step 5 tests for pan, cursor zoom, view conversion, bounds fitting, and overlay/view isolation.
|
||||
- Added Pyright virtualenv settings so `uv run pyright` resolves installed dependencies.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Ran a Dear PyGui context smoke check for `map_widget` child-window/drawlist/texture-registry/handler-registry creation.
|
||||
- Implemented Dear PyGui draw-layer bookkeeping for background, tiles, overlays, and attribution.
|
||||
- Rendered markers, polylines, and trajectories from GUI-thread overlay snapshots.
|
||||
- Isolated overlay redraws so they clear only the overlay draw layer and do not clear tile draw commands or textures.
|
||||
- Added live background-thread marker and trajectory stress examples.
|
||||
- Added Step 6 tests for overlay draw-layer isolation, overlay-only dirty flags, threaded update coalescing, and view/drag-state isolation.
|
||||
- Updated `README.md` with Step 6 overlay rendering behavior and examples.
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run pyright`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran a Dear PyGui context smoke check for `map_widget` with marker, polyline, and trajectory overlays.
|
||||
- Added `z_index` support for `add_layer`, layer visibility/clearing tests, and cache-safe map-wide memory clearing.
|
||||
- Implemented provider switching that validates providers, clamps zoom to the new provider range, preserves overlays/center, increments generation, and queues GUI-thread tile invalidation.
|
||||
- Implemented provider-scoped disk cache clearing and cache-size scanning.
|
||||
- Exported `CacheStats` publicly.
|
||||
- Added `examples/custom_provider.py` and cache control/stat buttons to `examples/cache_stress.py`.
|
||||
- Updated `README.md` with Step 7 behavior and examples.
|
||||
- Added Step 7 tests for provider switch tile invalidation, provider-scoped disk clearing, queued disk clear commands, layer z-order updates, and public exports.
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Ran `uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())"`.
|
||||
- Added docstrings for public API functions.
|
||||
- Rewrote `README.md` with uv install, local editable dependency, basic usage, sizing, live update, custom provider, cache, OpenStreetMap, and thread-safety documentation.
|
||||
- Added Step 8 hardening tests for unknown maps, overlays, providers, invalid coordinates, mismatched coordinate lengths, empty trajectory support, deleted overlays, provider switching while tiles are loading, overlay updates during dragging, and public docstrings.
|
||||
- Bumped package version to `0.3.0b1` and updated the fallback OpenStreetMap User-Agent version.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Tested editable install from `/tmp/dpg-map-editable-test` with `uv add --editable /home/hector/projects/dpg-map`.
|
||||
- Ran editable install import check: `uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())"`.
|
||||
- Ran `uv run python -m py_compile` across all example files.
|
||||
- Started `examples/basic_map.py` under a 5-second timeout; it launched without terminal errors and was stopped by timeout because the GUI loop blocks.
|
||||
- Ran `uv sync`.
|
||||
|
||||
## Next action
|
||||
|
||||
Commit and tag `v0.3.0b1`.
|
||||
719
codex/ARCHITECTURE.md
Normal file
719
codex/ARCHITECTURE.md
Normal file
@@ -0,0 +1,719 @@
|
||||
# dpg-map Architecture — Locked Rebuild
|
||||
|
||||
This document defines the internal architecture for the rebuild.
|
||||
|
||||
The central rule is:
|
||||
|
||||
> Public API calls update logical state or enqueue commands. The GUI thread alone owns Dear PyGui items, draw commands, textures, handlers, and frame rendering.
|
||||
|
||||
This prevents flicker, dragging failures, random recentering, and Dear PyGui thread-safety issues when overlays are updated from telemetry threads.
|
||||
|
||||
## High-level architecture
|
||||
|
||||
```text
|
||||
User API
|
||||
↓
|
||||
Thread-safe command/logical-state layer
|
||||
↓
|
||||
Map registry and MapState
|
||||
↓
|
||||
Renderer frame pump on GUI thread
|
||||
↓
|
||||
Dear PyGui child_window + drawlist + textures
|
||||
```
|
||||
|
||||
Detailed flow:
|
||||
|
||||
```text
|
||||
Telemetry thread / app thread / GUI callback
|
||||
↓
|
||||
public dpg_map function
|
||||
↓
|
||||
validate lightweight inputs
|
||||
↓
|
||||
acquire MapState.lock briefly
|
||||
↓
|
||||
update logical model and/or enqueue command
|
||||
↓
|
||||
mark specific dirty flags
|
||||
↓
|
||||
return immediately
|
||||
↓
|
||||
GUI frame callback drains commands
|
||||
↓
|
||||
renderer updates DPG draw layers/textures
|
||||
```
|
||||
|
||||
## Critical invariants
|
||||
|
||||
### Threading invariants
|
||||
|
||||
1. Dear PyGui APIs are called only on the GUI thread.
|
||||
2. Public API functions do not call Dear PyGui directly unless explicitly documented as GUI-thread-only.
|
||||
3. Tile worker threads do network and disk work only.
|
||||
4. Tile workers do not create or delete Dear PyGui textures.
|
||||
5. Texture creation and deletion are queued to the renderer and executed on the GUI thread.
|
||||
6. Runtime overlay updates are coalesced by overlay tag.
|
||||
7. Locks are held briefly and never during network, disk, image decoding, or Dear PyGui calls.
|
||||
|
||||
### View invariants
|
||||
|
||||
1. `MapState.center` and `MapState.zoom` are changed only by view commands or interaction.
|
||||
2. Overlay commands must never change center or zoom.
|
||||
3. Tile loading must never change center or zoom.
|
||||
4. Renderer redraw must render the current state; it must not infer a new center from overlays.
|
||||
5. Provider switching may clamp zoom, but must not recenter unless the current center is invalid for the projection.
|
||||
|
||||
### Rendering invariants
|
||||
|
||||
1. Overlay redraw does not clear tile draw commands.
|
||||
2. Tile redraw does not delete overlay logical state.
|
||||
3. Provider switch clears only provider-specific tile resources.
|
||||
4. Full redraw is reserved for initial render, size change, provider change, or recovery.
|
||||
5. The renderer always reads a consistent snapshot of state for each frame.
|
||||
6. DPG draw item tags are internal and not exposed as public overlay tags.
|
||||
|
||||
### Sizing invariants
|
||||
|
||||
1. The child window receives the requested Dear PyGui sizing intent.
|
||||
2. The drawlist receives concrete measured dimensions.
|
||||
3. Requested size and measured size are stored separately.
|
||||
4. A measured size of zero while hidden must not permanently collapse the map.
|
||||
5. The map should delay tile loading until a non-zero visible size is measured, unless explicitly configured otherwise.
|
||||
6. Resize triggers tile and overlay redraw.
|
||||
|
||||
## Package layout
|
||||
|
||||
```text
|
||||
src/dpg_map/
|
||||
__init__.py
|
||||
api.py
|
||||
widget.py
|
||||
state.py
|
||||
commands.py
|
||||
renderer.py
|
||||
draw_layers.py
|
||||
interaction.py
|
||||
overlays.py
|
||||
providers.py
|
||||
projection.py
|
||||
tiles.py
|
||||
cache.py
|
||||
sizing.py
|
||||
diagnostics.py
|
||||
types.py
|
||||
exceptions.py
|
||||
examples/
|
||||
basic_map.py
|
||||
sizing_window.py
|
||||
sizing_child.py
|
||||
sizing_table.py
|
||||
hidden_tab.py
|
||||
markers_live_thread.py
|
||||
trajectory_live_thread.py
|
||||
custom_provider.py
|
||||
cache_stress.py
|
||||
tests/
|
||||
```
|
||||
|
||||
## Module responsibilities
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Exports the public API only.
|
||||
|
||||
No heavy initialization should happen here except registering the default provider.
|
||||
|
||||
### `api.py`
|
||||
|
||||
Contains thin public API wrappers, especially for runtime calls.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- resolve map/overlay tags
|
||||
- validate simple arguments
|
||||
- enqueue commands
|
||||
- expose stable public functions
|
||||
|
||||
The public API should call into `state.py`, `commands.py`, and `overlays.py`, but should not know renderer internals.
|
||||
|
||||
### `commands.py`
|
||||
|
||||
Defines commands that can be submitted from any thread.
|
||||
|
||||
Suggested command model:
|
||||
|
||||
```python
|
||||
class CommandKind(Enum):
|
||||
SET_VIEW = "set_view"
|
||||
SET_PROVIDER = "set_provider"
|
||||
ADD_OVERLAY = "add_overlay"
|
||||
UPDATE_OVERLAY = "update_overlay"
|
||||
DELETE_OVERLAY = "delete_overlay"
|
||||
SET_LAYER_VISIBILITY = "set_layer_visibility"
|
||||
CLEAR_LAYER = "clear_layer"
|
||||
CLEAR_MAP = "clear_map"
|
||||
CLEAR_MEMORY_CACHE = "clear_memory_cache"
|
||||
CLEAR_DISK_CACHE = "clear_disk_cache"
|
||||
```
|
||||
|
||||
Command dataclass:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MapCommand:
|
||||
kind: CommandKind
|
||||
map_tag: str | int
|
||||
payload: dict[str, Any]
|
||||
created_at: float
|
||||
```
|
||||
|
||||
Coalescing rules:
|
||||
|
||||
- `UPDATE_OVERLAY` commands coalesce by `(map_tag, overlay_tag)`.
|
||||
- repeated `SET_VIEW` commands may coalesce, keeping the newest.
|
||||
- `ADD_OVERLAY`, `DELETE_OVERLAY`, `CLEAR_LAYER`, `CLEAR_MAP`, and `SET_PROVIDER` must preserve order.
|
||||
- provider switches form a barrier; older tile results for the previous provider must be ignored.
|
||||
|
||||
### `state.py`
|
||||
|
||||
Owns configuration, registries, and `MapState`.
|
||||
|
||||
Global config:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DpgMapConfig:
|
||||
user_agent: str | None = None
|
||||
cache_dir: Path | None = None
|
||||
default_provider: str | TileProvider = "osm"
|
||||
memory_cache_max_tiles: int = 512
|
||||
disk_cache_max_bytes: int | None = 2_000_000_000
|
||||
prefetch_margin_tiles: int = 1
|
||||
tile_worker_count: int = 4
|
||||
overlay_update_policy: str = "coalesce"
|
||||
debug: bool = False
|
||||
```
|
||||
|
||||
MapState:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MapState:
|
||||
tag: str | int
|
||||
|
||||
# DPG implementation tags, internal only
|
||||
child_window_tag: str | int
|
||||
drawlist_tag: str | int
|
||||
texture_registry_tag: str | int
|
||||
handler_registry_tag: str | int
|
||||
|
||||
# sizing
|
||||
requested_width: int
|
||||
requested_height: int
|
||||
requested_autosize_x: bool
|
||||
requested_autosize_y: bool
|
||||
measured_width: int = 0
|
||||
measured_height: int = 0
|
||||
last_nonzero_width: int = 0
|
||||
last_nonzero_height: int = 0
|
||||
is_visible: bool = False
|
||||
|
||||
# view
|
||||
center: LatLon = (0.0, 0.0)
|
||||
zoom: int = 2
|
||||
min_zoom: int = 0
|
||||
max_zoom: int = 19
|
||||
provider: TileProvider = field(default_factory=get_default_provider)
|
||||
|
||||
# models
|
||||
overlays: dict[str | int, Overlay] = field(default_factory=dict)
|
||||
layers: dict[str, LayerState] = field(default_factory=default_layers)
|
||||
|
||||
# subsystems
|
||||
command_queue: MapCommandQueue = field(default_factory=MapCommandQueue)
|
||||
tile_manager: TileManager = field(default_factory=TileManager)
|
||||
renderer: MapRenderer | None = None
|
||||
interaction: InteractionState = field(default_factory=InteractionState)
|
||||
|
||||
# concurrency
|
||||
lock: RLock = field(default_factory=RLock)
|
||||
|
||||
# redraw tracking
|
||||
dirty: DirtyFlags = DirtyFlags.FULL
|
||||
frame_scheduled: bool = False
|
||||
generation: int = 0
|
||||
```
|
||||
|
||||
`generation` increments on provider switch, full clear, or other operations that invalidate pending tile results.
|
||||
|
||||
### `types.py`
|
||||
|
||||
Contains aliases and small dataclasses:
|
||||
|
||||
```python
|
||||
LatLon = tuple[float, float]
|
||||
RGBA = tuple[int, int, int, int]
|
||||
ScreenPoint = tuple[float, float]
|
||||
```
|
||||
|
||||
### `exceptions.py`
|
||||
|
||||
Contains public exceptions:
|
||||
|
||||
```python
|
||||
class DpgMapError(Exception): ...
|
||||
class ProviderError(DpgMapError): ...
|
||||
class UnknownProviderError(ProviderError): ...
|
||||
class MapNotFoundError(DpgMapError): ...
|
||||
class OverlayNotFoundError(DpgMapError): ...
|
||||
class CoordinateError(DpgMapError): ...
|
||||
class ThreadingError(DpgMapError): ...
|
||||
class CacheError(DpgMapError): ...
|
||||
```
|
||||
|
||||
### `providers.py`
|
||||
|
||||
Defines the provider system.
|
||||
|
||||
The default OpenStreetMap provider is registered at import time.
|
||||
|
||||
Provider identity is part of `TileID`, so caches are provider-namespaced.
|
||||
|
||||
### `projection.py`
|
||||
|
||||
Pure Web Mercator math only.
|
||||
|
||||
No Dear PyGui imports.
|
||||
|
||||
No global state.
|
||||
|
||||
Must include tests.
|
||||
|
||||
### `overlays.py`
|
||||
|
||||
Defines logical overlay dataclasses only.
|
||||
|
||||
Overlay objects are not Dear PyGui draw items.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Overlay:
|
||||
tag: str | int
|
||||
map_tag: str | int
|
||||
layer: str
|
||||
show: bool = True
|
||||
user_data: Any = None
|
||||
revision: int = 0
|
||||
```
|
||||
|
||||
`revision` increments when an overlay changes. The renderer can use this to decide whether overlay draw data needs rebuilding.
|
||||
|
||||
Marker, Polyline, and Trajectory inherit from `Overlay`.
|
||||
|
||||
Path overlays should copy incoming coordinate sequences into internal tuples/lists during command processing, not store references to mutable user lists. This prevents background buffers changing while the renderer reads them.
|
||||
|
||||
### `widget.py`
|
||||
|
||||
Creates the Dear PyGui structure.
|
||||
|
||||
Internal structure:
|
||||
|
||||
```text
|
||||
child_window
|
||||
drawlist
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- child window receives the user-requested size.
|
||||
- drawlist uses measured concrete content size.
|
||||
- map is registered before entering the context body.
|
||||
- current map stack is pushed for context-style overlay creation.
|
||||
- frame callback is scheduled after creation to measure and render.
|
||||
- resize/visibility handlers mark `SIZE_DIRTY`.
|
||||
|
||||
The widget must not expose `child_window_tag` or `drawlist_tag`.
|
||||
|
||||
### `sizing.py`
|
||||
|
||||
Dedicated sizing helper module.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- measure child content region
|
||||
- maintain last non-zero measured size
|
||||
- decide whether tile loading can begin
|
||||
- resize drawlist safely
|
||||
- detect hidden-to-visible transitions
|
||||
- avoid permanent collapse from zero-size measurements
|
||||
|
||||
Suggested function:
|
||||
|
||||
```python
|
||||
def update_measured_size(state: MapState) -> bool:
|
||||
"""Return True if concrete drawlist size changed."""
|
||||
```
|
||||
|
||||
### `interaction.py`
|
||||
|
||||
Owns panning and zooming.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- hit-test against concrete drawlist rectangle
|
||||
- track active drag state
|
||||
- update center during drag
|
||||
- update zoom during wheel
|
||||
- zoom around cursor where practical
|
||||
- never allow overlay updates to reset interaction state
|
||||
|
||||
Interaction should use the current measured drawlist rect, not only `dpg.is_item_hovered`.
|
||||
|
||||
### `tiles.py`
|
||||
|
||||
Owns tile identity, tile lifecycle, and visible tile calculation.
|
||||
|
||||
TileID:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TileID:
|
||||
provider_name: str
|
||||
z: int
|
||||
x: int
|
||||
y: int
|
||||
```
|
||||
|
||||
Tile states:
|
||||
|
||||
```python
|
||||
class TileStatus(Enum):
|
||||
MISSING = "missing"
|
||||
QUEUED = "queued"
|
||||
LOADING = "loading"
|
||||
READY = "ready"
|
||||
FAILED = "failed"
|
||||
```
|
||||
|
||||
Tile manager responsibilities:
|
||||
|
||||
- compute visible tile IDs
|
||||
- queue missing tiles
|
||||
- avoid duplicate requests
|
||||
- ignore stale tile results from old generations/providers
|
||||
- manage memory cache metadata
|
||||
- request texture creation on GUI thread
|
||||
- request texture deletion on GUI thread
|
||||
- protect currently visible tiles from eviction
|
||||
|
||||
Tile workers:
|
||||
|
||||
- check disk cache
|
||||
- fetch network if needed
|
||||
- decode image bytes to RGBA-compatible data if practical
|
||||
- return loaded data to GUI queue
|
||||
- never call DPG APIs
|
||||
|
||||
### `cache.py`
|
||||
|
||||
Owns persistent disk cache and memory cache policy.
|
||||
|
||||
Disk path:
|
||||
|
||||
```text
|
||||
{cache_dir}/{provider_name}/{z}/{x}/{y}.{ext}
|
||||
```
|
||||
|
||||
Metadata path:
|
||||
|
||||
```text
|
||||
{cache_dir}/{provider_name}/{z}/{x}/{y}.json
|
||||
```
|
||||
|
||||
Metadata should support:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "...",
|
||||
"etag": "...",
|
||||
"last_modified": "...",
|
||||
"expires": "...",
|
||||
"downloaded_at": 0,
|
||||
"last_accessed_at": 0,
|
||||
"size_bytes": 0
|
||||
}
|
||||
```
|
||||
|
||||
MVP disk cache behaviour:
|
||||
|
||||
- file exists and is readable: use it
|
||||
- file missing: fetch it
|
||||
- update `last_accessed_at`
|
||||
- prune by LRU when above `disk_cache_max_bytes`
|
||||
- pruning must not delete files currently loading or currently visible
|
||||
|
||||
HTTP-aware cache headers can be added after the stable MVP.
|
||||
|
||||
### `renderer.py`
|
||||
|
||||
The renderer is GUI-thread-only.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- drain command queue
|
||||
- apply commands to logical state
|
||||
- process tile worker results
|
||||
- create/delete textures
|
||||
- measure size
|
||||
- compute visible tiles
|
||||
- redraw dirty layers
|
||||
- draw attribution/debug information
|
||||
|
||||
The renderer must support separate draw layers. Dear PyGui drawlist does not provide retained sublayers in the same way as a scene graph, so implementation may use internal groups/tags or controlled delete/rebuild by tag prefix. The important rule is behavioural:
|
||||
|
||||
- overlay update must not clear tile textures or tile logical state
|
||||
- tile update must not modify overlay logical state
|
||||
- full redraw must not reset view state
|
||||
|
||||
### `draw_layers.py`
|
||||
|
||||
Optional helper module for draw item bookkeeping.
|
||||
|
||||
Suggested structure:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DrawLayer:
|
||||
name: str
|
||||
item_tags: set[str | int] = field(default_factory=set)
|
||||
```
|
||||
|
||||
Renderer can delete and rebuild only a selected layer.
|
||||
|
||||
### `diagnostics.py`
|
||||
|
||||
Provides debug state for examples and troubleshooting.
|
||||
|
||||
```python
|
||||
def get_map_debug_state(map_tag) -> dict[str, Any]:
|
||||
...
|
||||
```
|
||||
|
||||
Should expose:
|
||||
|
||||
- center
|
||||
- zoom
|
||||
- measured size
|
||||
- requested size
|
||||
- visible tile count
|
||||
- queued/loading/failed tile counts
|
||||
- memory cache count
|
||||
- disk cache bytes
|
||||
- overlay count
|
||||
- dirty flags
|
||||
- active drag state
|
||||
- pending command count
|
||||
- generation
|
||||
|
||||
## Rendering frame lifecycle
|
||||
|
||||
Each frame callback should roughly do:
|
||||
|
||||
```text
|
||||
for each map:
|
||||
1. update measured size
|
||||
2. drain pending public commands
|
||||
3. apply interaction input
|
||||
4. process tile results
|
||||
5. compute visible tiles if view/size/provider changed
|
||||
6. queue missing visible tiles
|
||||
7. create/delete textures requested by tile manager
|
||||
8. redraw required layers based on dirty flags
|
||||
9. clear dirty flags
|
||||
10. schedule next frame only if work remains or map needs continuous interaction polling
|
||||
```
|
||||
|
||||
## Dirty flag model
|
||||
|
||||
Use bit flags:
|
||||
|
||||
```python
|
||||
class DirtyFlags(IntFlag):
|
||||
NONE = 0
|
||||
VIEW = auto()
|
||||
TILES = auto()
|
||||
OVERLAYS = auto()
|
||||
SIZE = auto()
|
||||
PROVIDER = auto()
|
||||
ATTRIBUTION = auto()
|
||||
DEBUG = auto()
|
||||
FULL = VIEW | TILES | OVERLAYS | SIZE | PROVIDER | ATTRIBUTION | DEBUG
|
||||
```
|
||||
|
||||
Expected usage:
|
||||
|
||||
| Operation | Dirty flags |
|
||||
|---|---|
|
||||
| marker update | `OVERLAYS` |
|
||||
| trajectory update | `OVERLAYS` |
|
||||
| layer visibility | `OVERLAYS` |
|
||||
| pan | `VIEW | TILES | OVERLAYS` |
|
||||
| zoom | `VIEW | TILES | OVERLAYS` |
|
||||
| resize | `SIZE | TILES | OVERLAYS` |
|
||||
| provider switch | `PROVIDER | TILES | ATTRIBUTION` |
|
||||
| cache tile ready | `TILES` |
|
||||
| debug setting changed | `DEBUG` |
|
||||
|
||||
## Command queue and coalescing
|
||||
|
||||
The command queue must prevent flooding during telemetry.
|
||||
|
||||
Recommended approach:
|
||||
|
||||
```python
|
||||
class MapCommandQueue:
|
||||
ordered: deque[MapCommand]
|
||||
overlay_updates: dict[tuple[map_tag, overlay_tag], MapCommand]
|
||||
view_update: MapCommand | None
|
||||
lock: Lock
|
||||
```
|
||||
|
||||
When draining:
|
||||
|
||||
1. preserve ordered structural commands
|
||||
2. apply newest coalesced view command
|
||||
3. apply newest overlay updates
|
||||
4. mark dirty flags once
|
||||
|
||||
Structural commands:
|
||||
|
||||
- add overlay
|
||||
- delete overlay
|
||||
- clear layer
|
||||
- clear map
|
||||
- provider switch
|
||||
|
||||
Coalesced commands:
|
||||
|
||||
- update marker
|
||||
- update polyline
|
||||
- update trajectory
|
||||
- set overlay show
|
||||
- set center/zoom/view
|
||||
|
||||
## Tile result generation safety
|
||||
|
||||
Every tile request should include:
|
||||
|
||||
```python
|
||||
provider_name
|
||||
generation
|
||||
tile_id
|
||||
```
|
||||
|
||||
When result returns:
|
||||
|
||||
- if generation no longer matches map generation: discard result
|
||||
- if provider no longer matches: discard result
|
||||
- if tile no longer requested/visible/cacheable: optionally keep disk cache but do not create texture immediately
|
||||
|
||||
This prevents stale workers from drawing old-provider tiles after a provider switch.
|
||||
|
||||
## Persistent disk cache pruning
|
||||
|
||||
Disk cache pruning should run:
|
||||
|
||||
- at startup/open map
|
||||
- after downloads push cache above the limit
|
||||
- optionally manually via `clear_disk_cache`
|
||||
|
||||
Pruning rules:
|
||||
|
||||
1. calculate provider or global cache size
|
||||
2. sort cached tile metadata by `last_accessed_at`
|
||||
3. delete oldest until under target
|
||||
4. never delete files currently visible/loading
|
||||
5. tolerate missing/corrupt metadata
|
||||
|
||||
## Sizing strategy
|
||||
|
||||
The map widget uses two sizes:
|
||||
|
||||
```python
|
||||
requested_width / requested_height
|
||||
measured_width / measured_height
|
||||
```
|
||||
|
||||
The child window is created with requested values:
|
||||
|
||||
```python
|
||||
dpg.add_child_window(width=requested_width, height=requested_height, ...)
|
||||
```
|
||||
|
||||
The drawlist is resized to measured content size:
|
||||
|
||||
```python
|
||||
content_width = dpg.get_item_width(child_window_tag)
|
||||
content_height = dpg.get_item_height(child_window_tag)
|
||||
dpg.configure_item(drawlist_tag, width=content_width, height=content_height)
|
||||
```
|
||||
|
||||
If measured size is zero:
|
||||
|
||||
- keep last non-zero size
|
||||
- do not permanently configure drawlist to zero
|
||||
- if no last non-zero size exists, render placeholder only and delay tile loading
|
||||
|
||||
## Initial setup instructions
|
||||
|
||||
These replace the old Step 0.
|
||||
|
||||
```bash
|
||||
mkdir dpg-map
|
||||
cd dpg-map
|
||||
git init
|
||||
uv init --package dpg-map
|
||||
uv add dearpygui pillow platformdirs requests
|
||||
uv add --dev pytest ruff pyright
|
||||
```
|
||||
|
||||
Create:
|
||||
|
||||
```text
|
||||
src/dpg_map/
|
||||
examples/
|
||||
tests/
|
||||
codex/FEATURES.md
|
||||
codex/ARCHITECTURE.md
|
||||
codex/STEPS.md
|
||||
codex/AGENTS.md
|
||||
README.md
|
||||
```
|
||||
|
||||
Use `codex/AGENTS.md` as the rolling implementation log. After every step:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
git status
|
||||
git add .
|
||||
git commit -m "step N: short description"
|
||||
```
|
||||
|
||||
## Development examples required
|
||||
|
||||
The rebuild must include examples specifically designed to reproduce the first build's failure modes:
|
||||
|
||||
```text
|
||||
examples/basic_map.py
|
||||
examples/sizing_window.py
|
||||
examples/sizing_child.py
|
||||
examples/sizing_table.py
|
||||
examples/hidden_tab.py
|
||||
examples/markers_live_thread.py
|
||||
examples/trajectory_live_thread.py
|
||||
examples/custom_provider.py
|
||||
examples/cache_stress.py
|
||||
```
|
||||
554
codex/FEATURES.md
Normal file
554
codex/FEATURES.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# dpg-map Feature Specification — Locked API Rebuild
|
||||
|
||||
This document defines the public behaviour and API contract for the next rebuild of `dpg-map`.
|
||||
|
||||
The first build reached overlay/layer functionality, but suffered from rendering instability, flickering, bad drag behaviour while overlays were updating, occasional unwanted recentering, cache limitations, and sizing mismatches between the child container and the drawlist.
|
||||
|
||||
The rebuild must treat the public API as a stable contract from the beginning.
|
||||
|
||||
## Project summary
|
||||
|
||||
`dpg-map` is a Python dependency providing a Dear PyGui map widget for raster XYZ map tiles and geographic overlays.
|
||||
|
||||
It should be usable as:
|
||||
|
||||
```python
|
||||
import dpg_map as dpgm
|
||||
```
|
||||
|
||||
The widget should feel natural in Dear PyGui code:
|
||||
|
||||
```python
|
||||
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)
|
||||
dpgm.add_trajectory("lap", lats=[], lons=[])
|
||||
```
|
||||
|
||||
Runtime updates from telemetry or worker threads must be safe:
|
||||
|
||||
```python
|
||||
dpgm.update_marker("vehicle", lat=current_lat, lon=current_lon)
|
||||
dpgm.update_trajectory("lap", lats=lat_buffer, lons=lon_buffer)
|
||||
```
|
||||
|
||||
Those calls must not directly call Dear PyGui APIs, block the caller, reset the map view, flicker the map, or interfere with dragging/zooming.
|
||||
|
||||
## Core requirements
|
||||
|
||||
### 1. Stable public API
|
||||
|
||||
The public API is considered locked once implemented. Internals may change, but user code should not need to change.
|
||||
|
||||
Required public areas:
|
||||
|
||||
- global configuration
|
||||
- tile providers
|
||||
- map widget creation
|
||||
- map view state
|
||||
- overlays
|
||||
- layers
|
||||
- cache configuration
|
||||
- diagnostics/debugging
|
||||
|
||||
### 2. Thread-safe runtime updates
|
||||
|
||||
Any public runtime function may be called from a non-GUI thread unless explicitly documented otherwise.
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
dpgm.update_marker(...)
|
||||
dpgm.update_polyline(...)
|
||||
dpgm.update_trajectory(...)
|
||||
dpgm.set_overlay_show(...)
|
||||
dpgm.delete_overlay(...)
|
||||
dpgm.set_center(...)
|
||||
dpgm.set_zoom(...)
|
||||
dpgm.set_view(...)
|
||||
dpgm.set_provider(...)
|
||||
dpgm.clear_layer(...)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Public runtime functions must be non-blocking or near non-blocking.
|
||||
- They must never call Dear PyGui draw, texture, item, handler, or viewport APIs directly.
|
||||
- They must update a thread-safe logical state model and/or enqueue commands.
|
||||
- The GUI thread drains the queue and performs Dear PyGui work.
|
||||
- Overlay updates must not mutate map center, zoom, pan state, active drag state, or tile provider unless the function is explicitly a view/provider function.
|
||||
- Overlay updates must be coalesced, so a telemetry thread can update at high rate without flooding the GUI thread.
|
||||
|
||||
### 3. Interaction must remain stable during overlay updates
|
||||
|
||||
While markers, polylines, or trajectories are being added/updated/deleted:
|
||||
|
||||
- the user must still be able to drag the map
|
||||
- the user must still be able to zoom
|
||||
- the map must not recenter unexpectedly
|
||||
- the map must not flash through an empty state
|
||||
- tile draw commands should not be destroyed just because an overlay changed
|
||||
- overlay draw refreshes should be isolated from tile draw refreshes where possible
|
||||
|
||||
### 4. Persistent tile caching
|
||||
|
||||
The cache must have two independent limits:
|
||||
|
||||
```python
|
||||
memory_cache_max_tiles: int = 512
|
||||
disk_cache_max_bytes: int | None = None
|
||||
```
|
||||
|
||||
Memory cache:
|
||||
|
||||
- runtime only
|
||||
- LRU or approximate LRU
|
||||
- stores decoded tile data and/or active texture references
|
||||
- protects currently visible tiles from eviction
|
||||
- evicts Dear PyGui textures only on the GUI thread
|
||||
|
||||
Disk cache:
|
||||
|
||||
- persistent across application restarts
|
||||
- provider-namespaced
|
||||
- configurable path
|
||||
- configurable size limit
|
||||
- pruned in the background or during startup
|
||||
- avoids repeatedly fetching regularly used tiles
|
||||
|
||||
Default disk cache path should use `platformdirs` unless overridden.
|
||||
|
||||
### 5. Sizing must be robust
|
||||
|
||||
The Dear PyGui child container and internal drawlist must stay in sync.
|
||||
|
||||
Requirements:
|
||||
|
||||
- `width=0` and `height=0` preserve Dear PyGui default behaviour.
|
||||
- `width=-1` fills available width.
|
||||
- `height=-1` fills available height where Dear PyGui allows it.
|
||||
- fixed positive dimensions are respected.
|
||||
- `autosize_x` and `autosize_y` are supported where practical.
|
||||
- the child window stores the requested sizing intent.
|
||||
- the drawlist always uses measured concrete pixel dimensions.
|
||||
- the drawlist must resize when the child content region changes.
|
||||
- the map must work inside windows, groups, child windows, tabs, tables, and hidden/show-later layouts.
|
||||
- the renderer must not get stuck at the first measured size.
|
||||
|
||||
### 6. Interchangeable TileProvider system
|
||||
|
||||
OpenStreetMap is the default provider, but users must be able to use custom XYZ providers.
|
||||
|
||||
```python
|
||||
provider = dpgm.TileProvider(
|
||||
name="custom",
|
||||
url_template="https://example.com/{z}/{x}/{y}.png",
|
||||
attribution="Tiles © Example",
|
||||
)
|
||||
dpgm.register_provider(provider)
|
||||
```
|
||||
|
||||
Provider switching should preserve overlays and the current center where possible.
|
||||
|
||||
### 7. uv-first package management
|
||||
|
||||
The project must be managed with `uv`.
|
||||
|
||||
Users should be able to install locally with:
|
||||
|
||||
```bash
|
||||
uv add -e ../dpg-map
|
||||
```
|
||||
|
||||
The package must support normal `uv add`, `uv sync`, `uv run pytest`, and editable local development.
|
||||
|
||||
## Non-goals for the first rebuilt version
|
||||
|
||||
Do not implement these in the first rebuild:
|
||||
|
||||
- vector map rendering
|
||||
- Mapbox GL rendering
|
||||
- geocoding/search
|
||||
- routing
|
||||
- offline planet files
|
||||
- arbitrary projections
|
||||
- map rotation/pitch
|
||||
- complex hit testing for lines/polygons
|
||||
- GPU-accelerated custom renderers
|
||||
- tile blending/fade animations
|
||||
|
||||
## Public API
|
||||
|
||||
### Global configuration
|
||||
|
||||
```python
|
||||
dpgm.configure(
|
||||
*,
|
||||
user_agent: str | None = None,
|
||||
cache_dir: str | Path | None = None,
|
||||
default_provider: str | dpgm.TileProvider = "osm",
|
||||
memory_cache_max_tiles: int = 512,
|
||||
disk_cache_max_bytes: int | None = 2_000_000_000,
|
||||
prefetch_margin_tiles: int = 1,
|
||||
tile_worker_count: int = 4,
|
||||
overlay_update_policy: str = "coalesce",
|
||||
debug: bool = False,
|
||||
) -> None
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `disk_cache_max_bytes=None` means unlimited disk cache.
|
||||
- `overlay_update_policy="coalesce"` means only the newest update for each overlay is rendered.
|
||||
- OpenStreetMap usage should require or strongly warn about a valid application-specific `user_agent`.
|
||||
|
||||
### TileProvider
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TileProvider:
|
||||
name: str
|
||||
url_template: str
|
||||
min_zoom: int = 0
|
||||
max_zoom: int = 19
|
||||
tile_size: int = 256
|
||||
attribution: str = ""
|
||||
headers: dict[str, str] = field(default_factory=dict)
|
||||
subdomains: tuple[str, ...] = ()
|
||||
retina: bool = False
|
||||
file_extension: str | None = None
|
||||
```
|
||||
|
||||
Provider helpers:
|
||||
|
||||
```python
|
||||
dpgm.register_provider(provider: dpgm.TileProvider) -> None
|
||||
dpgm.unregister_provider(name: str) -> None
|
||||
dpgm.get_provider(name: str) -> dpgm.TileProvider
|
||||
dpgm.list_providers() -> list[str]
|
||||
```
|
||||
|
||||
Default provider:
|
||||
|
||||
```python
|
||||
OSM = TileProvider(
|
||||
name="osm",
|
||||
url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
min_zoom=0,
|
||||
max_zoom=19,
|
||||
tile_size=256,
|
||||
attribution="© OpenStreetMap contributors",
|
||||
)
|
||||
```
|
||||
|
||||
Provider URL template rules:
|
||||
|
||||
- `{z}`, `{x}`, and `{y}` are required.
|
||||
- `{s}` is optional for subdomains.
|
||||
- `{r}` is optional for retina suffixes.
|
||||
- invalid templates must raise a clear provider exception.
|
||||
|
||||
### Map widget
|
||||
|
||||
```python
|
||||
with dpgm.map_widget(
|
||||
tag: str | int | None = None,
|
||||
center: tuple[float, float] = (0.0, 0.0),
|
||||
zoom: int = 2,
|
||||
min_zoom: int | None = None,
|
||||
max_zoom: int | None = None,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
autosize_x: bool = False,
|
||||
autosize_y: bool = False,
|
||||
border: bool = True,
|
||||
provider: str | dpgm.TileProvider | None = None,
|
||||
cache_dir: str | Path | None = None,
|
||||
user_agent: str | None = None,
|
||||
no_scrollbar: bool = True,
|
||||
delay_tile_load_until_visible: bool = True,
|
||||
) as map_tag:
|
||||
...
|
||||
```
|
||||
|
||||
The returned `map_tag` is the logical map tag, not a Dear PyGui item tag.
|
||||
|
||||
### View state API
|
||||
|
||||
```python
|
||||
dpgm.set_center(map_tag, lat: float, lon: float) -> None
|
||||
dpgm.get_center(map_tag) -> tuple[float, float]
|
||||
|
||||
dpgm.set_zoom(map_tag, zoom: int) -> None
|
||||
dpgm.get_zoom(map_tag) -> int
|
||||
|
||||
dpgm.set_view(
|
||||
map_tag,
|
||||
*,
|
||||
center: tuple[float, float] | None = None,
|
||||
zoom: int | None = None,
|
||||
) -> None
|
||||
|
||||
dpgm.fit_bounds(
|
||||
map_tag,
|
||||
min_lat: float,
|
||||
min_lon: float,
|
||||
max_lat: float,
|
||||
max_lon: float,
|
||||
padding: int = 32,
|
||||
) -> None
|
||||
|
||||
dpgm.screen_to_latlon(map_tag, x: float, y: float) -> tuple[float, float]
|
||||
dpgm.latlon_to_screen(map_tag, lat: float, lon: float) -> tuple[float, float]
|
||||
```
|
||||
|
||||
View state commands may be called from other threads, but they are queued and applied on the GUI thread. `get_*` functions return the latest committed logical state.
|
||||
|
||||
### Overlay creation API
|
||||
|
||||
All overlay creation functions can be used inside a map context or at runtime with `map_tag`.
|
||||
|
||||
#### Marker
|
||||
|
||||
```python
|
||||
dpgm.add_marker(
|
||||
tag: str | int,
|
||||
lat: float,
|
||||
lon: float,
|
||||
*,
|
||||
label: str | None = None,
|
||||
color: tuple[int, int, int, int] = (255, 80, 80, 255),
|
||||
radius: float = 5.0,
|
||||
layer: str = "markers",
|
||||
show: bool = True,
|
||||
show_label: bool = False,
|
||||
user_data: Any = None,
|
||||
callback: Callable | None = None,
|
||||
map_tag: str | int | None = None,
|
||||
) -> str | int
|
||||
```
|
||||
|
||||
#### Polyline
|
||||
|
||||
```python
|
||||
dpgm.add_polyline(
|
||||
tag: str | int,
|
||||
points: Sequence[tuple[float, float]] | None = None,
|
||||
*,
|
||||
lats: Sequence[float] | None = None,
|
||||
lons: Sequence[float] | None = None,
|
||||
color: tuple[int, int, int, int] = (80, 180, 255, 255),
|
||||
thickness: float = 2.0,
|
||||
layer: str = "lines",
|
||||
show: bool = True,
|
||||
closed: bool = False,
|
||||
simplify: bool = True,
|
||||
user_data: Any = None,
|
||||
map_tag: str | int | None = None,
|
||||
) -> str | int
|
||||
```
|
||||
|
||||
#### Trajectory
|
||||
|
||||
```python
|
||||
dpgm.add_trajectory(
|
||||
tag: str | int,
|
||||
lats: Sequence[float],
|
||||
lons: Sequence[float],
|
||||
*,
|
||||
timestamps: Sequence[float] | None = None,
|
||||
color: tuple[int, int, int, int] = (255, 180, 60, 255),
|
||||
thickness: float = 2.0,
|
||||
show_points: bool = False,
|
||||
point_stride: int = 1,
|
||||
layer: str = "trajectories",
|
||||
show: bool = True,
|
||||
user_data: Any = None,
|
||||
map_tag: str | int | None = None,
|
||||
) -> str | int
|
||||
```
|
||||
|
||||
### Overlay update API
|
||||
|
||||
Overlay update APIs must be safe from background threads and must not directly touch Dear PyGui.
|
||||
|
||||
```python
|
||||
dpgm.update_marker(
|
||||
tag: str | int,
|
||||
*,
|
||||
lat: float | None = None,
|
||||
lon: float | None = None,
|
||||
label: str | None = None,
|
||||
show: bool | None = None,
|
||||
color: tuple[int, int, int, int] | None = None,
|
||||
radius: float | None = None,
|
||||
) -> None
|
||||
```
|
||||
|
||||
```python
|
||||
dpgm.update_polyline(
|
||||
tag: str | int,
|
||||
points: Sequence[tuple[float, float]] | None = None,
|
||||
*,
|
||||
lats: Sequence[float] | None = None,
|
||||
lons: Sequence[float] | None = None,
|
||||
show: bool | None = None,
|
||||
color: tuple[int, int, int, int] | None = None,
|
||||
thickness: float | None = None,
|
||||
) -> None
|
||||
```
|
||||
|
||||
```python
|
||||
dpgm.update_trajectory(
|
||||
tag: str | int,
|
||||
*,
|
||||
lats: Sequence[float] | None = None,
|
||||
lons: Sequence[float] | None = None,
|
||||
timestamps: Sequence[float] | None = None,
|
||||
show: bool | None = None,
|
||||
color: tuple[int, int, int, int] | None = None,
|
||||
thickness: float | None = None,
|
||||
) -> None
|
||||
```
|
||||
|
||||
Convenience aliases:
|
||||
|
||||
```python
|
||||
dpgm.set_marker_position(tag, lat, lon) -> None
|
||||
dpgm.set_marker_label(tag, label) -> None
|
||||
dpgm.set_polyline_points(tag, points=None, *, lats=None, lons=None) -> None
|
||||
dpgm.set_overlay_show(tag, show: bool) -> None
|
||||
dpgm.delete_overlay(tag) -> None
|
||||
```
|
||||
|
||||
### Layer API
|
||||
|
||||
```python
|
||||
dpgm.add_layer(map_tag, name: str, z_index: int) -> None
|
||||
dpgm.show_layer(map_tag, name: str) -> None
|
||||
dpgm.hide_layer(map_tag, name: str) -> None
|
||||
dpgm.clear_layer(map_tag, name: str) -> None
|
||||
```
|
||||
|
||||
Layer operations are queued and applied by the GUI thread.
|
||||
|
||||
### Provider switching
|
||||
|
||||
```python
|
||||
dpgm.set_provider(map_tag, provider: str | dpgm.TileProvider) -> None
|
||||
```
|
||||
|
||||
Provider switching must:
|
||||
|
||||
- keep overlays
|
||||
- keep center where possible
|
||||
- clamp zoom to provider limits
|
||||
- clear or detach old tile draw layer only
|
||||
- update attribution
|
||||
- not rebuild overlays unnecessarily
|
||||
- not call Dear PyGui from the caller thread
|
||||
|
||||
### Cache control API
|
||||
|
||||
```python
|
||||
dpgm.clear_memory_cache(map_tag: str | int | None = None) -> None
|
||||
dpgm.clear_disk_cache(provider: str | None = None) -> None
|
||||
dpgm.get_cache_stats(map_tag: str | int | None = None) -> dpgm.CacheStats
|
||||
```
|
||||
|
||||
`CacheStats` should include at least:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class CacheStats:
|
||||
memory_tiles: int
|
||||
memory_limit_tiles: int
|
||||
disk_bytes: int
|
||||
disk_limit_bytes: int | None
|
||||
queued_tiles: int
|
||||
loading_tiles: int
|
||||
failed_tiles: int
|
||||
```
|
||||
|
||||
### Diagnostics
|
||||
|
||||
```python
|
||||
dpgm.get_map_debug_state(map_tag) -> dict[str, Any]
|
||||
```
|
||||
|
||||
Used to debug:
|
||||
|
||||
- measured child size
|
||||
- drawlist size
|
||||
- center/zoom
|
||||
- visible tile count
|
||||
- overlay count
|
||||
- pending command count
|
||||
- cache stats
|
||||
- whether the map is currently considered visible/hovered/dragging
|
||||
|
||||
## Rendering stability requirements
|
||||
|
||||
### Dirty flags
|
||||
|
||||
The implementation must separate dirty reasons:
|
||||
|
||||
```python
|
||||
VIEW_DIRTY
|
||||
TILES_DIRTY
|
||||
OVERLAYS_DIRTY
|
||||
SIZE_DIRTY
|
||||
PROVIDER_DIRTY
|
||||
FULL_DIRTY
|
||||
```
|
||||
|
||||
Overlay updates should set `OVERLAYS_DIRTY`, not `FULL_DIRTY`.
|
||||
|
||||
Panning/zooming should set `VIEW_DIRTY` and likely `TILES_DIRTY | OVERLAYS_DIRTY`.
|
||||
|
||||
Resizing should set `SIZE_DIRTY | TILES_DIRTY | OVERLAYS_DIRTY`.
|
||||
|
||||
Provider switching should set `PROVIDER_DIRTY | TILES_DIRTY`, but should not delete overlay logical state.
|
||||
|
||||
### Draw layers
|
||||
|
||||
Internally, the renderer should use separate draw layers/groups where possible:
|
||||
|
||||
```text
|
||||
background layer
|
||||
tile layer
|
||||
overlay layer
|
||||
attribution/debug layer
|
||||
```
|
||||
|
||||
Overlay redraws should not wipe tile draw commands unless the view, size, or provider changed.
|
||||
|
||||
### View ownership
|
||||
|
||||
Only these may intentionally change map center/zoom:
|
||||
|
||||
- user pan/zoom interaction
|
||||
- `set_center`
|
||||
- `set_zoom`
|
||||
- `set_view`
|
||||
- `fit_bounds`
|
||||
- provider switch only if zoom must be clamped
|
||||
|
||||
Overlay creation/update/delete must never recenter the map.
|
||||
|
||||
## Acceptance tests for the rebuild
|
||||
|
||||
The following manual and automated examples are required before treating the rebuild as usable:
|
||||
|
||||
1. Basic map shows OSM tiles.
|
||||
2. Map fills a resizable Dear PyGui window.
|
||||
3. Map inside a child window fills its container.
|
||||
4. Map inside a table cell fills its cell.
|
||||
5. Map inside a hidden tab starts loading when shown.
|
||||
6. Dragging works while a marker is updated at 20–60 Hz from another thread.
|
||||
7. Dragging works while a trajectory is updated at 10–30 Hz from another thread.
|
||||
8. No unexpected center change occurs during overlay updates.
|
||||
9. Returning to a recently viewed area uses memory cache.
|
||||
10. Restarting the app and viewing the same area uses disk cache.
|
||||
11. Disk cache prunes to the configured limit.
|
||||
12. Provider switch keeps overlays.
|
||||
13. `uv add -e ../dpg-map` works from another project.
|
||||
18
codex/README.md
Normal file
18
codex/README.md
Normal file
@@ -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`
|
||||
764
codex/STEPS.md
Normal file
764
codex/STEPS.md
Normal file
@@ -0,0 +1,764 @@
|
||||
# dpg-map Rebuild Steps
|
||||
|
||||
This file replaces the old long step list.
|
||||
|
||||
There is no Step 0. Initial setup is listed separately, then implementation starts at Step 1.
|
||||
|
||||
## Workflow rules
|
||||
|
||||
1. Use `uv` for all Python package and dependency management.
|
||||
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 `codex/AGENTS.md`
|
||||
- run relevant checks
|
||||
- commit to git
|
||||
6. Do not casually change public API once introduced.
|
||||
7. Public runtime APIs must be safe to call from background threads unless explicitly documented otherwise.
|
||||
8. Dear PyGui calls must happen on the GUI thread only.
|
||||
9. Prefer stable, boring implementation over clever rendering tricks.
|
||||
|
||||
## Initial setup instructions
|
||||
|
||||
Run these before Step 1 when starting from a clean repository:
|
||||
|
||||
```bash
|
||||
mkdir dpg-map
|
||||
cd dpg-map
|
||||
git init
|
||||
uv init --package dpg-map
|
||||
uv add dearpygui pillow platformdirs requests
|
||||
uv add --dev pytest ruff pyright
|
||||
```
|
||||
|
||||
Create this structure:
|
||||
|
||||
```text
|
||||
src/dpg_map/
|
||||
__init__.py
|
||||
api.py
|
||||
widget.py
|
||||
state.py
|
||||
commands.py
|
||||
renderer.py
|
||||
draw_layers.py
|
||||
interaction.py
|
||||
overlays.py
|
||||
providers.py
|
||||
projection.py
|
||||
tiles.py
|
||||
cache.py
|
||||
sizing.py
|
||||
diagnostics.py
|
||||
types.py
|
||||
exceptions.py
|
||||
examples/
|
||||
tests/
|
||||
codex/FEATURES.md
|
||||
codex/ARCHITECTURE.md
|
||||
codex/STEPS.md
|
||||
codex/AGENTS.md
|
||||
README.md
|
||||
```
|
||||
|
||||
Recommended `pyproject.toml` additions:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
```
|
||||
|
||||
Create `codex/AGENTS.md` with:
|
||||
|
||||
```markdown
|
||||
# AGENTS.md
|
||||
|
||||
## Current status
|
||||
|
||||
Rebuild initialized.
|
||||
|
||||
## Completed steps
|
||||
|
||||
None yet.
|
||||
|
||||
## Current step
|
||||
|
||||
Step 1 — Public API contract and pure core.
|
||||
|
||||
## Design decisions
|
||||
|
||||
- Package is managed with uv.
|
||||
- Public import is `import dpg_map as dpgm`.
|
||||
- Dear PyGui calls are GUI-thread-only.
|
||||
- Runtime public calls enqueue commands or update logical state.
|
||||
- Overlay updates must not reset center/zoom.
|
||||
- The widget uses child_window + measured-size drawlist.
|
||||
- Tiles use a memory cache and persistent disk cache.
|
||||
- Tile providers are interchangeable.
|
||||
|
||||
## Known issues
|
||||
|
||||
None yet.
|
||||
|
||||
## Commands used
|
||||
|
||||
None yet.
|
||||
|
||||
## Next action
|
||||
|
||||
Implement Step 1.
|
||||
```
|
||||
|
||||
Initial commit:
|
||||
|
||||
```bash
|
||||
uv run python -c "import dpg_map; print(dpg_map.__name__)"
|
||||
uv run ruff check .
|
||||
uv run ruff format .
|
||||
git add .
|
||||
git commit -m "initial uv project setup"
|
||||
```
|
||||
|
||||
## Step 1 — Public API contract and pure core
|
||||
|
||||
Goal: lock the public API surface and implement pure non-DPG components first.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Define public exports in `__init__.py`.
|
||||
|
||||
Required API names:
|
||||
|
||||
```python
|
||||
configure
|
||||
|
||||
TileProvider
|
||||
register_provider
|
||||
unregister_provider
|
||||
get_provider
|
||||
list_providers
|
||||
|
||||
map_widget
|
||||
|
||||
set_center
|
||||
get_center
|
||||
set_zoom
|
||||
get_zoom
|
||||
set_view
|
||||
fit_bounds
|
||||
screen_to_latlon
|
||||
latlon_to_screen
|
||||
|
||||
add_marker
|
||||
add_polyline
|
||||
add_trajectory
|
||||
update_marker
|
||||
update_polyline
|
||||
update_trajectory
|
||||
set_marker_position
|
||||
set_marker_label
|
||||
set_polyline_points
|
||||
set_overlay_show
|
||||
delete_overlay
|
||||
|
||||
add_layer
|
||||
show_layer
|
||||
hide_layer
|
||||
clear_layer
|
||||
clear_map
|
||||
|
||||
set_provider
|
||||
|
||||
clear_memory_cache
|
||||
clear_disk_cache
|
||||
get_cache_stats
|
||||
get_map_debug_state
|
||||
```
|
||||
|
||||
2. Add exceptions in `exceptions.py`.
|
||||
|
||||
3. Add common types in `types.py`.
|
||||
|
||||
4. Implement `TileProvider` and provider registry in `providers.py`.
|
||||
|
||||
5. Implement Web Mercator projection in `projection.py`.
|
||||
|
||||
6. Implement initial cache dataclasses and `CacheStats`.
|
||||
|
||||
7. Stub GUI-dependent public functions so imports succeed.
|
||||
|
||||
8. Add tests for:
|
||||
- package imports
|
||||
- provider registration
|
||||
- provider URL building
|
||||
- invalid provider templates
|
||||
- projection roundtrips
|
||||
- cache stats dataclass construction
|
||||
|
||||
Checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- public API imports cleanly
|
||||
- provider system works
|
||||
- projection tests pass
|
||||
- no Dear PyGui context is required for tests
|
||||
- API names are fixed before renderer work begins
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 1: lock public api and pure core"
|
||||
```
|
||||
|
||||
## Step 2 — Thread-safe state, commands, overlays, and cache model
|
||||
|
||||
Goal: build the logical state model before rendering anything.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement `DpgMapConfig` and `configure(...)`.
|
||||
|
||||
2. Implement `MapState`.
|
||||
|
||||
3. Implement global map registry and current map context stack.
|
||||
|
||||
4. Implement `DirtyFlags`.
|
||||
|
||||
5. Implement `MapCommand`, `CommandKind`, and `MapCommandQueue`.
|
||||
|
||||
6. Implement coalescing rules:
|
||||
- latest overlay update per overlay wins
|
||||
- latest view update wins
|
||||
- structural commands preserve order
|
||||
|
||||
7. Implement logical overlay dataclasses:
|
||||
- `Overlay`
|
||||
- `MarkerOverlay`
|
||||
- `PolylineOverlay`
|
||||
- `TrajectoryOverlay`
|
||||
- `LayerState`
|
||||
|
||||
8. Implement public overlay creation/update functions so they:
|
||||
- resolve map context or `map_tag`
|
||||
- validate inputs
|
||||
- copy coordinate sequences
|
||||
- enqueue commands or mutate logical model under lock
|
||||
- never call Dear PyGui
|
||||
|
||||
9. Implement cache model:
|
||||
- memory cache metadata
|
||||
- disk path generation
|
||||
- disk metadata read/write
|
||||
- disk size scanning
|
||||
- prune planning without deleting active files yet
|
||||
|
||||
10. Add tests for:
|
||||
- command coalescing
|
||||
- overlay update does not alter center/zoom
|
||||
- trajectory input lists are copied
|
||||
- mismatched lat/lon lengths raise
|
||||
- layer state
|
||||
- disk cache path generation
|
||||
- disk cache prune ordering
|
||||
|
||||
Checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- runtime overlay update functions are thread-safe at the logical level
|
||||
- no public overlay function calls Dear PyGui
|
||||
- center/zoom cannot be changed by overlay commands
|
||||
- cache has separate memory and disk configuration
|
||||
- persistent disk cache layout is decided
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 2: add thread safe state commands and cache model"
|
||||
```
|
||||
|
||||
## Step 3 — Widget shell, sizing system, and GUI-thread frame pump
|
||||
|
||||
Goal: create a stable Dear PyGui widget shell before loading real tiles.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement `map_widget(...)`.
|
||||
|
||||
2. Internally create:
|
||||
|
||||
```text
|
||||
child_window
|
||||
drawlist
|
||||
```
|
||||
|
||||
3. Child window receives requested sizing values.
|
||||
|
||||
4. Drawlist receives concrete measured size only.
|
||||
|
||||
5. Implement `sizing.py`:
|
||||
- measure child content region
|
||||
- track last non-zero size
|
||||
- resize drawlist
|
||||
- detect hidden/show-later transitions
|
||||
- avoid permanent zero-size collapse
|
||||
|
||||
6. Implement the renderer frame pump:
|
||||
- schedules frame callbacks
|
||||
- drains commands on GUI thread
|
||||
- updates size
|
||||
- draws background and attribution placeholder
|
||||
- exposes debug state
|
||||
|
||||
7. Implement map interaction hit-test rectangle calculation, but not full panning yet.
|
||||
|
||||
8. Add examples:
|
||||
- `examples/basic_map.py`
|
||||
- `examples/sizing_window.py`
|
||||
- `examples/sizing_child.py`
|
||||
- `examples/sizing_table.py`
|
||||
- `examples/hidden_tab.py`
|
||||
|
||||
9. Add tests where possible without opening a GUI:
|
||||
- sizing state transitions
|
||||
- zero-size does not overwrite last non-zero size
|
||||
- dirty flags from resize
|
||||
- command drain ordering
|
||||
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
uv run python examples/basic_map.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
|
||||
```
|
||||
|
||||
Automated checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- widget appears
|
||||
- map area fills its child container correctly
|
||||
- drawlist size matches measured child content size
|
||||
- hidden tab example loads/render placeholder after becoming visible
|
||||
- no map gets stuck at zero size
|
||||
- public API remains unchanged
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 3: add widget shell sizing and frame pump"
|
||||
```
|
||||
|
||||
## Step 4 — Tile manager, persistent cache, and asynchronous loading
|
||||
|
||||
Goal: implement stable tile loading and both cache limits before advanced overlays.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement `TileID`, `TileStatus`, `Tile`, and `TileManager`.
|
||||
|
||||
2. Implement visible tile calculation.
|
||||
|
||||
3. Implement persistent disk cache:
|
||||
- provider-namespaced paths
|
||||
- metadata files
|
||||
- `last_accessed_at`
|
||||
- disk size scanning
|
||||
- LRU pruning to `disk_cache_max_bytes`
|
||||
|
||||
4. Implement memory cache:
|
||||
- LRU or approximate LRU
|
||||
- `memory_cache_max_tiles`
|
||||
- visible-tile protection
|
||||
- deferred GUI-thread texture deletion
|
||||
|
||||
5. Implement worker thread/tile queue:
|
||||
- read disk cache
|
||||
- fetch HTTP if missing
|
||||
- decode tile image
|
||||
- return tile result with map generation and provider name
|
||||
- never call Dear PyGui
|
||||
|
||||
6. Renderer GUI thread:
|
||||
- processes tile results
|
||||
- ignores stale generation/provider results
|
||||
- creates textures
|
||||
- deletes evicted textures
|
||||
- draws tile layer
|
||||
|
||||
7. Ensure OpenStreetMap provider uses configured User-Agent or warns/raises clearly.
|
||||
|
||||
8. Add `examples/cache_stress.py`.
|
||||
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
uv run python examples/basic_map.py
|
||||
uv run python examples/cache_stress.py
|
||||
```
|
||||
|
||||
Automated checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- OSM tiles display
|
||||
- GUI does not freeze while tiles load
|
||||
- only visible tiles plus margin are requested
|
||||
- returning to recently viewed area uses memory cache
|
||||
- restarting app uses disk cache
|
||||
- disk cache prunes to configured limit
|
||||
- stale tile results are ignored after provider/generation change
|
||||
- texture deletion happens on GUI thread
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 4: add async tiles and persistent cache"
|
||||
```
|
||||
|
||||
## Step 5 — Interaction: pan, zoom, and view commands
|
||||
|
||||
Goal: make interaction stable before adding live overlays.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement left mouse drag panning.
|
||||
|
||||
2. Implement mouse wheel zoom.
|
||||
|
||||
3. Zoom around cursor where practical.
|
||||
|
||||
4. Implement programmatic view commands:
|
||||
- `set_center`
|
||||
- `set_zoom`
|
||||
- `set_view`
|
||||
- `fit_bounds`
|
||||
- `screen_to_latlon`
|
||||
- `latlon_to_screen`
|
||||
|
||||
5. Interaction must:
|
||||
- use concrete map rectangle
|
||||
- work in nested containers
|
||||
- not depend only on child-window hover state
|
||||
- not reset when overlay commands are queued
|
||||
- not recenter from tile results
|
||||
|
||||
6. Add debug fields for:
|
||||
- active drag
|
||||
- last mouse position
|
||||
- current center
|
||||
- current zoom
|
||||
- dirty flags
|
||||
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
uv run python examples/basic_map.py
|
||||
uv run python examples/sizing_child.py
|
||||
uv run python examples/sizing_table.py
|
||||
```
|
||||
|
||||
Automated checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- dragging pans smoothly
|
||||
- mouse wheel zooms
|
||||
- tile grid remains aligned
|
||||
- map works in nested containers
|
||||
- view commands work
|
||||
- no unexpected recenter occurs from tile loading
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 5: add stable pan zoom and view commands"
|
||||
```
|
||||
|
||||
## Step 6 — Overlay rendering and runtime update stress tests
|
||||
|
||||
Goal: implement overlays in a way that cannot break interaction.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement marker rendering.
|
||||
|
||||
2. Implement polyline rendering.
|
||||
|
||||
3. Implement trajectory rendering.
|
||||
|
||||
4. Implement overlay layer rendering separate from tile layer.
|
||||
|
||||
5. Overlay updates must set only `OVERLAYS` dirty unless a structural map operation requires more.
|
||||
|
||||
6. Overlay draw rebuild must not:
|
||||
- clear tile textures
|
||||
- clear tile draw state
|
||||
- reset center
|
||||
- reset zoom
|
||||
- reset active drag state
|
||||
- rebuild the whole map unnecessarily
|
||||
|
||||
7. Implement:
|
||||
- `add_marker`
|
||||
- `add_polyline`
|
||||
- `add_trajectory`
|
||||
- `update_marker`
|
||||
- `update_polyline`
|
||||
- `update_trajectory`
|
||||
- `set_marker_position`
|
||||
- `set_marker_label`
|
||||
- `set_polyline_points`
|
||||
- `set_overlay_show`
|
||||
- `delete_overlay`
|
||||
|
||||
8. Add examples:
|
||||
- `examples/markers_live_thread.py`
|
||||
- `examples/trajectory_live_thread.py`
|
||||
|
||||
The live examples must update overlays from a background thread while the user drags and zooms.
|
||||
|
||||
Manual stress checks:
|
||||
|
||||
```bash
|
||||
uv run python examples/markers_live_thread.py
|
||||
uv run python examples/trajectory_live_thread.py
|
||||
```
|
||||
|
||||
While examples run:
|
||||
|
||||
- drag continuously for at least 30 seconds
|
||||
- zoom in and out
|
||||
- verify no flicker through blank map
|
||||
- verify no random recentering
|
||||
- verify overlay remains geographically attached
|
||||
- verify update thread does not block badly
|
||||
|
||||
Automated checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- marker updates at 20–60 Hz do not break dragging
|
||||
- trajectory updates at 10–30 Hz do not break dragging
|
||||
- overlay updates are coalesced
|
||||
- center/zoom remain stable unless explicitly changed
|
||||
- overlay update APIs are safe from background threads
|
||||
- tile layer is not destroyed by overlay updates
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 6: add stable overlays and live update stress tests"
|
||||
```
|
||||
|
||||
## Step 7 — Layers, provider switching, and clearing APIs
|
||||
|
||||
Goal: add structural map operations after rendering stability is proven.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Implement layer operations:
|
||||
- `add_layer`
|
||||
- `show_layer`
|
||||
- `hide_layer`
|
||||
- `clear_layer`
|
||||
- `clear_map`
|
||||
|
||||
2. Implement provider switching:
|
||||
- `set_provider`
|
||||
|
||||
3. Provider switch must:
|
||||
- keep overlays
|
||||
- keep center
|
||||
- clamp zoom if needed
|
||||
- increment map generation
|
||||
- clear old provider tile draw state
|
||||
- ignore stale tile results
|
||||
- redraw attribution
|
||||
- not perform Dear PyGui work from caller thread
|
||||
|
||||
4. Implement cache control:
|
||||
- `clear_memory_cache`
|
||||
- `clear_disk_cache`
|
||||
- `get_cache_stats`
|
||||
|
||||
5. Add examples:
|
||||
- `examples/custom_provider.py`
|
||||
- extend `examples/cache_stress.py`
|
||||
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
uv run python examples/custom_provider.py
|
||||
uv run python examples/cache_stress.py
|
||||
```
|
||||
|
||||
Automated checks:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- layers can be shown/hidden/cleared
|
||||
- provider switch keeps overlays
|
||||
- provider switch does not render stale old-provider tiles
|
||||
- cache clear functions do not call Dear PyGui from non-GUI threads
|
||||
- memory/disk cache stats are usable
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 7: add layers provider switching and cache controls"
|
||||
```
|
||||
|
||||
## Step 8 — Documentation, hardening, and internal release
|
||||
|
||||
Goal: make the package usable as a dependency in DataFlux or another project.
|
||||
|
||||
Tasks:
|
||||
|
||||
1. Update `README.md` with:
|
||||
- uv install
|
||||
- local editable dependency
|
||||
- basic map
|
||||
- sizing examples
|
||||
- live marker update
|
||||
- live trajectory update
|
||||
- custom provider
|
||||
- memory/disk cache config
|
||||
- OpenStreetMap attribution/User-Agent note
|
||||
- thread-safety contract
|
||||
|
||||
2. Add docstrings for all public functions.
|
||||
|
||||
3. Review public API exports.
|
||||
|
||||
4. Add tests for failure cases:
|
||||
- unknown map
|
||||
- unknown overlay
|
||||
- unknown provider
|
||||
- invalid coordinates
|
||||
- mismatched lat/lon lengths
|
||||
- empty trajectory
|
||||
- clear deleted overlay
|
||||
- provider switch while tiles loading
|
||||
- overlay update while dragging model state
|
||||
|
||||
5. Run all examples manually.
|
||||
|
||||
6. Test local editable install from another project:
|
||||
|
||||
```bash
|
||||
cd ../some-test-project
|
||||
uv add -e ../dpg-map
|
||||
uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())"
|
||||
```
|
||||
|
||||
7. Bump version.
|
||||
|
||||
Suggested initial rebuilt beta:
|
||||
|
||||
```text
|
||||
0.3.0b1
|
||||
```
|
||||
|
||||
8. Tag release:
|
||||
|
||||
```bash
|
||||
git tag v0.3.0b1
|
||||
```
|
||||
|
||||
Final checks:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- all examples run
|
||||
- local editable dependency works
|
||||
- live overlay stress examples are stable
|
||||
- cache survives restart
|
||||
- disk cache limit works
|
||||
- sizing examples work
|
||||
- README accurately documents thread-safety and cache behaviour
|
||||
- codex/AGENTS.md accurately describes status
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "step 8: harden docs and prepare rebuilt beta"
|
||||
git tag v0.3.0b1
|
||||
```
|
||||
Reference in New Issue
Block a user