The same rewrite as for the core package. No help= string, epilog, or other UX text changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""The ``map`` tool: plot the located nodes of the mesh on an OpenStreetMap terminal map.
|
|
|
|
The tool collects the nodes that share their location in adverts from the passive-monitor
|
|
history (:meth:`~meshterm.persistence.repository.Repository.heard_nodes`). It adds our node
|
|
when its position is known. Then it draws all the nodes over a real street basemap,
|
|
rendered as Unicode braille (streets, rivers, place names). This is the same terminal-map
|
|
idea as ``mapscii``.
|
|
|
|
Menu only: the tool opens a full-screen map that the user can pan and zoom (refer to
|
|
:mod:`meshterm.ui.map_screen`). It registers no CLI subcommand, because a map is a picture,
|
|
and the scripted CLI gives facts (refer to :meth:`MapTool.register_cli`). Repeaters have
|
|
priority over ordinary nodes: they have a different marker, drawn on top. The basemap is a
|
|
best effort. With no network (and no cached tiles), the tool plots the nodes on a blank
|
|
grid, and it still works offline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import typer
|
|
|
|
from ..context import AppContext
|
|
from ..services.markers import gather_markers
|
|
from .base import Tool, ToolResult, register
|
|
|
|
|
|
def _fraction(ctx: AppContext, params: dict[str, Any]) -> float:
|
|
"""The fraction of the mesh that a new map frames: the flag if given, else the preference.
|
|
|
|
``--fraction`` is resolved here, not as the default of the Typer option, because the
|
|
options are declared when the CLI is registered. That occurs before a context exists,
|
|
and thus before MeshTerm reads the preferences file.
|
|
|
|
Args:
|
|
ctx: The shared application context.
|
|
params: The parameters of this run.
|
|
|
|
Returns:
|
|
The fraction to frame.
|
|
"""
|
|
fraction = params.get("fraction")
|
|
return ctx.preferences.map_view_fraction if fraction is None else float(fraction)
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from ..core.models import MapMarker
|
|
|
|
|
|
@register
|
|
class MapTool(Tool):
|
|
"""Show the nodes of the mesh that share a location on a braille OpenStreetMap map."""
|
|
|
|
name = "map"
|
|
title = "Map"
|
|
icon = "🌍"
|
|
help = "Mesh nodes on a pannable street map"
|
|
category = "Explore"
|
|
order = 10
|
|
|
|
async def run(self, ctx: AppContext, params: dict[str, Any]) -> ToolResult:
|
|
"""Collect the located nodes and open the interactive map.
|
|
|
|
The tool is for the menu only (refer to :meth:`register_cli`), so this method has
|
|
one path.
|
|
|
|
Args:
|
|
ctx: The shared application context.
|
|
params: An optional ``fraction``: the part of the mesh to frame when the map
|
|
opens.
|
|
|
|
Returns:
|
|
A :class:`ToolResult` with the number of plotted nodes.
|
|
"""
|
|
from ..ui.map_screen import open_map
|
|
|
|
markers = await self._gather(ctx)
|
|
if not markers:
|
|
ctx.ui.note(
|
|
"[muted]no contacts or heard nodes have shared a location yet — a node "
|
|
"appears here once it advertises coordinates[/muted]"
|
|
)
|
|
return ToolResult(summary={"located": 0})
|
|
|
|
await open_map(ctx, markers, fraction=_fraction(ctx, params))
|
|
|
|
repeaters = sum(1 for m in markers if m.is_repeater and not m.is_self)
|
|
self_located = any(m.is_self for m in markers)
|
|
return ToolResult(
|
|
summary={
|
|
"located": len(markers),
|
|
"repeaters": repeaters,
|
|
"nodes": len(markers) - repeaters - (1 if self_located else 0),
|
|
"self_located": self_located,
|
|
},
|
|
)
|
|
|
|
# -- the markers ------------------------------------------------------------
|
|
|
|
async def _gather(self, ctx: AppContext) -> list[MapMarker]:
|
|
"""Collect each located node to plot (refer to :func:`gather_markers`)."""
|
|
return await gather_markers(ctx)
|
|
|
|
def register_cli(self, app: typer.Typer) -> None:
|
|
"""Register no CLI command, because a map is a picture, and pictures are menu only.
|
|
|
|
Each other feature has a scripted face, because its answer is a set of facts that
|
|
a script can use. The answer of a map is a drawing. Its braille cells get their
|
|
meaning from their position on a grid, and colour tells its nodes apart. The
|
|
scripted CLI does not have these (refer to :mod:`meshterm.ui.script`). A one-shot
|
|
render on the CLI is a block of glyphs that no program can parse. If the render
|
|
has no colour, as the rest of the CLI, a person cannot read it either.
|
|
|
|
A script can still get the located nodes: ``meshterm contacts`` lists each of
|
|
them, with their coordinates.
|
|
|
|
Args:
|
|
app: The Typer application (not changed).
|
|
"""
|