12-review.md (REQUEST_CHANGES, attempt 2/3) flagged 6 must-fix items
in the analysis/architecture artefacts plus matching bugs that had
already leaked into the committed implementation. This patch lands
both: documents corrected, code aligned with corrected specs, tests
updated.
P1-1: TRZ/ADR/Data/Risks referenced fictional layer ids
(`trails-grade1..5-halo-satellite`, `paths-bridleway-halo-satellite`).
Actual style*.json has only `trails-track-halo-satellite` and
`trails-path-bridleway-halo-satellite`; grade differentiation lives
inside one `match` expression on `tracktype` within `trails-track`.
Docs rewritten to operate on real ids.
P1-2: POI labels contrast was broken — spec changed only halo-color
to black, leaving `text-color: #333333` (light theme baseline)
unreadable over the new black halo. Code+docs now switch BOTH
`text-color` (-> `#ffffff` on satellite) AND halo together, with
per-theme baselines (`#333333` light / `#e0e0e0` dark) restored on
return to Schematic.
P1-3: BRD §5 hillshade risk said «hillshade auto-disabled on
satellite», contradicting TRZ/ADR/AC. BRD wording aligned: hillshade
keeps working over satellite; visual check is AC-04.
P1-4: background-color had four divergent sources (`#1a1a1a`,
`#2a2a2a`, `#1a1a2e`, `#f0ede6`), incl. an inverted-theme typo and a
baseline `#1a1a1a` that didn't match the actual `style-dark.json:28`
value `#1a1a2e`. Settled on ADR-004's single-constant model: `#2a2a2a`
on satellite for both themes; on Schematic restore per-theme baselines
`#f0ede6` (light) / `#1a1a2e` (dark). `_applyBackgroundForSatellite`
fixed accordingly.
P1-5: app.js already had `layerState.basemap` and `toggleLayer
('basemap')` (legacy «Базовая карта» switch). Neither TRZ nor ADR
specified the interaction. Added save&restore contract: on entering
Satellite save `layerState.basemap` to `_savedBasemapState` and
force-hide `osm-base`; on returning to Schematic restore osm-base
visibility from the saved value. CSS hook `body.satellite-active
#btn-basemap { display:none }` keeps the user from trying to enable
a hybrid mode (out of scope, BRD §3). TRZ §5.6, ADR-004 §8.
P1-6: `restoreTrailsState()` and `onTrailsCheckbox()` only managed
visibility of `trails-track` / `trails-path-bridleway`, leaving
their halo-underlay siblings as «phantom» halos when the user
unchecked grunты/тропы under Satellite. Introduced
`_applyTrailHaloVisibility(map, base)` reading checkbox state from
DOM; called from `onTrailsCheckbox`, `restoreTrailsState`, and both
branches of `applyBaseLayer`. Rule: halo visible ⇔ (base ===
satellite) AND (checkbox ON). TRZ §5.7, ADR-004 §9.
Docs bumped: BRD v2, TRZ v2, AC v2, Data v2, Risks v2; ADR-004
получает «Ревизии»-секцию (status remains accepted — only editorial
fixes, no decision change).
Tests:
- tests/unit/base_layer.test.js: rewritten 2 background-color
assertions (#1a1a1a expectation removed), added 6 new tests for
P1-2 / P1-4 (POI text-color per-theme baselines, single satellite
bg #2a2a2a, baseline restore on Schematic).
- All 33 JS unit tests + 22 pytest static checks green.
- Full pytest suite: 76 passed (excluding pre-existing
shapely-import skipped collection in tests/unit/test_health.py).
Refs: ET-007
Review: docs/work-items/ET-007/12-review.md (P1-1..P1-6)
ADR: docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md (rev. 2026-05-31)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 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 — гибридный подход:
- Для линий грунтовок и троп в обоих
style.json/style-dark.jsonприсутствуют парные «underlay»-слоиtrails-track-halo-satelliteиtrails-path-bridleway-halo-satellite(более широкая полупрозрачная белая обводка,layout.visibility = "none"). При входе в «Спутник» эти слои становятся видимыми; при возврате на «Схему» — скрываются. Никаких runtime-правок paint не требуется. Слоёв на каждую grade (trails-grade1..5-halo-satellite) не заводится: дифференциация grade хранится внутри одногоmatch-выражения поtracktypeвtrails-track, halo единого цвета/ширины накладывается на весь слой целиком — этого достаточно для читаемости (под halo всё равно ляжет цветная линияtrails-track). Аналогично для троп — единый halo на весьtrails-path-bridleway(фильтрhighway in path/bridleway/footway).trails-asphalthalo не получает: он по умолчанию скрыт (visibility: none,line-opacity: 0); если в будущей фазе включится — добавится halo тем же паттерном. - Для POI labels меняются динамически три свойства:
text-color(#ffffffна спутнике / baseline текущей темы на схеме —#333333для light,#e0e0e0для dark),text-halo-color(#000000на спутнике / baseline#ffffffдля light,#1a1a2eдля dark на схеме),text-halo-width(2на спутнике / baseline1.5для light,2для dark на схеме). Менять обе пары (color + halo) необходимо: иначе тёмный baseline- текст светлой темы (#333333) поверх чёрного halo не читается. Baseline-значения известны и зафиксированы в Data §5; всегда выставляем явные значения для обоих режимов. - POI circles — обводка
circle-stroke-color: #ffffff/circle-stroke-width: 2динамически на спутнике, возврат к baseline текущей темы из Data §5 на схеме (#ffffff/1.5light,#333333/1.5dark).
- Для линий грунтовок и троп в обоих
-
Цвет
backgroundв режиме «Спутник» меняется черезsetPaintProperty('background', 'background-color', '#2a2a2a')— единая константа#2a2a2aдля обеих тем (тёмно-серый, чтобы не «бликовал» под медленно подгружающимися спутниковыми плитками). На обеих темах используется одно и то же значение; per-theme- развилки нет (упрощает код и исключает рассинхрон). При возврате на «Схему» восстанавливаются baseline-значения текущей темы —#f0ede6(light, изstyle.json) и#1a1a2e(dark, изstyle-dark.json; не#1a1a1a— это была ошибка в более раннем черновике). Эти baseline-константы зафиксированы в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) — никаких миграций старых значений не требуется. -
Контракт с существующим
toggleLayer('basemap')(app.js:384–391). В коде уже есть отдельный пользовательский выключатель «Базовая карта» (управляетosm-base.visibilityиlayerState.basemap). ET-007 принимает паттерн save & restore (см. TRZ §5.6): при входе в «Спутник» сохраняемlayerState.basemapв_savedBasemapStateи принудительно скрываемosm-base; UI-кнопка#btn-basemapскрывается через CSS-класс.satellite-active(чтобы пользователь не пытался включить «гибрид»: out of scope BRD §3). При возврате на «Схему» восстанавливаемosm-base.visibilityиз сохранённого значения. На «Схеме»toggleLayer('basemap')работает как раньше — ET-007 этот код не трогает. -
Синхронизация halo-слоёв с пользовательскими чекбоксами «Грунтовки» / «Тропы» (
app.js:2783–2826). В существующихonTrailsCheckbox()/restoreTrailsState()управляется видимость толькоtrails-trackиtrails-path-bridleway. Halo- underlay-слои сами по себе не отслеживаются; на спутнике это даёт «фантом» halo при выключенной грунтовке/тропе. Решение (TRZ §5.7): ввести единый хелперapplyTrailHaloVisibility(trackOn, pathOn)и вызывать его из (а)onTrailsCheckbox, (б)restoreTrailsState, (в)applyBaseLayer('satellite' | 'schematic'). Правило: halo видим ⇔currentBaseLayer === 'satellite' AND checkbox === ON. POI отдельной синхронизации не требуют — paint-правки текста привязаны к самимpoi-circles/poi-labels, которые управляютсяlayerState.poi/restorePoiState(). -
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 не требуется.
Ревизии
- 2026-05-31 — editorial: code-review fixes (12-review.md attempt 2/3).
Решения P/M/S/O/H не пересматривались. Правки:
- §5 пункт 1: реальные id halo-слоёв
(
trails-track-halo-satellite,trails-path-bridleway-halo-satellite) вместо несуществующихtrails-grade1..5-halo-satellite/paths-bridleway-halo-satellite(P1-1). - §5 пункт 2 (POI labels): добавлена правка
text-colorна спутнике + явный baseline возврата per-theme — без этого тёмный#333333поверх чёрного halo был нечитаем (P1-2). - §6: зафиксирована единая satellite-константа
#2a2a2aдля обеих тем; baseline dark исправлен#1a1a1a→#1a1a2eпод фактическое значениеstyle-dark.json:28(P1-4). - Добавлен §8: контракт с существующим
toggleLayer('basemap')/layerState.basemap— паттерн save&restore через_savedBasemapState(P1-5). - Добавлен §9: синхронизация halo-слоёв с пользовательскими
чекбоксами «Грунтовки»/«Тропы» — хелпер
applyTrailHaloVisibility(P1-6).
- §5 пункт 1: реальные id halo-слоёв
(
Связанные документы
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) — «одна фича = один скрипт + глобали»