From 231c99c045dc5e247cd1a9e99a91c80fc46dcf4e Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 31 May 2026 20:01:06 +0000 Subject: [PATCH] docs(ET-007): architecture - ADR, infra-requirements, data-requirements, tech-risks --- docs/architecture/README.md | 15 +- docs/architecture/adr/README.md | 1 + .../06-adr/ADR-004-satellite-base-layer.md | 305 ++++++++++++++++++ .../ET-007/07-infra-requirements.md | 163 ++++++++++ .../work-items/ET-007/08-data-requirements.md | 135 ++++++++ docs/work-items/ET-007/10-tech-risks.md | 200 ++++++++++++ 6 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md create mode 100644 docs/work-items/ET-007/07-infra-requirements.md create mode 100644 docs/work-items/ET-007/08-data-requirements.md create mode 100644 docs/work-items/ET-007/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index aabaab8..893e5dd 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,12 +11,25 @@ - **Database** — SQLite + Spatialite (точки интереса, маршруты) ## Слои карты -- Base map (OpenStreetMap) +- Base map: **Схема** (OpenStreetMap raster) либо **Спутник** (Esri World Imagery raster) — переключается в UI (ET-007 / ADR-004) - Hillshade (рельеф с тенями) - TRI (Terrain Ruggedness Index — сложность рельефа) - Hypsometric (высотная раскраска) - Trails (маршруты из OSM) +## Внешние тайл-провайдеры + +Клиент (браузер) обращается напрямую к двум внешним raster-tile сервисам. +Сервер mva154 эти тайлы не проксирует и не кэширует. + +| Провайдер | Назначение | URL | Активация | API-ключ | +|-----------|-----------|-----|-----------|----------| +| OpenStreetMap | Базовый слой «Схема» | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | всегда (default подложка) | нет | +| Esri World Imagery | Базовый слой «Спутник» | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` | лениво — только при включении «Спутник» пользователем (ET-007) | нет | + +Атрибуция обоих провайдеров выводится MapLibre автоматически при +активном source. + ## Деплой Один Docker Compose на mva154. Nginx проксирует /enduro/ на контейнер. diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 51da402..c91bad5 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -7,3 +7,4 @@ | ADR-001 | Блокировка шлагбаумов через `mode.inaccessible` | accepted | 2026-05-15 | [ET-001](../../work-items/ET-001/06-adr/ADR-001-barrier-blocking.md) | | ADR-002 | GPX-фича как отдельный модуль `gpx.js` | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md) | | ADR-003 | Парсинг GPX — `DOMParser` в основном потоке с чанковой конвертацией | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-003-gpx-parsing-strategy.md) | +| ADR-004 | Спутниковая подложка: Esri World Imagery, ленивый raster-source, гибридное halo | accepted | 2026-05-31 | [ET-007](../../work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md) | diff --git a/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md new file mode 100644 index 0000000..37dea4e --- /dev/null +++ b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md @@ -0,0 +1,305 @@ +--- +type: adr +work_item_id: ET-007 +adr_id: ADR-004 +title: "ADR-004: Спутниковая подложка — провайдер Esri World Imagery, лениво-добавляемый raster-source, гибридная стратегия halo" +status: accepted +created_at: 2026-05-31 +authors: + - "agent:architect" +supersedes: [] +superseded_by: [] +labels: [] +--- + +# ADR-004 — Спутниковая подложка: провайдер, размещение кода, схема восстановления + +## Статус + +Accepted + +## Контекст + +ET-007 вводит вторую базовую подложку карты — спутниковые растровые +снимки — с переключателем «Схема / Спутник» в попапе слоёв +(см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`). + +Существующее состояние, проверенное в коде: + +- В обоих стилях карты (`src/web/style.json` стр. 16–41, + `src/web/style-dark.json`) уже определён единственный raster-source + `osm-raster` и слой `osm-base`, лежащий поверх слоя `background`. + Тайлы OSM раздаются `https://tile.openstreetmap.org/{z}/{x}/{y}.png` + — то есть прецедент **внешней (кросс-оригинальной) тайл-зависимости с + атрибуцией без API-ключа уже существует**. +- `src/web/app.js` (3 132 строки) содержит функцию `rebuildMapOverlays()` + (стр. 127), которая последовательно вызывает `restoreTerrainState()`, + `restoreTrailsState()`, `restorePoiState()`, перерисовку маршрутов / + GPX / линейки. Эта функция — единственная точка восстановления + визуальных слоёв после `map.setStyle()` (переключение тёмной/светлой + темы, `switchMapStyle()` стр. 100–117). +- Фронтенд плоский, без сборщика: `index.html`, `app.js`, `units.js` + (190 строк, ADR-0001), `gpx.js` (1 242 строки, ADR-002). Сложившийся + паттерн — «одна крупная фича = один классический скрипт + глобали» + (ADR-002). Все JS-функции глобальные, обработчики навешаны через + инлайновые `onclick`. +- Динамические мутации слоёв через `setPaintProperty` / + `setLayoutProperty` / `addSource` / `addLayer` в `app.js` уже широко + используются (~30 вхождений). +- В `app.js` уже есть зрелые «restore*State()»-функции для каждой + группы слоёв; ET-007 встраивается в этот же паттерн ещё одной такой + функцией `restoreBaseLayerState()`. + +Решения, которые предстоит зафиксировать архитектурно: + +1. Какого провайдера спутниковых тайлов выбрать. +2. Где разместить код переключателя — в `app.js` или в новом модуле. +3. Как именно добавлять спутниковый source/layer (заранее в `style.json` + или лениво из JS), и как переживать `map.setStyle()`. +4. Каким способом обеспечивать читаемость линий грунтовок/троп и POI + на тёмной спутниковой подложке (halo). +5. Классификацию изменения и нужна ли эскалация `arch:major-change`. + +## Рассмотренные варианты + +### Вариант P (провайдер) — выбор провайдера спутниковых тайлов + +| Провайдер | API-ключ | Лицензия / условия | Покрытие | Решение | +|---|---|---|---|---| +| **Esri World Imagery** (`server.arcgisonline.com/.../World_Imagery/MapServer/tile/{z}/{y}/{x}`) | нет | Условия Esri ArcGIS Online: бесплатное использование с атрибуцией для некоммерческой и demo-разработки; широко применяется open-source-проектами (Leaflet, OpenLayers, QGIS) | глобальное, до z19 | **выбран** | +| Mapbox Satellite | требуется | бесплатный квот-лимит, далее платно | глобальное | отклонён — BRD F-02 явно требует «без API-ключа» | +| Bing Maps | требуется | сложная лицензия, обязательная регистрация | глобальное | отклонён — то же | +| Google Maps Tiles | требуется | прямо запрещён ToS для нативного встраивания не через Google Maps JS API | глобальное | отклонён | +| OpenAerialMap | нет | open-source, CC-BY | **фрагментарное**, нет глобального бесшовного слоя | отклонён — не покрывает РФ-эндуро-сценарии | +| MapTiler Satellite | требуется | бесплатный квот-лимит | глобальное | отклонён — API-ключ | + +Esri World Imagery — единственный вариант, удовлетворяющий +**одновременно** трём ограничениям BRD: без API-ключа, с глобальным +покрытием, с лицензионно допустимой формой использования через +атрибуцию. + +### Вариант M (модуль) — где разместить код + +- **M-A — добавить в `app.js`** (выбран). +~150 строк + (`onBaseLayerToggle`, `applyBaseLayer`, `restoreBaseLayerState`, + `syncBaseLayerUI`, плюс хук в `rebuildMapOverlays()` и handler + `onclick` в `index.html`). Минимальный blast radius, никаких новых + файлов, никаких изменений в подключении скриптов. +- **M-B — выделить `src/web/basemap.js`** (по аналогии с ADR-002 для + GPX). Отклонён: ADR-002 разделил фичу, потому что её объём был + 600–900 строк и она имела собственную модель данных (`gpxTracks`), + собственный bottom sheet и собственный canvas. Здесь фича плоская и + объём в 5–7 раз меньше; разделение даёт чистоту, но не покрывает + стоимости новой связки `app.js ↔ basemap.js` ради ~150 строк. + Контракт интеграции с `rebuildMapOverlays()` и так глобальный — + никакой инкапсуляции отдельный файл не добавит. + +### Вариант S (source) — как добавить спутниковый source/layer + +- **S-A — задекларировать source `satellite-raster` и слой + `satellite-base` (`visibility: none`) в обоих `style.json` / + `style-dark.json`**. Source активен всегда, тайлы не запрашиваются + до показа слоя. Плюс: восстановление после `setStyle()` + тривиально (`setLayoutProperty('satellite-base', 'visibility', ...)`). + Минус: `style.json` обоих тем нужно править симметрично; дрейф + значений между двумя стилями. +- **S-B — лениво создавать source и layer из JS при первом включении + «Спутник»** (выбран, совпадает с TRZ §1 REQ-F-02). Плюс: `style.json` + не трогаем; ноль внешних запросов у пользователей, которые не + включают спутник; единая точка определения source — в `app.js`. После + `map.setStyle()` source и layer исчезают и переcоздаются вызовом + `restoreBaseLayerState()` из `rebuildMapOverlays()` — это та же + логика, что уже используется для terrain/trails/POI/GPX. Минус: + холодное переключение «Схема → Спутник» включает в себя `addSource` + + `addLayer` + сетевой запрос — но укладывается в НФТ 500 мс. + +### Вариант O (order) — порядок восстановления в `rebuildMapOverlays()` + +- **O-A — `restoreBaseLayerState()` вызывается ПЕРВЫМ**, до + `restoreTerrainState()` (выбран, совпадает с TRZ §5.5). Гарантирует + z-order: `background` → `satellite-base` → `osm-base` → terrain → + trails → POI → routes → GPX. terrain/trails/POI оказываются выше + спутника, маршрут/GPX — выше terrain. +- **O-B — добавлять `satellite-base` с явным `beforeId` первого + trails-слоя**. Идемпотентно к порядку, но в `rebuildMapOverlays()` + моменты создания слоёв не атомарны (terrain/trails добавляются + асинхронно); использовать `beforeId` слоёв, которых ещё нет, нельзя. + Поэтому простой «вызвать первым» надёжнее. + +### Вариант H (halo) — обеспечение читаемости поверх спутника + +- **H-A — динамический `setPaintProperty` по всем затрагиваемым слоям**. + Все правки делаем из `applyBaseLayer()`; на «Схема» возвращаем + исходные значения. Минус: нужно где-то хранить «исходные» paint- + значения; при `map.setStyle()` они сбрасываются, что повышает риск + drift между двумя темами. +- **H-B — отдельные «underlay»-слои с halo, `visibility: none` по + умолчанию, включаются на спутнике** + **`setPaintProperty` только + для POI text-halo** (выбран, совпадает с TRZ §1 REQ-F-04). Halo-линии + декларативны в `style.json` обеих тем — никакого «запомнить + исходное» не нужно, восстановление по `visibility`. Для POI label + правок одна (`text-halo-color`/`text-halo-width`) — её проще менять + динамически, чем заводить параллельные label-слои. +- **H-C — толстая полупрозрачная белая обводка прямо в существующих + trails-слоях через `line-gap-width`**. Отклонён: ломает «Схему» + (там halo не нужен и портит вид светлой подложки). + +## Решение + +Принимается комбинация: **P-Esri + M-A + S-B + O-A + H-B**. + +1. **Провайдер — Esri World Imagery.** URL-шаблон, атрибуция и параметры + source — как в TRZ §4.1. HTTPS обязателен. Атрибуция строки — + `"Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`. + Альтернативные провайдеры не закладываются в код фичи; точка + расширения — единственный объект source-spec в `applyBaseLayer()`, + при будущей смене провайдера правка локализуется одним местом + (см. R-2 в `10-tech-risks.md`). + +2. **Код фичи живёт в `app.js`.** Никакой новый JS-файл не вводится. + Новые глобальные функции — `onBaseLayerToggle()`, `applyBaseLayer()`, + `restoreBaseLayerState()`, `syncBaseLayerUI()` — добавляются по + соседству с уже существующими `restoreTerrainState()` / + `restoreTrailsState()`. Если в будущей фазе появится потребность + (например, второй провайдер, гибридный режим, оффлайн-кэш) — фича + мигрирует в `src/web/basemap.js` без изменения публичного контракта + (имена функций глобальные и стабильные). + +3. **Source и layer добавляются лениво** при первом включении + «Спутник» через `addSource('satellite-raster', {...})` + + `addLayer({ id: 'satellite-base', ... })`. До этого момента + запросов к `server.arcgisonline.com` не происходит. Это важно с + точки зрения приватности: пользователи, которые никогда не + используют спутник, не светят свой IP на серверы Esri (см. + `10-tech-risks.md`, R-3). + +4. **Восстановление после `map.setStyle()` — через `rebuildMapOverlays()`.** + В функцию добавляется **первым** вызов + `if (typeof restoreBaseLayerState === 'function') restoreBaseLayerState();` + до `restoreTerrainState()`. Это гарантирует, что terrain и trails + окажутся выше спутника, без необходимости вычислять `beforeId`. + `restoreBaseLayerState()` идемпотентен: читает `localStorage` ключа + `map-base-layer` и применяет `applyBaseLayer()`. + +5. **Halo — гибридный подход:** + + - Для **линий grade1..5 и paths/bridleway** в обоих `style.json` / + `style-dark.json` заводятся парные «underlay»-слои + (`*-halo-satellite`) с более широкой полупрозрачной белой + обводкой и `layout.visibility = "none"`. При входе в «Спутник» + эти слои становятся видимыми; при возврате на «Схему» — + скрываются. Никаких runtime-правок paint не требуется. + - Для **POI labels** меняются динамически только два свойства — + `text-halo-color` (`#000000` на спутнике / исходное на схеме) и + `text-halo-width` (`2` на спутнике / исходное на схеме) — через + `setPaintProperty`. Эти исходные значения известны и + зафиксированы в `style.json`; читать «текущее» через + `getPaintProperty` не нужно — всегда выставляем явные значения + для обоих режимов. + - **POI circles** — обводка `circle-stroke-color: #ffffff` / + `circle-stroke-width: 2` динамически на спутнике, возврат к + исходным значениям из `style.json` на схеме. + +6. **Цвет `background`** в режиме «Спутник» меняется через + `setPaintProperty('background', 'background-color', '#2a2a2a')` + (тёмно-серый), чтобы не «бликовало» под медленно подгружающимися + спутниковыми плитками. При возврате на «Схему» восстанавливаются + исходные значения из `style.json` (`#f0ede6` для светлой темы, + тёмное значение из `style-dark.json` для тёмной). Эти константы — + единственные «дублирующие» значения; они зафиксированы в + `applyBaseLayer()` и в `08-data-requirements.md` §5. + +7. **localStorage — ключ `map-base-layer`** (см. TRZ §4.3), значения + `"schematic"` / `"satellite"`, default `"schematic"`. Ключ + полностью обособлен от существующих UI-настроек + (`enduro-theme-mode`, `distance_unit`, `terrain-*`, `trails-*`, + `poi-visible`) — никаких миграций старых значений не требуется. + +8. **C4 / архитектурная диаграмма.** В репозитории нет файлов + `c4-*.mmd`; описание архитектуры — текстовое в + `docs/architecture/README.md`. Туда добавляется отдельный раздел + «Внешние тайл-провайдеры» с двумя строками: OSM (существующий) + и Esri World Imagery (новый, для подложки «Спутник»). Дополнительно + `docs/architecture/adr/README.md` пополняется записью ADR-004. + +## Последствия + +### Положительные + +- Изменения — **только в коде фронтенда** (`src/web/index.html`, + `src/web/app.js`, `src/web/app.css`, оба `style*.json`). Backend, + БД, OSRM, nginx, Docker-конфигурация — без изменений (см. + `07-infra-requirements.md`). +- Лазерная локальность точки расширения: для смены провайдера + достаточно отредактировать один объект source-spec в `app.js`. +- НФТ 500 мс выполнима: при холодном переключении расходы — это + единичные вызовы `addSource` + `addLayer` + первая сетевая загрузка + плитки z=текущий; последующие переключения мгновенные (только + `visibility`). +- Пользователи, никогда не использующие «Спутник», не отправляют ни + одного запроса на серверы Esri — минимизация утечки данных по + умолчанию (см. R-3). +- Существующая инфраструктура восстановления после `map.setStyle()` + переиспользуется без изменения её формы — единый паттерн для + terrain/trails/POI/GPX/base-layer. + +### Отрицательные / ограничения + +- **Зависимость от третьей стороны.** Сервис Esri может ввести + лимит / потребовать API-ключ / изменить URL. Митигация: точка + расширения в `applyBaseLayer()`; риск зафиксирован + (`10-tech-risks.md`, R-2). +- **Утечка IP при использовании спутника.** При активном «Спутник» + IP пользователя становится виден Esri (так же, как сейчас он виден + tile.openstreetmap.org). Это **не регрессия приватности относительно + OSM**, но — расширение перечня третьих сторон, к которым клиент + обращается. Зафиксировано в `08-data-requirements.md` §5 и + `10-tech-risks.md` R-3. +- **Корпоративные / анти-трекинг блокировки.** Часть пользователей + (корпсети, NextDNS-фильтры) могут блокировать `arcgisonline.com`. + Поведение в этом случае — MapLibre показывает прозрачные плитки + поверх `#2a2a2a` фона; пользователь сам переключится на «Схему». + Это нормальное поведение (TRZ §1 REQ-F-08); fallback на схему + автоматически — **не закладываем**. +- **Halo-слои в `style.json` обоих тем.** Любые будущие правки + trails-слоёв требуют согласованной правки соответствующих + `*-halo-satellite` слоёв. Зафиксировано в `10-tech-risks.md` R-1. +- **Background цвет.** В коде `applyBaseLayer()` появляется маленький + дубль констант фона по темам. При смене палитры тем — править здесь + тоже. Зафиксировано в `10-tech-risks.md` R-5. + +### Технический долг + +- Если позже появится потребность во **втором** провайдере (например, + для альтернативной геополитической юрисдикции) или в гибридном + режиме «Спутник + подписи дорог OSM поверх», логичный путь — + вынести фичу в `src/web/basemap.js` (ADR-002-стиль) и расширить + локальное состояние до `{ provider, hybrid }`. Имена глобальных + функций (`onBaseLayerToggle`, `restoreBaseLayerState`) остаются + стабильным контрактом — `index.html` и `app.js` не меняются. +- Если в проект введут CSP-заголовок (сейчас его нет, см. ET-006 + `07-infra-requirements.md` §4), для спутника потребуется + `img-src 'self' https://*.openstreetmap.org https://server.arcgisonline.com data:;`. + +## Классификация изменения + +**Minor change.** Новых контейнеров, сервисов, БД, серверных API +ET-007 не вводит. Внешний тайл-провайдер — расширение уже +существующего класса зависимостей (OSM-tile), а не новый +архитектурный класс. Лейбл `arch:major-change` **не требуется**. +Обязательного дополнительного архитектурного approve не требуется. + +## Связанные документы + +- `docs/work-items/ET-007/01-brd.md` +- `docs/work-items/ET-007/02-trz.md` +- `docs/work-items/ET-007/03-acceptance-criteria.md` +- `docs/work-items/ET-007/04-test-plan.yaml` +- `docs/work-items/ET-007/04b-ui-test-cases.md` +- `docs/work-items/ET-007/07-infra-requirements.md` +- `docs/work-items/ET-007/08-data-requirements.md` +- `docs/work-items/ET-007/10-tech-risks.md` +- `docs/architecture/README.md` +- `docs/architecture/adr/README.md` +- ADR-0001 (ET-005) — паттерн классических скриптов +- ADR-002 (ET-006) — «одна фича = один скрипт + глобали» diff --git a/docs/work-items/ET-007/07-infra-requirements.md b/docs/work-items/ET-007/07-infra-requirements.md new file mode 100644 index 0000000..352b061 --- /dev/null +++ b/docs/work-items/ET-007/07-infra-requirements.md @@ -0,0 +1,163 @@ +--- +type: infra-requirements +work_item_id: ET-007 +title: "Инфраструктурные требования — ET-007: Спутниковая карта (Схема / Спутник)" +version: 1 +status: approved +created_at: 2026-05-31 +authors: + - "agent:architect" +--- + +# Инфраструктурные требования — ET-007 + +## 1. Резюме + +ET-007 — изменение **исключительно фронтенда**: `src/web/index.html`, +`src/web/app.js`, `src/web/app.css`, `src/web/style.json`, +`src/web/style-dark.json`. Новой инфраструктуры, новых контейнеров, +новых портов и серверной конфигурации **не требуется**. Документ +зафиксирован для полноты work-item и явно подтверждает отсутствие +инфра-воздействия (см. `06-adr/ADR-004-satellite-base-layer.md`). + +## 2. Контейнеры и сервисы + +| Аспект | Требование | +|--------|------------| +| Новые контейнеры | Нет | +| Изменения существующих сервисов (api, osrm, nginx) | Нет | +| Изменения `docker-compose.yml` | Нет | +| Изменения `Dockerfile` | Нет — все правки попадают в образ через уже существующий `COPY src/web/ ./src/web/` | +| Изменения подключения скриптов в `index.html` | Нет новых `