step 8: harden docs and prepare rebuilt beta

This commit is contained in:
2026-05-23 10:47:34 +02:00
parent d0ba8c4218
commit 50e38e18ee
17 changed files with 653 additions and 65 deletions

View File

@@ -32,6 +32,7 @@ from .api import (
update_polyline,
update_trajectory,
)
from .cache import CacheStats
from .providers import (
TileProvider,
get_provider,
@@ -42,6 +43,7 @@ from .providers import (
from .widget import map_widget
__all__ = [
"CacheStats",
"TileProvider",
"add_layer",
"add_marker",

View File

@@ -10,7 +10,12 @@ from typing import Any
from .cache import CacheStats, clear_disk_cache_path, disk_cache_root, disk_cache_size_bytes
from .commands import CommandKind, MapCommand
from .exceptions import CoordinateError, OverlayNotFoundError
from .exceptions import (
CoordinateError,
InvalidProviderError,
MapNotFoundError,
OverlayNotFoundError,
)
from .interaction import latlon_to_screen_in_state, screen_to_latlon_in_state
from .overlays import LayerState, MarkerOverlay, Overlay, PolylineOverlay, TrajectoryOverlay
from .projection import latlon_to_world
@@ -22,6 +27,7 @@ from .state import (
find_map_for_overlay,
get_config,
get_map_state,
list_map_states,
mark_dirty,
)
from .types import Bounds, LatLon, Point, Tag
@@ -90,6 +96,8 @@ def configure(
overlay_update_policy: str = "coalesce",
debug: bool = False,
) -> None:
"""Configure package-wide defaults used by subsequently created maps."""
configure_state(
user_agent=user_agent,
cache_dir=cache_dir,
@@ -104,20 +112,28 @@ def configure(
def set_center(lat: float, lon: float, *, map_tag: Tag | None = None) -> None:
"""Set a map center without changing its zoom."""
set_view(center=_validate_latlon(lat, lon), map_tag=map_tag)
def get_center(*, map_tag: Tag | None = None) -> LatLon:
"""Return the current logical center of a map."""
state = get_map_state(map_tag)
with state.lock:
return state.center
def set_zoom(zoom: int, *, map_tag: Tag | None = None) -> None:
"""Set a map zoom, clamped to the map/provider zoom range."""
set_view(zoom=zoom, map_tag=map_tag)
def get_zoom(*, map_tag: Tag | None = None) -> int:
"""Return the current logical zoom of a map."""
state = get_map_state(map_tag)
with state.lock:
return state.zoom
@@ -129,6 +145,8 @@ def set_view(
zoom: int | None = None,
map_tag: Tag | None = None,
) -> None:
"""Set map center and/or zoom as one logical view update."""
state = get_map_state(map_tag)
with state.lock:
payload: dict[str, Any] = {}
@@ -145,6 +163,8 @@ def set_view(
def fit_bounds(bounds: Bounds, *, map_tag: Tag | None = None) -> None:
"""Set center and zoom so geographic bounds fit the current draw area."""
(south_west, north_east) = bounds
south, west = _validate_latlon(south_west[0], south_west[1])
north, east = _validate_latlon(north_east[0], north_east[1])
@@ -171,11 +191,15 @@ def fit_bounds(bounds: Bounds, *, map_tag: Tag | None = None) -> None:
def screen_to_latlon(x: float, y: float, *, map_tag: Tag | None = None) -> LatLon:
"""Convert map-local screen coordinates to latitude/longitude."""
state = get_map_state(map_tag)
return screen_to_latlon_in_state(state, float(x), float(y))
def latlon_to_screen(lat: float, lon: float, *, map_tag: Tag | None = None) -> Point:
"""Convert latitude/longitude to map-local screen coordinates."""
lat_value, lon_value = _validate_latlon(lat, lon)
state = get_map_state(map_tag)
return latlon_to_screen_in_state(state, lat_value, lon_value)
@@ -192,6 +216,8 @@ def add_marker(
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
"""Add or replace a marker overlay and return its tag."""
state = get_map_state(map_tag)
color = kwargs.get("color", (255, 80, 80, 255))
radius = float(kwargs.get("radius", 5.0))
@@ -231,6 +257,8 @@ def add_polyline(
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
"""Add or replace a polyline overlay and return its tag."""
state = get_map_state(map_tag)
copied_points = _points_from_inputs(points, lats=lats, lons=lons)
with state.lock:
@@ -264,6 +292,8 @@ def add_trajectory(
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
"""Add or replace a trajectory overlay and return its tag."""
state = get_map_state(map_tag)
copied_points = _points_from_inputs(points, lats=lats, lons=lons)
timestamps = kwargs.get("timestamps")
@@ -303,6 +333,8 @@ def update_marker(
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
"""Update marker properties without changing the map view."""
state = find_map_for_overlay(tag, map_tag)
with state.lock:
overlay = state.overlays.get(tag)
@@ -335,6 +367,8 @@ def update_polyline(
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
"""Update polyline properties without changing the map view."""
state = find_map_for_overlay(tag, map_tag)
with state.lock:
overlay = state.overlays.get(tag)
@@ -362,6 +396,8 @@ def update_trajectory(
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
"""Update trajectory properties without changing the map view."""
state = find_map_for_overlay(tag, map_tag)
with state.lock:
overlay = state.overlays.get(tag)
@@ -386,10 +422,14 @@ def update_trajectory(
def set_marker_position(tag: Tag, lat: float, lon: float, *, map_tag: Tag | None = None) -> None:
"""Set a marker latitude/longitude."""
update_marker(tag, lat=lat, lon=lon, map_tag=map_tag)
def set_marker_label(tag: Tag, label: str, *, map_tag: Tag | None = None) -> None:
"""Set a marker label."""
update_marker(tag, label=label, map_tag=map_tag)
@@ -399,10 +439,14 @@ def set_polyline_points(
*,
map_tag: Tag | None = None,
) -> None:
"""Replace a polyline point sequence."""
update_polyline(tag, points=points, map_tag=map_tag)
def set_overlay_show(tag: Tag, show: bool, *, map_tag: Tag | None = None) -> None:
"""Show or hide an overlay without deleting it."""
state = find_map_for_overlay(tag, map_tag)
with state.lock:
overlay = state.overlays.get(tag)
@@ -415,6 +459,8 @@ def set_overlay_show(tag: Tag, show: bool, *, map_tag: Tag | None = None) -> Non
def delete_overlay(tag: Tag, *, map_tag: Tag | None = None) -> None:
"""Delete an overlay from its map and layer."""
state = find_map_for_overlay(tag, map_tag)
with state.lock:
overlay = state.overlays.pop(tag, None)
@@ -427,16 +473,46 @@ def delete_overlay(tag: Tag, *, map_tag: Tag | None = None) -> None:
_queue(state, CommandKind.DELETE_OVERLAY, {"tag": tag})
def add_layer(name: str, *, show: bool = True, map_tag: Tag | None = None) -> None:
def _cache_target_states(map_tag: Tag | None) -> list[Any]:
if map_tag is not None:
return [get_map_state(map_tag)]
try:
return [get_map_state(None)]
except MapNotFoundError:
return list_map_states()
def add_layer(
name: str,
*,
z_index: int | None = None,
show: bool = True,
map_tag: Tag | None = None,
) -> None:
"""Create or update a logical overlay layer."""
state = get_map_state(map_tag)
with state.lock:
layer = _ensure_layer(state, name, z_index=len(state.layers), show=show)
layer = _ensure_layer(
state,
name,
z_index=len(state.layers) if z_index is None else int(z_index),
show=show,
)
if z_index is not None:
layer.z_index = int(z_index)
layer.show = show
mark_dirty(state, DirtyFlags.OVERLAYS)
_queue(state, CommandKind.ADD_LAYER, {"name": name, "show": show})
_queue(
state,
CommandKind.ADD_LAYER,
{"name": name, "show": show, "z_index": layer.z_index},
)
def show_layer(name: str, *, map_tag: Tag | None = None) -> None:
"""Show all overlays assigned to a layer."""
state = get_map_state(map_tag)
with state.lock:
_ensure_layer(state, name).show = True
@@ -445,6 +521,8 @@ def show_layer(name: str, *, map_tag: Tag | None = None) -> None:
def hide_layer(name: str, *, map_tag: Tag | None = None) -> None:
"""Hide all overlays assigned to a layer."""
state = get_map_state(map_tag)
with state.lock:
_ensure_layer(state, name).show = False
@@ -453,6 +531,8 @@ def hide_layer(name: str, *, map_tag: Tag | None = None) -> None:
def clear_layer(name: str, *, map_tag: Tag | None = None) -> None:
"""Delete all overlays assigned to a layer."""
state = get_map_state(map_tag)
with state.lock:
layer = _ensure_layer(state, name)
@@ -464,6 +544,8 @@ def clear_layer(name: str, *, map_tag: Tag | None = None) -> None:
def clear_map(*, map_tag: Tag | None = None) -> None:
"""Delete all overlays and invalidate map tile resources."""
state = get_map_state(map_tag)
with state.lock:
state.overlays.clear()
@@ -475,36 +557,55 @@ def clear_map(*, map_tag: Tag | None = None) -> None:
def set_provider(provider: str | TileProvider, *, map_tag: Tag | None = None) -> None:
provider_obj = get_provider(provider) if isinstance(provider, str) else provider
"""Switch a map to another tile provider while preserving overlays."""
if isinstance(provider, str):
provider_obj = get_provider(provider)
elif isinstance(provider, TileProvider):
provider_obj = provider
else:
raise InvalidProviderError("provider must be a provider name or TileProvider")
state = get_map_state(map_tag)
with state.lock:
if state.provider == provider_obj:
return
state.provider = provider_obj
state.min_zoom = max(state.min_zoom, provider_obj.min_zoom)
state.max_zoom = min(state.max_zoom, provider_obj.max_zoom)
state.min_zoom = provider_obj.min_zoom
state.max_zoom = provider_obj.max_zoom
state.zoom = max(state.min_zoom, min(state.max_zoom, state.zoom))
state.generation += 1
mark_dirty(state, DirtyFlags.PROVIDER | DirtyFlags.TILES)
mark_dirty(state, DirtyFlags.PROVIDER | DirtyFlags.TILES | DirtyFlags.OVERLAYS)
_queue(state, CommandKind.SET_PROVIDER, {"provider": provider_obj.name})
def clear_memory_cache(*, map_tag: Tag | None = None) -> None:
state = get_map_state(map_tag)
with state.lock:
mark_dirty(state, DirtyFlags.TILES)
_queue(state, CommandKind.CLEAR_MEMORY_CACHE, {})
"""Clear decoded in-memory tile data through the renderer command queue."""
for state in _cache_target_states(map_tag):
with state.lock:
state.generation += 1
mark_dirty(state, DirtyFlags.TILES)
_queue(state, CommandKind.CLEAR_MEMORY_CACHE, {})
def clear_disk_cache(*, map_tag: Tag | None = None) -> None:
def clear_disk_cache(provider: str | None = None, *, map_tag: Tag | None = None) -> None:
"""Clear persistent tile cache data globally or for one map/provider."""
if provider is not None:
get_provider(provider)
if map_tag is None:
clear_disk_cache_path(get_config().cache_dir)
clear_disk_cache_path(get_config().cache_dir, provider=provider)
return
state = get_map_state(map_tag)
with state.lock:
state.generation += 1
mark_dirty(state, DirtyFlags.TILES)
_queue(state, CommandKind.CLEAR_DISK_CACHE, {})
_queue(state, CommandKind.CLEAR_DISK_CACHE, {"provider": provider})
def get_cache_stats(*, map_tag: Tag | None = None) -> CacheStats:
"""Return memory and disk cache diagnostics."""
config = get_config()
if map_tag is None:
cache_dir = config.cache_dir
@@ -531,6 +632,8 @@ def get_cache_stats(*, map_tag: Tag | None = None) -> CacheStats:
def get_map_debug_state(*, map_tag: Tag | None = None) -> dict[str, Any]:
"""Return a diagnostic snapshot for a map."""
state = get_map_state(map_tag)
with state.lock:
return {

View File

@@ -225,10 +225,20 @@ def scan_disk_cache(cache_dir: str | Path | None) -> list[DiskCacheEntry]:
return entries
def disk_cache_size_bytes(cache_dir: str | Path | None) -> int:
"""Return total bytes for cached tile files."""
def disk_cache_size_bytes(
cache_dir: str | Path | None,
*,
provider: str | None = None,
) -> int:
"""Return total bytes for cached tile files, optionally scoped to one provider."""
return sum(entry.metadata.size_bytes for entry in scan_disk_cache(cache_dir))
if provider is None:
return sum(entry.metadata.size_bytes for entry in scan_disk_cache(cache_dir))
safe_provider = provider.replace("/", "_")
provider_root = disk_cache_root(cache_dir) / safe_provider
if not provider_root.exists():
return 0
return sum(entry.metadata.size_bytes for entry in scan_disk_cache(provider_root))
def plan_disk_prune(
@@ -277,10 +287,12 @@ def prune_disk_cache(
return planned
def clear_disk_cache_path(cache_dir: str | Path | None) -> None:
"""Remove all persistent tile cache files under a cache root."""
def clear_disk_cache_path(cache_dir: str | Path | None, *, provider: str | None = None) -> None:
"""Remove persistent tile cache files under a cache root."""
root = disk_cache_root(cache_dir)
if provider is not None:
root = root / provider.replace("/", "_")
if not root.exists():
return
try:

View File

@@ -415,7 +415,7 @@ def drain_renderer_commands(state: MapState) -> list[MapCommand]:
state.dirty |= DirtyFlags.VIEW | DirtyFlags.TILES | DirtyFlags.OVERLAYS
elif command.kind is CommandKind.SET_PROVIDER:
state.tile_manager.clear_memory_cache()
state.dirty |= DirtyFlags.PROVIDER | DirtyFlags.TILES
state.dirty |= DirtyFlags.PROVIDER | DirtyFlags.TILES | DirtyFlags.OVERLAYS
elif command.kind in {
CommandKind.ADD_OVERLAY,
CommandKind.UPDATE_OVERLAY,
@@ -432,7 +432,10 @@ def drain_renderer_commands(state: MapState) -> list[MapCommand]:
state.tile_manager.clear_memory_cache()
state.dirty |= DirtyFlags.TILES
elif command.kind is CommandKind.CLEAR_DISK_CACHE:
state.tile_manager.clear_disk_cache(state.cache_dir)
provider = command.payload.get("provider")
if not isinstance(provider, str):
provider = None
state.tile_manager.clear_disk_cache(state.cache_dir, provider=provider)
state.dirty |= DirtyFlags.TILES
return commands

View File

@@ -276,6 +276,13 @@ def resolve_map_tag(map_tag: Tag | None = None) -> Tag:
raise MapNotFoundError("map_tag is required outside a map_widget context")
def list_map_states() -> list[MapState]:
"""Return registered map states as a snapshot."""
with _maps_lock:
return list(_maps.values())
def find_map_for_overlay(tag: Tag, map_tag: Tag | None = None) -> MapState:
"""Find the map containing an overlay, optionally scoped by map tag."""

View File

@@ -316,7 +316,7 @@ class TileManager:
RuntimeWarning,
stacklevel=3,
)
headers["User-Agent"] = "dpg-map/0.1"
headers["User-Agent"] = "dpg-map/0.3.0b1"
return headers
def _visible_disk_paths(self, cache_dir: str | Path | None) -> list[Path]:
@@ -428,10 +428,12 @@ class TileManager:
self._failed.clear()
return tags
def clear_disk_cache(self, cache_dir: str | Path | None) -> None:
"""Clear the persistent cache root."""
def clear_disk_cache(
self, cache_dir: str | Path | None, *, provider: str | None = None
) -> None:
"""Clear the persistent cache root or one provider namespace."""
clear_disk_cache_path(cache_dir)
clear_disk_cache_path(cache_dir, provider=provider)
def snapshot(self) -> TileManagerSnapshot:
"""Return diagnostic counters."""