# Map Template

`Map Page 0.1.0` — первый готовый функциональный шаблон NDC Module Foundry. Он описывает пространственный интерфейс через стабильные доменные сущности, а не через API конкретного renderer.

## Границы ответственности

- Page Template владеет компоновкой страницы, Inspector, Toolbar, Assistant entry point, слотами данных и системными действиями.
- Empty Map Scene Fixture проверяет shell без каких-либо shipped domain-объектов.
- Application Manifest фиксирует экземпляр страницы, выбранный Design Profile и feature visibility.
- Platform capability binding связывает слоты шаблона с scoped data products через Foundry runtime BFF.
- Renderer adapter преобразует provider-neutral scene в Cesium или другой поддерживаемый renderer.

Engine/NDC остаётся средой создания и исполнения автоматизаций. Он не является владельцем визуального языка Map Template и не экспортирует в Studio внутренние Cesium objects.

## Зафиксированные сущности v0.1

- viewport и базовые capabilities;
- base/buildings/grid layers;
- planet-scale grid с LOD bands;
- place targets;
- moving objects и traces;
- универсальные pins и labels;
- rail/metro/stop/terminal stations;
- routes и track segments;
- polygon/multipolygon zones;
- selection;
- ready/loading/empty/stale/error/offline states.

Размеры и внешний вид разновидностей сущностей задаются ссылками на
`styleProfiles`. Для live `entity-stream` и `zone-stream` Application page хранит отдельные
versioned `presentationProfiles`, а binding выбирает один из них через
`presentationProfileId`. Это позволяет одному доменному типу иметь разные
подтверждённые варианты, не создавая новый renderer-specific тип.

Канонический moving-object профиль находится в
`registry/map-presentation-profiles.json`. Он задаёт универсальную геометрию
таргета, плашку подписи, LOD и два пользовательских status facets: связь и
движение. Для текущего Gelios binding значения и подписи повторяют официальный
статусный набор: `Онлайн`, `Офлайн`, `В движении`, `Стоят`. Position
quality и freshness остаются внутренними фактами качества и не дублируют
пользовательские фильтры. Классы, фильтры, счётчики и sort order читают один
profile; renderer не содержит provider-specific условий.

Кнопка `Объекты` открывает вверх текстовый dropdown из Application bindings. Выбор строки открывает или фокусирует отдельное canonical `WorkspaceWindow`, а его универсальное содержимое строится из presentation profile/facets. Несколько binding-окон могут быть открыты одновременно; stable identity всегда `bindingId`, поэтому пользовательское переименование не ломает сохранённый layout.

Клик по pin или его label открывает второе canonical `WorkspaceWindow` — карточку
выбранного объекта. Её вкладки и поля задаёт versioned provider-neutral
`subjectDetailProfile`, а не renderer и не provider payload. Базовый профиль
находится в `registry/map-subject-detail-profiles.json`. Он разделяет обзор,
позицию, телеметрию, счётчики, оборудование, настройки, ТО и происхождение
данных. Неизвестные поля не получают автоматическую вкладку `Other`: UI
показывает только явно классифицированную проекцию. Динамические показания
сенсоров требуют отдельного `allowedReadingIds`; пустой список означает явный
запрет вывода.

Несколько Data Product могут собираться в одну карточку без смешивания
контрактов. Primary binding владеет `subjectDetailProfileId`, а
`subject-aspect-stream` bindings указывают `joinToBindingId` и уникальный
`aspectId`. Соединение выполняется по стабильному `sourceId`; provider endpoint,
credentials и raw payload в Application Manifest не попадают.

В меню `Объекты` показываются только primary bindings. Joined aspects
(`technical profile`, `contacts`, `identity` и следующие типизированные
проекции) не являются самостоятельными слоями карты: они дополняют карточку
выбранного primary subject и потому не дублируются в меню. Если пользователь
переключает один subject на другой с совместимым detail profile, активная
вкладка карточки сохраняется; отсутствующие поля и секции не создаются.

Строка facet в окне объектов разделяет два действия. Клик по телу по-прежнему
применяет facet-фильтр, а отдельный chevron раскрывает список совпавших
subjects. Выбор subject в списке открывает его карточку и переносит текущую
композицию камеры к точке. Адаптер повторяет проверенную механику legacy
MMAP/AIS: сохраняет `heading`, `pitch`, `roll` и смещение камеры относительно
центра viewport; длительность перелёта для текущего канонического workflow —
`2.1 s`.

