23 KiB
type, work_item_id, adr_id, title, status, created_at, authors, supersedes, superseded_by, labels
| type | work_item_id | adr_id | title | status | created_at | authors | supersedes | superseded_by | labels | |
|---|---|---|---|---|---|---|---|---|---|---|
| adr | ET-007 | ADR-004 | ADR-004: Спутниковая подложка — провайдер Esri World Imagery, лениво-добавляемый raster-source, гибридная стратегия halo | accepted | 2026-05-31 |
|
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-sourceosm-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().
Решения, которые предстоит зафиксировать архитектурно:
- Какого провайдера спутниковых тайлов выбрать.
- Где разместить код переключателя — в
app.jsили в новом модуле. - Как именно добавлять спутниковый source/layer (заранее в
style.jsonили лениво из JS), и как переживатьmap.setStyle(). - Каким способом обеспечивать читаемость линий грунтовок/троп и POI на тёмной спутниковой подложке (halo).
- Классификацию изменения и нужна ли эскалация
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()и handleronclickв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. Минус: холодное переключение «Схема → Спутник» включает в себяaddSourceaddLayer+ сетевой запрос — но укладывается в НФТ 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.
-
Провайдер — 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). -
Код фичи живёт в
app.js. Никакой новый JS-файл не вводится. Новые глобальные функции —onBaseLayerToggle(),applyBaseLayer(),restoreBaseLayerState(),syncBaseLayerUI()— добавляются по соседству с уже существующимиrestoreTerrainState()/restoreTrailsState(). Если в будущей фазе появится потребность (например, второй провайдер, гибридный режим, оффлайн-кэш) — фича мигрирует вsrc/web/basemap.jsбез изменения публичного контракта (имена функций глобальные и стабильные). -
Source и layer добавляются лениво при первом включении «Спутник» через
addSource('satellite-raster', {...})+addLayer({ id: 'satellite-base', ... }). До этого момента запросов кserver.arcgisonline.comне происходит. Это важно с точки зрения приватности: пользователи, которые никогда не используют спутник, не светят свой IP на серверы Esri (см.10-tech-risks.md, R-3). -
Восстановление после
map.setStyle()— черезrebuildMapOverlays(). В функцию добавляется первым вызовif (typeof restoreBaseLayerState === 'function') restoreBaseLayerState();доrestoreTerrainState(). Это гарантирует, что terrain и trails окажутся выше спутника, без необходимости вычислятьbeforeId.restoreBaseLayerState()идемпотентен: читаетlocalStorageключаmap-base-layerи применяетapplyBaseLayer(). -
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на схеме.
- Для линий grade1..5 и paths/bridleway в обоих
-
Цвет
backgroundв режиме «Спутник» меняется черезsetPaintProperty('background', 'background-color', '#2a2a2a')(тёмно-серый), чтобы не «бликовало» под медленно подгружающимися спутниковыми плитками. При возврате на «Схему» восстанавливаются исходные значения изstyle.json(#f0ede6для светлой темы, тёмное значение изstyle-dark.jsonдля тёмной). Эти константы — единственные «дублирующие» значения; они зафиксированы вapplyBaseLayer()и в08-data-requirements.md§5. -
localStorage — ключ
map-base-layer(см. TRZ §4.3), значения"schematic"/"satellite", default"schematic". Ключ полностью обособлен от существующих UI-настроек (enduro-theme-mode,distance_unit,terrain-*,trails-*,poi-visible) — никаких миграций старых значений не требуется. -
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.mdR-3. - Корпоративные / анти-трекинг блокировки. Часть пользователей
(корпсети, NextDNS-фильтры) могут блокировать
arcgisonline.com. Поведение в этом случае — MapLibre показывает прозрачные плитки поверх#2a2a2aфона; пользователь сам переключится на «Схему». Это нормальное поведение (TRZ §1 REQ-F-08); fallback на схему автоматически — не закладываем. - Halo-слои в
style.jsonобоих тем. Любые будущие правки trails-слоёв требуют согласованной правки соответствующих*-halo-satelliteслоёв. Зафиксировано в10-tech-risks.mdR-1. - Background цвет. В коде
applyBaseLayer()появляется маленький дубль констант фона по темам. При смене палитры тем — править здесь тоже. Зафиксировано в10-tech-risks.mdR-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.mddocs/work-items/ET-007/02-trz.mddocs/work-items/ET-007/03-acceptance-criteria.mddocs/work-items/ET-007/04-test-plan.yamldocs/work-items/ET-007/04b-ui-test-cases.mddocs/work-items/ET-007/07-infra-requirements.mddocs/work-items/ET-007/08-data-requirements.mddocs/work-items/ET-007/10-tech-risks.mddocs/architecture/README.mddocs/architecture/adr/README.md- ADR-0001 (ET-005) — паттерн классических скриптов
- ADR-002 (ET-006) — «одна фича = один скрипт + глобали»