Skip to content

Navaid

tangram_navaid

plugin module-attribute

plugin = tangram_core.Plugin(frontend_path='dist-frontend', routers=[_router], config_class=TangramNavaidConfig, frontend_config_class=TangramNavaidFrontendConfig, into_frontend_config_function=_into_frontend, lifespan=_lifespan)

TangramNavaidFrontendConfig dataclass

Source code in packages/tangram_navaid/src/tangram_navaid/__init__.py
33
34
35
@dataclass(frozen=True, slots=True)
class TangramNavaidFrontendConfig:
    enable_faa: bool = False

enable_faa class-attribute instance-attribute

enable_faa: bool = False

__init__

__init__(enable_faa: bool = False) -> None

cache

log module-attribute

log = logging.getLogger(__name__)

FaaDataset module-attribute

FaaDataset = Literal['airports', 'routes', 'points', 'navaids']

XplaneDataset module-attribute

XplaneDataset = Literal['nav', 'fix', 'awy']

SourceGroup module-attribute

SourceGroup = Literal['ddr', 'xplane', 'faa']

XplaneAssets dataclass

Remote X-Plane navigation datasets cached by the backend.

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
39
40
41
42
43
44
45
@dataclass(frozen=True, slots=True)
class XplaneAssets:
    """Remote X-Plane navigation datasets cached by the backend."""

    nav_url: str = f"{_XPLANE_BASE_URL}/earth_nav.dat"
    fix_url: str = f"{_XPLANE_BASE_URL}/earth_fix.dat"
    awy_url: str = f"{_XPLANE_BASE_URL}/earth_awy.dat"
nav_url class-attribute instance-attribute
nav_url: str = f'{_XPLANE_BASE_URL}/earth_nav.dat'
fix_url class-attribute instance-attribute
fix_url: str = f'{_XPLANE_BASE_URL}/earth_fix.dat'
awy_url class-attribute instance-attribute
awy_url: str = f'{_XPLANE_BASE_URL}/earth_awy.dat'
__init__
__init__(nav_url: str = f'{_XPLANE_BASE_URL}/earth_nav.dat', fix_url: str = f'{_XPLANE_BASE_URL}/earth_fix.dat', awy_url: str = f'{_XPLANE_BASE_URL}/earth_awy.dat') -> None

FaaAssets dataclass

Remote FAA ArcGIS collections cached by the backend.

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
48
49
50
51
52
53
54
55
@dataclass(frozen=True, slots=True)
class FaaAssets:
    """Remote FAA ArcGIS collections cached by the backend."""

    airports_url: str = f"{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['airports']}.geojson"
    routes_url: str = f"{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['routes']}.geojson"
    points_url: str = f"{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['points']}.geojson"
    navaids_url: str = f"{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['navaids']}.geojson"
airports_url class-attribute instance-attribute
airports_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['airports']}.geojson'
routes_url class-attribute instance-attribute
routes_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['routes']}.geojson'
points_url class-attribute instance-attribute
points_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['points']}.geojson'
navaids_url class-attribute instance-attribute
navaids_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['navaids']}.geojson'
__init__
__init__(airports_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['airports']}.geojson', routes_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['routes']}.geojson', points_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['points']}.geojson', navaids_url: str = f'{_FAA_ARCGIS_BASE_URL}/{_FAA_DATASET_IDS['navaids']}.geojson') -> None

TangramNavaidConfig dataclass

Configuration for the navaid, fix, and Field 15 search plugin.

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@dataclass(frozen=True, slots=True)
class TangramNavaidConfig:
    """Configuration for the navaid, fix, and Field 15 search plugin."""

    path_cache: Path = Path(platformdirs.user_cache_dir("tangram_navaid"))
    """Directory used for cached navigation data."""
    ddr_archive_url: str = _DEFAULT_DDR_ARCHIVE_URL
    """EUROCONTROL DDR archive URL cached and served by the backend."""
    xplane: XplaneAssets = field(default_factory=XplaneAssets)
    """X-Plane dataset URLs cached and served by the backend."""
    enable_faa: bool = False
    """Attach the cached FAA navigation source in the browser."""
    faa: FaaAssets = field(default_factory=FaaAssets)
    """FAA ArcGIS collection URLs cached and served by the backend."""