Facet-фильтры поддерживают мультивыбор: OR внутри одного facet и AND между facets. Missing facet означает отсутствие ограничения, а явно пустой список — ноль совпадений. Отжатие последнего chip не включает `Все`; `Все` включается и выключается только явным кликом. Переключение фильтра не двигает камеру, обзор выполняется отдельным действием.

## Platform reference layers

Станции метро, железнодорожные станции и вокзалы не являются L1/L2 workflow и
не материализуются как пользовательский Data Product. Это медленно меняющийся
provider-neutral справочный слой Platform Map Gateway:

```text
OSM/approved seed → Map Gateway spatial cache → map.station | map.terminal
                  → reference-points → Foundry presentation profile → renderer
```

Контракт `transport-stations.v1` возвращает только allowlisted проекцию:
стабильный `sourceId`, нормализованный semantic type, категорию, названия,
оператора/сеть, допустимые официальные идентификаторы и координаты. Raw OSM
payload, upstream endpoint, credentials и произвольные tags в Foundry не
попадают. Начальный snapshot Москвы получен из legacy L1 MMAP как donor и
разделён на три категории: `metro`, `railway_station`,
`railway_terminal`. За пределами snapshot Map Gateway может последовательно
загрузить spatial cells из OSM и сохраняет нормализованный результат в
persistent cache; повторный viewport читается локально.

Page Layout хранит три независимых reference bindings:

- `reference.transport.metro` → `map.reference.transport.metro.v1`;
- `reference.transport.railway_terminal` →
  `map.reference.transport.terminal.v1`;
- `reference.transport.railway_station` →
  `map.reference.transport.railway-station.v1`.

Каждый профиль отдельно управляет видимостью, высотой таргета, размером точки,
подписью и высотой скрытия через обычный Inspector. Старые сохранённые Map
Layout получают зарегистрированные reference bindings и profiles при чтении;
их файл не переписывается до явного сохранения пользователем.

Тот же Page Layout хранит `inspectorOpenSections`. В текущем single-open
Inspector это пустой список либо id ровно одной раскрытой секции. Поэтому
закрытие окна настроек и повторное открытие не сбрасывает аккордеон, а общая
кнопка Application Save сохраняет его вместе с камерой и остальным layout.

Cesium adapter рендерит elevated targets и label plates как единый overlay над
terrain, buildings и imagery (`disableDepthTestDistance`). Цветная обводка
головки остаётся частью `elevated-spike` profile. Головка рисуется как
billboard до label-слоя, поэтому непрозрачная плашка нормально перекрывает
лежащий под ней маркер вместо «рентгеновского» проступания его обводки. Text
outline плашки отключён и не создаёт дополнительный halo. Контуры
`surface-fill` остаются частью отдельного контракта геозон.

## Acceptance fixtures

- `registry/fixtures/map/map-empty-offline-v0.1.json` — отсутствие provider/live data без разрушения shell и управляющих действий.

Page/Visual/Application previews не содержат shipped domain fixtures. Объекты появляются только из declared Application bindings. Большие геоданные, tile cache, credentials и реальные streaming snapshots в репозиторий гайдлайнов не входят.

## Sandbox credit overlay

Во внутренней sandbox-сборке Map Page временно скрывает визуальный credit overlay renderer, потому что он перекрывает рабочую композицию во время настройки шаблона. Метаданные provider attribution не удаляются из runtime. Перед любым внешним, пользовательским или коммерческим развёртыванием overlay должен быть возвращён в соответствии с условиями выбранного provider. Это зафиксированный технический долг, а не правило production-интерфейса.

## Cesium adapter 1.143

Текущий reference adapter использует CesiumJS `1.143.0`. Он загружается отдельным lazy chunk только при открытии Map Page и преобразует только declared Application bindings в реальные Cesium entities. Пустая Map Page не содержит domain entities.

Server-side runtime contract:

- `GET /api/map/runtime-config` сообщает версию renderer и readiness возможностей, но не возвращает master token;
- `GET /api/map/ion/assets/:assetId/endpoint` разрешает только allowlisted assets и возвращает public provider URL без credential; private Gateway добавляет asset credential только к своему upstream request;
- `CESIUM_ION_TOKEN` передаётся только private Platform Map Gateway через deployment environment/secret, не в Foundry;
- `CESIUM_ION_ASSET_ALLOWLIST` ограничивает terrain/buildings/Gaussian assets;
- production endpoint переезжает в `platform/services/map-gateway`.

Development и production gateway позволяют проверить World Terrain и 3D Buildings, не сериализуя никакой Cesium/Bing credential во frontend.

