step 8: harden docs and prepare rebuilt beta
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user