path_cache class-attribute instance-attribute
path_cache: Path = Path(platformdirs.user_cache_dir('tangram_navaid'))

Directory used for cached navigation data.

ddr_archive_url class-attribute instance-attribute
ddr_archive_url: str = _DEFAULT_DDR_ARCHIVE_URL

EUROCONTROL DDR archive URL cached and served by the backend.

xplane class-attribute instance-attribute
xplane: XplaneAssets = field(default_factory=XplaneAssets)

X-Plane dataset URLs cached and served by the backend.

enable_faa class-attribute instance-attribute
enable_faa: bool = False

Attach the cached FAA navigation source in the browser.

faa class-attribute instance-attribute
faa: FaaAssets = field(default_factory=FaaAssets)

FAA ArcGIS collection URLs cached and served by the backend.

__init__
__init__(path_cache: Path = Path(user_cache_dir('tangram_navaid')), ddr_archive_url: str = _DEFAULT_DDR_ARCHIVE_URL, xplane: XplaneAssets = XplaneAssets(), enable_faa: bool = False, faa: FaaAssets = FaaAssets()) -> None

Source dataclass

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
74
75
76
77
78
79
80
81
@dataclass(frozen=True, slots=True)
class Source:
    name: str
    group: SourceGroup
    url: str
    suffix: str
    media_type: str
    required: bool = True
name instance-attribute
name: str
group instance-attribute
group: SourceGroup
url instance-attribute
url: str
suffix instance-attribute
suffix: str
media_type instance-attribute
media_type: str
required class-attribute instance-attribute
required: bool = True
__init__
__init__(name: str, group: SourceGroup, url: str, suffix: str, media_type: str, required: bool = True) -> None

CacheEntry

Bases: TypedDict

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
84
85
86
87
88
89
90
91
class CacheEntry(TypedDict):
    name: str
    group: SourceGroup
    required: bool
    ready: bool
    source_url: str
    path: str
    size_bytes: int | None
name instance-attribute
name: str
group instance-attribute
group: SourceGroup
required instance-attribute
required: bool
ready instance-attribute
ready: bool
source_url instance-attribute
source_url: str
path instance-attribute
path: str
size_bytes instance-attribute
size_bytes: int | None

CacheStatus

Bases: TypedDict

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
94
95
96
class CacheStatus(TypedDict):
    ready: bool
    entries: list[CacheEntry]
ready instance-attribute
ready: bool
entries instance-attribute
entries: list[CacheEntry]

CacheOperation

Bases: TypedDict

Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
 99
100
101
102
class CacheOperation(TypedDict):
    status: CacheStatus
    updated: list[str]
    unchanged: list[str]
status instance-attribute
status: CacheStatus
updated instance-attribute
updated: list[str]
unchanged instance-attribute
unchanged: list[str]

ddr_source

ddr_source(config: TangramNavaidConfig) -> Source
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
108
109
110
111
112
113
114
115
def ddr_source(config: TangramNavaidConfig) -> Source:
    return Source(
        "ddr",
        "ddr",
        config.ddr_archive_url,
        ".zip",
        "application/zip",
    )

xplane_source

xplane_source(config: TangramNavaidConfig, dataset: XplaneDataset) -> Source
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
118
119
120
121
122
123
124
125
def xplane_source(config: TangramNavaidConfig, dataset: XplaneDataset) -> Source:
    if dataset == "nav":
        url = config.xplane.nav_url
    elif dataset == "fix":
        url = config.xplane.fix_url
    else:
        url = config.xplane.awy_url
    return Source(f"xplane-{dataset}", "xplane", url, ".dat", "text/plain")

faa_source

faa_source(config: TangramNavaidConfig, dataset: FaaDataset) -> Source
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def faa_source(config: TangramNavaidConfig, dataset: FaaDataset) -> Source:
    if dataset == "airports":
        url = config.faa.airports_url
    elif dataset == "routes":
        url = config.faa.routes_url
    elif dataset == "points":
        url = config.faa.points_url
    else:
        url = config.faa.navaids_url
    return Source(
        f"faa-{dataset}",
        "faa",
        url,
        ".geojson",
        "application/geo+json",
        required=config.enable_faa,
    )

iter_sources