## TileCache boundary

Runtime tile cache не является исходным кодом и не хранится в Git или Docker image layer. Production `map-gateway` использует NAS bind directory `/volume1/docker/nodedc-platform/map-gateway/live-tile-cache` и поддерживает:

- cache-first read/write и явный live-refresh;
- нормализацию cache key без credentials;
- upstream allowlist и SSRF protection;
- ограничения размера объекта и общего объёма;
- append-only no-overwrite по умолчанию, явный refresh viewport, stats, health и наблюдаемость;
- отдельный export/import версионируемых seed snapshots при необходимости.

Существующий Engine cache используется как donor поведения, но его runtime-файлы не копируются в Studio.

## Следующие контракты

`registry/schemas/map-lod-policy-v0.1.schema.json` и `registry/fixtures/map/map-lod-policies-v0.1.json` фиксируют первый provider-neutral LOD/visibility contract. Единственная метрика v0.1 — расстояние камеры в метрах; интервалы полуоткрытые `[minRange, maxRange)`, а `hysteresisRatio` предотвращает дрожание на границах. Renderer переводит эту политику в собственный API, но не меняет её смысл.

Дальше:

1. уточнить единый Label Style contract и варианты размеров уже на живой сцене;
2. расширить существующий HGeoZone batching contract явной spatial tiling-политикой для десятков тысяч геозон;
3. вынести development gateway в `platform/services/map-gateway` и подключить persistent cache;
4. добавить реальный Gaussian Splat 3D Tiles acceptance asset;
5. добавить специализированные adapters для traces и routes;
6. оформить renderer adapter capability matrix для Cesium и будущих providers.

## Live data-product runtime

`Map Page` хранит только provider-neutral binding: `dataProductId`, approved
semantic types, field projection, `presentationProfileId`, optional
`subjectDetailProfileId`, `aspectId`/`joinToBindingId` и `slotId`: `points`
для live point entities, `zones` для polygon/multipolygon `map.zone` или
`subject-details` для non-spatial current aspects выбранного объекта.
Platform-owned справочные сущности используют отдельный `reference-points`
slot и не подменяют этот Data Product contract.
При открытии Application page browser делает same-origin запрос к Foundry:

```text
Application/Page/Binding → Foundry BFF → External Data Plane snapshot/history → SSE patch
```

Foundry сопоставляет target с opaque reader grant в закрытом persistent
runtime. При первом exact consumer apply Foundry генерирует token локально и
передаёт EDP только его digest в запросе с отдельной service-подписью; EDP сам
разрешает unique active writer scope и fail-closed отклоняет ambiguity. Legacy
root-owned deployment directory используется только как совместимый fallback.
При смене versioned Data Product Foundry создаёт successor reader generation,
bootstrap-ит новый snapshot и только затем отзывает predecessor; существующий
grant никогда не переписывается другим request hash внутри одного поколения.
Browser, manifest и Cesium adapter не получают provider
endpoint, tenant/connection scope, reader token или raw provider payload.
Сначала server-owned Foundry consumer коммитит snapshot, затем применяет patch
events с exact cursor и только после commit делает fan-out всем viewers. При
пропуске cursor требуется snapshot rebase; несколько вкладок одного binding не
создают несколько upstream EDP subscriptions. Timeline использует provider-neutral
history route (`from/to/resolution/sourceIds/cursor`) через тот же BFF и не
занимает общую L2 execution queue. Renderer держит отдельный `CustomDataSource`
на point binding и обновляет stable entity id без пересоздания viewer. `zones`
рендерятся отдельным HGeoZone ground-projection layer: Polygon/MultiPolygon
пакуются ограниченными batches в Cesium ground primitives, а границы — в
ground-polyline primitives. Это renderer adapter, не новый тип онтологии:
источником истины остаётся `map.zone`, а цвета и прозрачности остаются в
provider-neutral presentation profile. Sampling и retention остаются политикой
Data Plane, а не Map Template.

`fleet.positions.current.v2@2.0.0` является первым state-aware moving-object
контрактом. Пользовательский status contract ограничен
`availability_state=online|offline` и `motion_state=moving|stationary`;
границы берутся из настроек объекта Gelios (`lostConnectionTimeValue` и
`minimumMovementSpeed`), а не из придуманных Foundry thresholds. Технические
`position_state`, `freshness_state` и `state_policy_version` могут оставаться в
факте для quality/diagnostics, но не образуют дополнительные UI-фильтры. Цвет,
геометрия таргета, label и порядок фильтров остаются в Foundry presentation
profile.
