Перейти к содержанию

Системные требования: интеграция карт (SR-MAP)

Дата: 2026-09-18. Ветка: fix4/maps-sys-req (от release/m1).

Основание: вердикт исследования 01-research.mdself-host OSM vector (Planetiler → PMTiles) + Copernicus GLO-30 terrain-RGB; запасной путь — Яндекс Tiles API только на интернет-стенде https://aerozveno.ff. Бизнес-требования — docs/task5/60-maps/02-business-requirements.md (ветка fix4/maps-biz-req, worktree w3-maps-biz-req, BR-MAP-01…20); трассировка ниже использует те же ID и смыслы, что в том документе.

Связанные артефакты: docs/task5/92-code/03-map-and-3d.md (../92-code/03-map-and-3d.md) (MAP-xx, MAP-R-13), docs/task5/50-stack/04-frontend-3d.md (../50-stack/04-frontend-3d.md) (UTM, terrain-RGB, interleaved), simulate/docs/10-world/01-geodata-sources.md. Координация симулятора с картами: пакет Б-7simulate/docs/98-fixes/real-geodata.md (ветка fix4/sim-geodata, worktree w3-sim-geodata; в release/m1 / этой ветке нет); схема манифеста — ../../simulate/src/geoscan_sim/world/manifest.py.


1. Резюме решения

Решение Обоснование (кратко)
Подложка — стиль MapLibre в существующем MapCanvas, не новый виджет layers.ts не знает про тайлы; исследование §1
PMTiles по HTTP Range с того же origin, что SPA R-DOC-3, SIM-BIZ-09, MAP-R-13; отдельный тайл-сервер (Martin, TileServer GL) не нужен для PMTiles
DEM — terrain-RGB (encoding: mapbox) в отдельном terrain.pmtiles MAP-08, 92-code/03-map-and-3d.md §4.2
Отображение EPSG:3857 (Web Mercator); хранение сценария EPSG:4326; геометрическая математика — UTM зоны сценария 50-stack/04-frontend-3d.md §«Крутить-вертеть полигоны», 40-formats/03-our-mission-schema.md
Инфра стенда продукта сегодня — n1, ns aerozveno, статика + /tiles/ через nginx/MinIO k3s/aerozveno/45-deployment-frontend.yaml, docs/task5/95-deploy/frontend-on-aerozveno.md
Опциональный хост tiles.ff — выделенная раздача больших архивов (ЦФО/РФ), не замена same-origin на первой фазе см. SR-MAP-INF-04

2. Контекст развёртывания

flowchart TB
  subgraph browser["Браузер оператора"]
    shell["apps/shell"]
    planner["apps/planner MapCanvas + deck.gl"]
    shell --> planner
  end

  subgraph n1["n1 / ns aerozveno"]
    ing["Ingress aerozveno.ff"]
    fe["deploy/geoscan-frontend nginx :8080"]
    minio["MinIO bucket aerozveno-frontend"]
    gw["deploy/geoscan gateway"]
    ing --> fe
    ing --> gw
    minio -->|"mc mirror"| fe
  end

  subgraph build["Разовая сборка на n1"]
    pt["Planetiler → basemap.pmtiles"]
    dem["GLO-30 → rio-rgbify → terrain.pmtiles"]
    pt --> minio
    dem --> minio
  end

  browser -->|"HTTPS GET /, /remotes/*, /tiles/*.pmtiles Range"| ing
  browser -->|"/api/*"| gw

  subgraph sim["Симулятор air-gap"]
    simweb["simulate/web nginx"]
    vol["Volume / public/tiles"]
    vol --> simweb
  end

3. Требования по слоям

3.1. Frontend (src/frontend)

Точка врезки подложки — src/frontend/apps/planner/src/map/MapCanvas.tsx. Слои сценария — src/frontend/apps/planner/src/map/layers.ts (без изменений контракта с тайлами). Рельеф для профилей — src/frontend/packages/domain/src/terrain.ts (переход на TerrainSource, MAP-07).