iter_sources(config: TangramNavaidConfig) -> Iterator[Source]
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
147
148
149
150
151
152
153
def iter_sources(config: TangramNavaidConfig) -> Iterator[Source]:
    yield ddr_source(config)
    yield from (xplane_source(config, dataset) for dataset in ("nav", "fix", "awy"))
    yield from (
        faa_source(config, dataset)
        for dataset in ("airports", "routes", "points", "navaids")
    )

cache_path

cache_path(source: Source, path_cache: Path) -> Path
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
156
157
158
def cache_path(source: Source, path_cache: Path) -> Path:
    digest = hashlib.sha256(source.url.encode()).hexdigest()
    return path_cache.expanduser() / f"{source.name}-{digest}{source.suffix}"

ensure async

ensure(client: AsyncClient, source: Source, path_cache: Path) -> Path
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
async def ensure(
    client: httpx.AsyncClient,
    source: Source,
    path_cache: Path,
) -> Path:
    destination = cache_path(source, path_cache)
    if destination.exists():
        return destination

    lock = _download_locks.setdefault(destination, asyncio.Lock())
    async with lock:
        if not destination.exists():
            await _download(client, source, destination)
    return destination

iter_cache_entries

iter_cache_entries(config: TangramNavaidConfig) -> Iterator[CacheEntry]
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
195
196
197
198
199
200
201
202
203
204
205
206
207
def iter_cache_entries(config: TangramNavaidConfig) -> Iterator[CacheEntry]:
    for source in iter_sources(config):
        destination = cache_path(source, config.path_cache)
        ready = destination.exists()
        yield CacheEntry(
            name=source.name,
            group=source.group,
            required=source.required,
            ready=ready,
            source_url=source.url,
            path=str(destination),
            size_bytes=destination.stat().st_size if ready else None,
        )

cache_status

cache_status(config: TangramNavaidConfig) -> CacheStatus
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
210
211
212
213
214
215
def cache_status(config: TangramNavaidConfig) -> CacheStatus:
    entries = list(iter_cache_entries(config))
    return CacheStatus(
        ready=all(entry["ready"] for entry in entries if entry["required"]),
        entries=entries,
    )

prewarm async

prewarm(client: AsyncClient, config: TangramNavaidConfig) -> CacheOperation
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
async def prewarm(
    client: httpx.AsyncClient,
    config: TangramNavaidConfig,
) -> CacheOperation:
    required = tuple(source for source in iter_sources(config) if source.required)
    cached = {
        source.name
        for source in required
        if cache_path(source, config.path_cache).exists()
    }
    await asyncio.gather(
        *(ensure(client, source, config.path_cache) for source in required)
    )
    return CacheOperation(
        status=cache_status(config),
        updated=[source.name for source in required if source.name not in cached],
        unchanged=[source.name for source in required if source.name in cached],
    )

prewarm_faa async

prewarm_faa(client: AsyncClient, config: TangramNavaidConfig) -> None
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
238
239
240
241
242
243
244
245
246
247
248
249
async def prewarm_faa(
    client: httpx.AsyncClient,
    config: TangramNavaidConfig,
) -> None:
    faa = tuple(source for source in iter_sources(config) if source.group == "faa")
    results = await asyncio.gather(
        *(ensure(client, source, config.path_cache) for source in faa),
        return_exceptions=True,
    )
    for source, result in zip(faa, results, strict=True):
        if isinstance(result, Exception):
            log.warning("could not cache %s: %s", source.name, result)

refresh_faa async

refresh_faa(client: AsyncClient, config: TangramNavaidConfig) -> CacheOperation
Source code in packages/tangram_navaid/src/tangram_navaid/cache.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
async def refresh_faa(
    client: httpx.AsyncClient,
    config: TangramNavaidConfig,
) -> CacheOperation:
    faa = tuple(
        faa_source(config, dataset)
        for dataset in ("airports", "routes", "points", "navaids")
    )

    async def refresh(source: Source) -> None:
        destination = cache_path(source, config.path_cache)
        async with _download_locks.setdefault(destination, asyncio.Lock()):
            # Assume FAA ArcGIS returned the requested collection; traffic.js parses it.
            await _download(client, source, destination)

    await asyncio.gather(*(refresh(source) for source in faa))
    return CacheOperation(
        status=cache_status(config),
        updated=[source.name for source in faa],
        unchanged=[],
    )