ID Требование Трассировка BR Файлы / артефакты
SR-MAP-FE-01 Режим подложки задаётся runtime, не хардкодом в бандле: mapBasemap: 'pmtiles' \| 'openfreemap' \| 'yandex-tiles'. На изолированном контуре допустим только pmtiles. BR-MAP-10, BR-MAP-11, BR-MAP-16 src/frontend/packages/state/src/runtime-config.ts, src/frontend/apps/shell/public/config.json.example, src/frontend/apps/shell/src/runtime-config.ts
SR-MAP-FE-02 Для pmtiles: до new maplibregl.Map регистрируется протокол pmtiles (import { Protocol } from 'pmtiles', maplibregl.addProtocol('pmtiles', …)). Зависимость pmtiles добавляется в src/frontend/apps/planner/package.json. BR-MAP-10 src/frontend/apps/planner/src/map/MapCanvas.tsx, src/frontend/apps/planner/package.json
SR-MAP-FE-03 Стиль BASEMAP_STYLE (замена OFFLINE_STYLE) содержит источники osm (vector, url: 'pmtiles:///tiles/basemap.pmtiles') и terrain (raster-dem, url: 'pmtiles:///tiles/terrain.pmtiles', tileSize: 256, encoding: 'mapbox'), блок terrain: { source: 'terrain', exaggeration: 1 }. URL в стиле — только pmtiles:// или относительные /… (MAP-R-13). BR-MAP-01, BR-MAP-08 src/frontend/apps/planner/src/map/MapCanvas.tsx, эталон в 01-research.md §6.1
SR-MAP-FE-04 Фаза 0 (интернет-стенд): mapBasemap: 'openfreemap' подставляет style: 'https://tiles.openfreemap.org/styles/dark' (или liberty). Тест MAP-R-13 для этого режима — исключение с отдельным барьерным тестом MAP-R-13-OFM. BR-MAP-11 src/frontend/apps/planner/src/map/MapCanvas.tsx или src/frontend/apps/planner/src/map/basemap-styles.ts (новый)
SR-MAP-FE-05 attributionControl: true; текст OSM и Copernicus берётся из source.attribution стиля. Дополнительный React-комponent MapAttribution дублирует обязательные строки при кастомном UI (режим Яндекс — логотип-ссылка). BR-MAP-15 src/frontend/apps/planner/src/map/MapCanvas.tsx, новый src/frontend/apps/planner/src/map/MapAttribution.tsx
SR-MAP-FE-06 При наличии raster-dem в стиле: MapboxOverlay с interleaved: true, overlay подключается в map.once('load', …) до отрисовки deck-слоёв с рельефом. BR-MAP-08, BR-MAP-18 src/frontend/apps/planner/src/map/MapCanvas.tsx, 92-code/03-map-and-3d.md §2
SR-MAP-FE-07 Переключатель спутника (optional): второй raster/vector source satellite из pmtiles:///tiles/satellite.pmtiles или слой в том же архиве; UI — флаг в LayerFlags (src/frontend/packages/domain/src/store.ts) / переключатель в панели карты, не отдельный dвижок. BR-MAP-04 src/frontend/packages/domain/src/store.ts, src/frontend/apps/planner/src/map/layers.ts не обязан меняться
SR-MAP-FE-08 Офлайн-fallback: если fetch/pmtiles HEAD/Range на /tiles/basemap.pmtiles завершился ошибкой или таймаутом > 8 с — карта переключается на встроенный OFFLINE_STYLE (фон #0D1117), показывается banner «Подложка недоступна, сценарий и рельеф (если загружен) работают». deck.gl-слои остаются. BR-MAP-10 src/frontend/apps/planner/src/map/MapCanvas.tsx, src/frontend/apps/planner/src/map/BasemapErrorBanner.tsx (новый)
SR-MAP-FE-09 Ошибки сети: при mapBasemap: 'yandex-tiles' неуспех тайла (HTTP ≥ 400, сеть) — полупрозрачная сетка + сообщение; ключ API не логировать. BR-MAP-12 src/frontend/apps/planner/src/map/MapCanvas.tsx, стиль с raster source
SR-MAP-FE-10 Glyphs/symbol: по умолчанию стиль без symbol-слоёв OSM (подписи сценария — TextLayer deck.gl, как сейчас). Опционально: /tiles/glyphs/{fontstack}/{range}.pbf и /tiles/sprite в статике. BR-MAP-01 src/frontend/apps/planner/src/map/MapCanvas.tsx, каталог в MinIO /tiles/glyphs/
SR-MAP-FE-11 DemTerrain.preload(bbox) вызывается при загрузке сценария (bbox области + 1 км буфер); до preload — skeleton на профиле высот. SyntheticTerrain сохраняется для mock/tests. BR-MAP-08 src/frontend/packages/domain/src/terrain.ts, хук загрузки сценария в planner
SR-MAP-FE-12 Сборка: base remotes не меняется; тайлы не в git. Для локальной разработки planner может монтировать public/tiles/ (опционально src/frontend/apps/planner/public/tiles/ + vite publicDir). Конфиги vite — только если понадобится proxy /tiles на локальный каталог. BR-MAP-10, BR-MAP-20 src/frontend/apps/planner/vite.config.ts, src/frontend/apps/shell/vite.config.ts, src/frontend/apps/viewer3d/vite.config.ts, src/frontend/apps/validator/vite.config.ts
SR-MAP-FE-13 Барьерный тест MAP-R-13 (и исключение OFM) в src/frontend/apps/planner/src/map/ — обход JSON стиля, отсутствие подстроки http в URL источников для режима pmtiles. BR-MAP-10, BR-MAP-13 новый src/frontend/apps/planner/src/map/basemap-style.spec.ts

Запрещено (наследие исследования): подмена MapLibre на ymaps3 / 2GIS MapGL; tile.openstreetmap.org в прод-стиле; спутник Яндекс JS API в MapLibre.


3.2. Backend / gateway (src/backend/gateway)

ID Требование Обоснование
SR-MAP-BE-01 По умолчанию gateway не меняется: тайлы и стиль отдаёт nginx фронтенда (или будущий tiles.ff), не NestJS. PMTiles не требует прокси для tailnet-стенда. 01-research.md §6.1; отсутствие маршрутов tiles в src/backend/gateway/src/main.ts
SR-MAP-BE-02 Опционально (только yandex-tiles на aerozveno): маршрут GET /api/tiles/yandex/{z}/{x}/{y}.png проксирует Tiles API с ключом из creds-store project/geoscan / YANDEX_TILES_API_KEY, query projection=web_mercator, lang=ru_RU, l=map. Ключ не попадает в config.json клиента. BR-MAP-12, BR-MAP-16; исследование §6.2
SR-MAP-BE-03 Прокси Яндекс: без дискового кеша > 30 суток; заголовки Cache-Control не противоречат п. 5.1.5 соглашения Яндекс Maps API. BR-MAP-12, L-MAP-03
SR-MAP-BE-04 Gateway не отдаёт GeoJSON зон с карты — зоны остаются в теле сценария API (airspace в JSON сценария). src/frontend/apps/planner/src/map/layers.ts, 40-formats/03-our-mission-schema.md
SR-MAP-BE-05 Высоты для метрик качества плана по-прежнему считает ядро/валидатор; gateway не становится WMS/DEM-сервисом. 50-stack/04-frontend-3d.md §«Чего на фронтенде быть не должно»

Вывод: критический путь не блокируется бэкендом; SR-MAP-BE-02 — отдельный инкремент для демо «как Яндекс» без PMTiles.


3.3. Инфра (k3s/, сборка данных)

ID Требование Детали
SR-MAP-INF-01 Артефакты карт хранятся в MinIO бакет aerozveno-frontend, префикс tiles/, раздаются nginx как https://aerozveno.ff/tiles/…. Не emptyDir как единственная копия. k3s/aerozveno/45-deployment-frontend.yaml
SR-MAP-INF-02 Nginx: для location /tiles/try_files $uri =404, add_header Accept-Ranges bytes, gzip off для .pmtiles, кеш Cache-Control: public, max-age=86400 (обновление — новый hash-имя файла или rollout restart после mirror). Правка k3s/aerozveno/45-deployment-frontend.yaml (ConfigMap geoscan-frontend-nginx). PMTiles Range
SR-MAP-INF-03 Отдельный образ тайл-сервера не разворачивается для PMTiles. Planetiler/rio-rgbifyJob/CronJob вне hot path или ручной запуск на n1; скрипт scripts/build-map-tiles.sh (создать). 01-research.md §6.1
SR-MAP-INF-04 Хост tiles.ff (предложение): второй Ingress на n1 (тот же nginx/MinIO или read-only mirror). DNS правится в CoreDNS на n2 (cm/ff-coredns, ns ff-dns): A-запись tiles.ff = тот же tailnet-IP, что у aerozveno.ff (на момент ревью dig +short aerozveno.ff100.66.249.6, n1 / nubble01; не IP n2 100.119.7.25 / geoscan.ff). Назначение — разгрузить основной бандл и отдавать ЦФО/РФ extract (>1 ГБ) без смешения с SPA. Фронт на первой фазе может оставаться на /tiles/ same-origin; tiles.ff — для geoscan.ff и CI mirror. BR-MAP-20; бюджет диска n1
SR-MAP-INF-05 Объём (бюджет): демо bbox s04 ~14×14 км: basemap.pmtiles < 100 МБ, terrain.pmtiles < 20 МБ; ЦФО ~0,5–2 ГБ + 5–8 ГБ temp при сборке; РФ ~3–8 ГБ PMTiles при необходимости (n1 ~800 ГБ свободно — вне репо, допущение задания). 01-research.md §2.2
SR-MAP-INF-06 Node placement: сборка и MinIO остаются на n1 (aerozveno). n2 (geoscan.ff) на M1 без фронта — при переносе продукта тайлы реplicate/mc mirror на n2, не пересборка planet. docs/task5/95-deploy/frontend-on-aerozveno.md
SR-MAP-INF-07 Мониторинг (docs/05-monitoring.md): (1) HTTP probe (blackbox или эквивалент в Prometheus) на https://aerozveno.ff/tiles/basemap.pmtiles — после SR-MAP-INF-02 ожидается 200/206 и Content-Type не text/html; пока файла нет — 404, но не SPA-fallback index.html; (2) алерт AerozvenoTilesProbeFailed: probe_success{instance=~".*aerozveno.ff/tiles/basemap.pmtiles.*"} == 0 sustained 10m или (до появления probe) AerozvenoFrontendDown: up{job="aerozveno",component="frontend"} == 0 — по образцу k3s/monitoring/aerozveno.rules.yml; (3) панель на k3s/monitoring/grafana/dashboards/aerozveno.jsonsum(rate(nginx_http_requests_total{job="aerozveno"}[5m])) (stub_status без лейблов uri/status). Не использовать PromQL с несуществующими лейблами stub_status. k3s/monitoring/prometheus-aerozveno-scrape.yaml, k3s/monitoring/aerozveno.rules.yml
SR-MAP-INF-08 Опциональный satellite.pmtiles (Sentinel-2 bbox) — тот же каталог tiles/, лимит демо < 200 МБ. BR-MAP-04

Образ (если когда-либо понадобится dedicated tiles pod): nginx:alpine + volume mount read-only; не MapTiler On-Prem. Для SR-MAP-INF-04 достаточно второго Service+Ingress на существующий nginx или MinIO console bypass.


3.4. Симулятор (simulate/)

ID Требование Детали
SR-MAP-SIM-01 docker compose up без интернета: UI карты использует те же PMTiles, что планировщик (basemap.pmtiles, terrain.pmtiles), mapBasemap жёстко pmtiles. BR-MAP-10, SIM-BIZ-09, simulate/docs/90-final/02-requirements-registry.md
SR-MAP-SIM-02 Файлы лежат в simulate/web/public/tiles/ (или volume, монтируемый в образ web при сборке stand); стиль — расширение simulate/web/public/style/offline.json или simulate/web/public/style/pmtiles.json с pmtiles:// / /tiles/…. simulate/web/src/components/map/RunMap2D.tsx, simulate/web/src/test/offline.test.ts
SR-MAP-SIM-03 simulate/web/src/components/map/RunMap2D.tsx: регистрация протокола pmtiles, attributionControl: true, тест simulate/web/src/test/offline.test.ts обновлён: нет http в стиле, наличие sources terrain. SR-MAP-FE-02, BR-MAP-10, MAP-R-13
SR-MAP-SIM-04 World loader (контракт Б-7): физика — GeoTIFF float32 EPSG:4326 (спецификация: demo_glo30_30m.tif, bbox WGS84 [37.4, 55.6, 37.9, 55.9] — см. simulate/fixtures/mirror/manifest.json и real-geodata.md, fix4/sim-geodata), ENU-кэш demo_glo30_enu_30m.npz, мир demo-glo30, source_id=copernicus_glo30. terrain.pmtiles / basemap.pmtiles — тот же клип (единый world_snapshot_id). В ветке release/m1 mirror status: blocked — файлы datasets появятся с merge Б-7. Следующий шаг пайплайна: float32 COG в UTM — simulate/docs/10-world/01-geodata-sources.md §2.3. BR-MAP-14
SR-MAP-SIM-05 world_manifest.json — схема ../../simulate/src/geoscan_sim/world/manifest.py: layers (8 имён: terrainquality), у каждого слоя source_id, release, crs, vertical_datum, resolution_m, sha256. layers.terrain.sha256 — только SHA256 GeoTIFF физики (datasets[] с source_id=copernicus_glo30, путь fixtures/mirror/datasets/demo_glo30_30m.tif); runtime Б-7 выставляет его из этой записи, не из PMTiles. layers.basemap.sha256 — целевой SHA256 basemap.pmtiles (source_id=osm_odbl); в Б-7 слой synthetic (пустой sha). terrain.pmtiles — отдельная запись в fixtures/mirror/manifest.jsondatasets[] (тот же bbox/клип, свой path, отдельный source_id, например terrain_rgb_pmtiles / производный rgbify), sha в datasets[].sha256, без перезаписи layers.terrain. Top-level поля dem_source / basemap_source не вводятся. BR-MAP-14, SIM-GEO-17
SR-MAP-SIM-06 Compose не тянет внешние URL при старте web-контейнера; проверка — расширить offline.test.ts + e2e «нет DNS lookup на openfreemap/yandex» в CI simulate. SIM-BIZ-09

4. Контракты данных

4.1. URL и протоколы

Контракт Значение Примечание
Basemap vector pmtiles:///tiles/basemap.pmtiles MapLibre + protocol handler
DEM pmtiles:///tiles/terrain.pmtiles, type: raster-dem, encoding: mapbox Декод: h = −10000 + (R·65536 + G·256 + B) · 0,1
Satellite (opt.) pmtiles:///tiles/satellite.pmtiles Raster или RGB в MVT-схеме
Glyphs (opt.) /tiles/glyphs/{fontstack}/{range}.pbf Protomaps/OpenMapTiles layout
Sprite (opt.) /tiles/sprite + @2x.json/png
OpenFreeMap phase 0 https://tiles.openfreemap.org/styles/dark только openfreemap
Yandex fallback /api/tiles/yandex/{z}/{x}/{y}.png или Tiles API с referrer web_mercator обязателен

4.2. Проекции и CRS

Слой CRS Правило
MapLibre view, deck.gl overlay, тайлы OSM/DEM EPSG:3857 (display) Яндекс Tiles — только projection=web_mercator
Сценарий, KML/GeoJSON export, API EPSG:4326 (WGS84, RFC 7946 порядок [lon, lat]) 40-formats/03-our-mission-schema.md
Редактирование, буферы, азимут галсов UTM (зона по центроиду сценария, поле в схеме) 50-stack/04-frontend-3d.md:121-128
DEM симулятора (Б-7 сейчас) EPSG:4326 GeoTIFF float32 + ENU-кэш; runtime sampling в LOCAL_ENU simulate/fixtures/mirror/manifest.json (bbox), спецификация файлов — real-geodata.md (fix4/sim-geodata)
DEM симулятора / backend sampling (целевой пайплайн) UTM зона сценария, vertical EGM2008 (GLO-30) 01-geodata-sources.md §2.3 — следующий шаг после Б-7, не текущий файл

Инвариант: координаты в deck.gl layers — WGS84; MapLibre проецирует в 3857; geo.ts/makeProjector — единственное место локальной метрической геометрии (MAP-R-06).

4.3. DEM terrain-RGB

Поле Тип Описание
encoding 'mapbox' MapLibre raster-dem
tileSize 256
maxzoom ≤ 14 для bbox 14 км ~z12 достаточно (92-code/03-map-and-3d.md)
nodata не кодировать в RGB клип по bbox до rgbify

4.4. GeoJSON зон (сценарий, не тайловый слой)

Схема — фрагмент airspace в 40-formats/03-our-mission-schema.md:

{
  "allowed": { "type": "Polygon", "coordinates": [ [ [lon, lat], ... ] ] },
  "no_fly": [
    {
      "geometry": { "type": "Polygon", "coordinates": [ [ [lon, lat], ... ] ] },
      "altitude_min_m": 0,
      "altitude_max_m": 500,
      "zone_kind": "no_fly",
      "valid_from": null,
      "valid_to": null
    }
  ]
}

На карте: allowed — штрих layers.ts id allowed; NFZ — nfz, nfz-buffer с safety_buffer_m из objective. Отдельной tile-схемы для зон нет.

erDiagram
  SCENARIO ||--|| AIRSPACE : contains
  SCENARIO ||--o{ TASK : tasks
  AIRSPACE ||--|| ALLOWED_POLYGON : allowed
  AIRSPACE ||--o{ NFZ : no_fly
  BASEMAP_PMTILES ||--o{ VECTOR_TILE : encodes
  TERRAIN_PMTILES ||--o{ DEM_TILE : terrain_rgb
  MAP_STYLE ||--|| BASEMAP_PMTILES : source_osm
  MAP_STYLE ||--|| TERRAIN_PMTILES : source_terrain
  WORLD_MANIFEST ||--|| GLO30_GEOTIFF : layers_terrain_sha256
  WORLD_MANIFEST ||--|| BASEMAP_PMTILES : layers_basemap_sha256
  WORLD_MANIFEST ||--o{ DATASET_MIRROR : datasets
  GLO30_GEOTIFF ||--|| TERRAIN_PMTILES : same_clip_rgbify
  GLO30_GEOTIFF ||--o| COG_UTM_TARGET : pipeline_step_2_3

  SCENARIO {
    string schema_version
    json airspace
    json tasks
  }
  ALLOWED_POLYGON {
    string type "Polygon"
    json coordinates "EPSG:4326"
  }
  NFZ {
    json geometry
    number altitude_min_m
    number altitude_max_m
    string zone_kind
  }
  BASEMAP_PMTILES {
    string url "/tiles/basemap.pmtiles"
    string attribution "OSM ODbL"
  }
  TERRAIN_PMTILES {
    string url "/tiles/terrain.pmtiles"
    string encoding "mapbox"
  }
  MAP_STYLE {
    int version "8"
    string glyphs "optional /tiles/glyphs/..."
  }
  WORLD_MANIFEST {
    string source_id "copernicus_glo30 demo-glo30"
    json layers "terrain basemap quality..."
    string world_snapshot_id
  }
  GLO30_GEOTIFF {
    string spec "real-geodata.md B-7"
    string crs "EPSG:4326"
    json bbox "manifest.json 37.4-55.9"
    string path "datasets/demo_glo30_30m.tif"
  }
  DATASET_MIRROR {
    string source_id "copernicus_glo30 terrain_rgb_pmtiles..."
    string path "datasets/ or tiles/"
    string sha256 "per-file hash"
  }
  COG_UTM_TARGET {
    string crs "EPSG:326xx"
    string status "planned 01-geodata-sources 2.3"
  }

5. Офлайн-режим

5.1. Что входит в пакет (демо-регион s04)

Компонент Включать Оценка размера
MapLibre style JSON (без внешних URL) да < 50 КБ
basemap.pmtiles (OSM vector, bbox ~14×14 км) да 30–80 МБ
terrain.pmtiles (GLO-30 terrain-RGB, тот же bbox) да 5–20 МБ
satellite.pmtiles опционально до 200 МБ
glyphs + sprite нет в минимальном пакете 0 (или +20 МБ полный)
JS бандл planner/shell уже в SPA / simulate web без изменения логики

Итого минимальный offline-пакет: ~50–100 МБ; с опциональным спутником — до ~300 МБ.

5.2. Переключение на фронте

flowchart TD
  start[load config.json] --> mode{mapBasemap}
  mode -->|pmtiles| reg[register pmtiles protocol]
  reg --> fetch[HEAD /tiles/basemap.pmtiles]
  fetch -->|200| full[BASEMAP_STYLE + terrain]
  fetch -->|fail| fb[OFFLINE_STYLE + banner]
  mode -->|openfreemap| ofm[external style URL]
  mode -->|yandex-tiles| ya[raster + own DEM pmtiles]
  full --> dem[DemTerrain.preload scenario bbox]
  dem --> ready[interleaved overlay on load]
  fb --> deck[deck.gl layers only]
  1. Shell читает /config.json (src/frontend/apps/shell/src/runtime-config.ts).
  2. Planner строит стиль по mapBasemap.
  3. При pmtiles — проверка доступности архива; при сбое — SR-MAP-FE-08.
  4. DemTerrain.preload — параллельно отрисовке карты; ошибка DEM — banner, профиль на SyntheticTerrain запрещён на prod-стенде (только dev/mock).

5.3. Изолированный стенд симулятора

  • Те же файлы в simulate/web/public/tiles/, стиль указывает /tiles/….
  • docker compose web-сервис (когда появится в stand-docker) монтирует volume read-only; нет переменных с API-ключами карт.
  • Согласование с Б-7 (real-geodata.md, fix4/sim-geodata): layers.terrain.sha256 = sha GeoTIFF demo_glo30_30m.tif (copernicus_glo30); layers.basemap.sha256 = sha basemap.pmtiles (целевое); terrain.pmtiles и basemap.pmtiles в mirror — отдельные строки datasets[] со своими path/sha256 (PMTiles не подменяют layers.terrain). GeoTIFF GLO-30 — тот же клип, что feed для rio-rgbifyterrain.pmtiles.

6. Последовательность загрузки (runtime)

sequenceDiagram
  participant U as Пользователь
  participant S as shell config.json
  participant M as MapCanvas
  participant N as nginx /tiles
  participant P as pmtiles protocol
  participant D as DemTerrain
  participant G as deck.gl layers.ts

  U->>S: открыть /plan
  S->>M: mapBasemap pmtiles
  M->>M: addProtocol pmtiles
  M->>N: GET basemap.pmtiles Range bytes=0-16383
  N-->>P: 206 partial
  P-->>M: vector tiles
  M->>N: GET terrain.pmtiles Range
  N-->>M: DEM tiles
  M->>M: map load interleaved true
  M->>G: MapboxOverlay layers
  par preload
    M->>D: preload scenario bbox
    D->>N: Range DEM tiles
  end
  G-->>U: NFZ swaths routes on terrain

7. План миграции (независимые инкременты)

Оценка — агенто-заходы (1 заход ≈ ревьюируемый PR).

Шаг Содержание Заходов Зависимости Риски
M0 mapBasemap в runtime + фаза 0 OpenFreeMap на aerozveno only + attributionControl: true 1 MAP-R-13 красный до исключения OFM; жюри видит внешний CDN
M1 scripts/build-map-tiles.sh, выкладка demo bbox в MinIO, nginx /tiles/ + Range 2 доступ n1, Geofabrik сборка Planetiler OOM — уменьшить bbox; mirror без Range ломает PMTiles
M2 MAP-07 TerrainSource + DemTerrain.preload 2 M1 terrain file рассинхрон bbox COG/sim vs rgbify
M3 MAP-08 стиль pmtiles, protocol, interleaved, SR-MAP-FE-08 fallback 2 M2 забыть load before overlay — рельеф плоский
M4 Simulate: public/tiles + стиль + offline tests 1 M1 artifacts большой git LFS — не коммитить, только CI artifact
M5 MAP-09 contours, optional satellite layer 1 M3 perf на слабом GPU
M6 SR-MAP-BE-02 Yandex proxy (optional) 1 creds-store ключ 30 RPS лимит; блокировка без интернета
M7 SR-MAP-INF-04 tiles.ff + мониторинг алертов 1 M1 DNS ff-coredns забыли — 404 на geoscan.ff
M8 ЦФО extract вместо demo bbox 1 M7 disk 8 ГБ temp заполнил диск n1

Текущее состояние (baseline): OFFLINE_STYLE без sources (src/frontend/apps/planner/src/map/MapCanvas.tsx:20-24), attributionControl: false, interleaved: false, синтетика src/frontend/packages/domain/src/terrain.ts, simulate simulate/web/public/style/offline.json пустой.


8. Критерии приёмки (действие → доказательство)

ID Критерий приёмки
SR-MAP-FE-01 На aerozveno в DevTools → Network видно GET /config.json; смена mapBasemap без пересборки бандла меняет источник карты после F5.
SR-MAP-FE-02 В консоли нет ошибки «Unknown protocol pmtiles»; cd src/frontend && npm test проходит (регистрация протокола покрыта unit/integration в planner map).
SR-MAP-FE-03 cd src/frontend && npx vitest run apps/planner/src/map/basemap-style.spec.ts — MAP-R-13 green для pmtiles.
SR-MAP-FE-04 При mapBasemap: openfreemap карта грузит liberty/dark; тест MAP-R-13-OFM помечен @openfreemap-only.
SR-MAP-FE-05 В UI виден блок атрибуции © OpenStreetMap и текст Copernicus; скриншот для отчёта.
SR-MAP-FE-06 На DEM: incline map pitch 60°, swath дraped; борт за хребтом перекрыт рельефом (checklist MAP-08).
SR-MAP-FE-07 Toggle «Спутник» меняет видимость слоя; без сети спутник доступен только если satellite.pmtiles локален.
SR-MAP-FE-08 curl -I https://aerozveno.ff/tiles/basemap.pmtiles → 404 или offline nginx: UI показывает banner, deck-слои NFZ видны.
SR-MAP-FE-09 Симуляция 503 от proxy Yandex: сетка ошибки, приложение не падает.
SR-MAP-FE-10 Стиль без glyphs URL — карта открывается; подписи сценария от TextLayer.
SR-MAP-FE-11 s01-simple: профиль высот в drawer не плоский линия 112…288 синтетики — значения совпадают с DemTerrain ±0,1 м на 3 контрольных точках.
SR-MAP-FE-12 git grep pmtiles не находит .pmtiles в tracked files.
SR-MAP-FE-13 CI job frontend: MAP-R-13 green.
SR-MAP-BE-01 curl https://aerozveno.ff/tiles/basemap.pmtiles -H 'Range: bytes=0-100' -I206 от nginx, не от gateway.
SR-MAP-BE-02 curl -o /dev/null -w '%{http_code}' 'https://aerozveno.ff/api/tiles/yandex/10/618/320.png' → 200 при настроенном ключе; в ответе нет заголовка с apikey.
SR-MAP-BE-03 Code review: нет disk cache backend для Yandex tiles.
SR-MAP-BE-04 GET /api/scenarios/{id} — зоны только в JSON, отдельного /zones.mvt нет.
SR-MAP-BE-05 grep gateway — нет elevationAt, DEM routes.
SR-MAP-INF-01 mc ls minio/aerozveno-frontend/tiles/ показывает pmtiles после сборки.
SR-MAP-INF-02 Response headers: Accept-Ranges: bytes, partial content 206.
SR-MAP-INF-03 scripts/build-map-tiles.sh --bbox s04 exit 0 на n1; образ tileserver не в k3s/aerozveno/kustomization.yaml.
SR-MAP-INF-04 После A-записи tiles.ff → IP aerozveno.ff: curl --cacert ff-ca/ff-ca.crt -I https://tiles.ff/tiles/basemap.pmtiles → 200/206 (или 404 без text/html до выкладки файла).
SR-MAP-INF-05 du -sh каталога tiles в пределах бюджета шага M1/M8.
SR-MAP-INF-06 kubectl --context n1 get pods -n aerozveno -o wide — frontend на n1.
SR-MAP-INF-07 В Prometheus загружен probe (или правило AerozvenoFrontendDown); kubectl --context n1 -n monitoring get prometheusrule содержит AerozvenoTilesProbeFailed или эквивалент; probe_success==1 для URL PMTiles после M1+INF-02.
SR-MAP-INF-08 Toggle спутника + файл satellite.pmtiles present.
SR-MAP-SIM-01 На машине без default route: docker compose up + открыть web UI — рельеф виден, Network tab без внешних hosts карт.
SR-MAP-SIM-02 ls simulate/web/public/tiles/*.pmtiles после make target.
SR-MAP-SIM-03 cd simulate/web && npm test -- offline.test.ts — green.
SR-MAP-SIM-04 Для мира demo-glo30: sha256sum simulate/fixtures/mirror/datasets/demo_glo30_30m.tif совпадает с layers.terrain.sha256 в world_manifest.json (или mirror manifest); sha256sum simulate/web/public/tiles/terrain.pmtiles — с sha256 записи datasets[], у которой path указывает на этот PMTiles (отдельный source_id, не copernicus_glo30).
SR-MAP-SIM-05 Два прогона sim с одним manifest — одинаковые layers.terrain.source_id и sha256 в отчёте.
SR-MAP-SIM-06 CI simulate: test «no external map hosts».

9. Трассировка BR-MAP → SR-MAP

Источник ID и формулировок: docs/task5/60-maps/02-business-requirements.md (ветка fix4/maps-biz-req).

BR-MAP Смысл (кратко) SR-MAP
01 Ориентация на местности, топосъёмка FE-03, FE-10, INF-01
02 Навигация (масштаб, панорама) (поведение MapLibre по умолчанию; MAP-R-16 — без отдельного SR)
03 Поиск / flyTo по координатам (Could в BR; отдельный SR не в scope карт M1 — UI planner)
04 Спутник / схема FE-07, INF-08
05 Область съёмки на подложке (deck.gl + MAP-05; подложка — FE-03)
06 ВПП на карте (MAP-05; подложка — FE-03)
07 БПЗ и ВП поверх подложки (layers.ts; подложка — FE-03)
08 Терренный рельеф, AGL FE-03, FE-06, FE-11
09 Изолинии M5 / MAP-09 (SR через FE-06 после MAP-08)
10 Офлайн-демо FE-01, FE-08, FE-12, FE-13, SIM-01…06, INF-03
11 OpenFreeMap фаза 0 FE-04
12 Яндекс Tiles fallback FE-09, BE-02, BE-03
13 KML / WGS-84 без сдвига FE-13, MAP-R-06 (src/frontend/packages/domain/src/geo.ts)
14 Тот же рельеф/bbox для sim SIM-04, SIM-05, INF-01
15 Атрибуция FE-05, SIM-03
16 Секреты карт FE-01, BE-02
17 План на карте после расчёта (MAP-02/03 + FE-03; deck.gl)
18 Производительность FE-06
19 Ограничения точности DEM (документ limitations; DemTerrain badge — вне SR-MAP)
20 Сборка и поставка тайлов INF-03, INF-04, INF-05, FE-12, M1, M8

10. Открытые точки

  1. BR-MAP-02/03/05…07/17 — в основном закрываются существующими MAP-xx и deck.gl; при появлении отдельных SR в planner их добавить с трассировкой из 02-business-requirements.md.
  2. HTTP probe PMTiles — модуль blackbox в кластере n1 может потребовать отдельного scrape config (сейчас только stub_status nginx).
  3. quantized-mesh для Cesium (MAP-11) — второй артеfact из того же GLO-30, в scope SR-MAP только упоминание; детали DEV-02.
  4. Свободное место n1 не автоматизировано — перед M8 ручной df -h.

Документ согласован с 02-business-requirements.md (fix4/maps-biz-req) и Б-7 real-geodata.md (fix4/sim-geodata).