MapView
Векторная карта — один и тот же движок (maplibre-native) на iOS и Android — как
Presentable: пушь её, открывай на весь экран и накладывай поверх свой UI
виджетами. Ты приносишь стиль MapLibre (по умолчанию — бесплатный готовый);
карта рисует подложку, твои именованные слои маркеров и линий и метку местоположения пользователя,
которую ты питаешь из Geolocation. Опционален для хоста —
проверяй через MapView.isSupported.
Плагин приложения: перечисли его в app.json — "plugins": { "map": "*" }. Список один для всех
платформ (заголовок бандла // plugins:): плагина, которого он не называет, нет — и в шелле,
собранном из проекта, и в веб-плеере, а компиляция предупреждает о том, который приложение
использует, но не перечисляет. Приложение-просмотрщик LeCodes регистрирует каждый первопартийный
плагин, что бы ни говорил список (в его iOS App Clip карты нет).
Где работает: iOS и Android, а также веб-плеер (lecodes dev --web), чья половина — близнец плагина
на maplibre-gl — тот же стиль, тот же API, так что логику карты можно тестировать в браузере;
внешний вид — у родственного рендерера, проверяй его на устройстве. Не на десктопе, не в headless
(lecodes render) и не в Web Lite — там isSupported равен false.
Обзор
const places: { id: string, name: string, lngLat: LngLat }[] = [
{ id: "opera", name: "Opera", lngLat: [56.2415, 58.0105] },
{ id: "embankment", name: "Embankment", lngLat: [56.2280, 58.0150] },
]
const title = UIText("").style({ color: "white", fontSize: 18 })
const card = UIBottomSheet(title).style({ bgColor: "#1c1c1e", padding: 24 })
const map = MapView({ center: [56.24, 58.01], zoom: 12 }) // стиль "liberty" по умолчанию
.onTap(() => card.hide())
map.markers("places", { cluster: true })
.onTap(m => { title.text = m.properties.title; card.show() })
.set(places.map(p => ({ id: p.id, lngLat: p.lngLat, title: p.name, color: "#DD4B40" })))
const header = UIWidget(UIButton(UIText("Back")).onClick(() => Router.pop()))
.style({ top: "safe-top", left: 16 })
map.setContent([header, card]) // содержимое карты: показывается вместе с ней; карточка скрыта до тапа
card.hide()
Router.init(UIScreen(
MapView.isSupported
? UIButton(UIText("Show the map")).onClick(() => Router.push(map))
: UIText("No map on this device"),
))Создание
MapView.isSupported: boolean // статическое — зарегистрировал ли этот хост view "map"?
MapView(options?: {
style?: "liberty" | "positron" | … // готовый стиль (по умолчанию "liberty"), URL,
| string | FetchResponse | object // asset(), прочитанный файл или сам стиль
center?: [lng, lat], zoom?, minZoom?, maxZoom?, bearing?, pitch?
rotate?: boolean // жест поворота, по умолчанию true
tilt?: boolean // жест наклона, по умолчанию false
}): MapViewКоординаты везде — [longitude, latitude] (порядок GeoJSON — тот же, что у стиля, тайлов и
данных, которые у тебя уже есть). MapView — полноценный NativeView: Router.push(map),
map.open({ transition }) / map.close(), onOpen / onClose / onBack, а также сырой канал
call / on остаются доступны.
Откуда берётся стиль
MapView() // карта мира, без настройки
MapView({ style: "positron" }) // другой готовый
MapView({ style: "https://tiles.example.com/styles/city/style.json" }) // URL, карта загружает его сама
MapView({ style: asset("./map/style.json") }) // в комплекте с приложением
MapView({ style: fetchLocal("style.json") }) // файл, который у приложения уже есть
fetch(url).then(style => MapView({ style })) // скачан один раз, закеширован в `files`
MapView({ style: { ...base, sources: { openmaptiles: { type: "vector", url: `${server}/data/v3.json` } } } })MapView() вообще без стиля рисует карту: готовые имена — liberty (по умолчанию), bright,
positron, dark, fiord — это стили на OpenFreeMap, который отдаёт
данные OSM без ключа, регистрации и лимитов запросов, включая коммерческое использование. Две вещи,
которые надо знать, прежде чем на нём выпускаться: укажи авторство («© OpenFreeMap ©
OpenMapTiles, data from OpenStreetMap» — данные OSM требуют атрибуции при любом стиле), и это
публичный инстанс на пожертвования без SLA, так что продукт с реальным трафиком должен крутить
собственные тайлы (весь стек open-source) и передавать URL того стиля.
URL переходит в нативную карту как URL. Любая другая форма читается обёрткой и переходит как
текст стиля, один раз, при создании — так что собранному в комплект asset("./map/style.json")
не нужен сервер для самого стиля (его тайлы, спрайты и глифы по-прежнему загружаются с url, которые
он называет), а объект стиля — естественный способ подставить адрес тайл-сервера в рантайме. asset()
даёт URL в серверной компиляции и локальный файл в компиляции шелла; тот же вызов работает в обоих
случаях.
Каждый URL внутри стиля должен быть абсолютным. maplibre-native — в отличие от maplibre-gl-js — никогда не разрешает относительные
sources[…].url/sprite/glyphsотносительно адреса самого стиля: стиль загружается,readyсрабатывает, твои слои рисуются, а подложка остаётся пустой. Инстанс tileserver-gl выдаёт относительные url, пока не задан егоpublicUrl(--public_url). У стиля, переданного файлом или объектом, адреса нет вовсе, так что правило то же.
нативная карта существует только после показа и загрузки её стиля. Каждый вызов данных и камеры до
этого ставится в очередь и воспроизводится по порядку, так что собирай карту декларативно и пушь
когда угодно — onReady срабатывает после воспроизведения. Перезагрузка стиля стирает всё, что
приложение положило в стиль, поэтому обёртка заново применяет свои слои, их данные, отступы и
метку каждый раз, когда карта сообщает о готовности; onReady срабатывает снова вместе с ними.
getCamera() — единственный вызов не из очереди: он спрашивает карту сразу и отклоняется, пока
карты ещё нет — вызывай его после onReady.
Слои
map.markers(name, options?: { cluster?, clusterRadius?, clusterMaxZoom? }): MarkerLayer
map.line(name, options?: { color?, width?, opacity? }): LineLayer
markers.set(items: { id, lngLat, icon?, color?, title?, ...props }[]): this // заменяет
markers.clear(): this
markers.onTap(cb: (m: { id, lngLat, properties }) => void): this
line.set(coordinates: [lng, lat][]): this
line.clear(): this
map.setData(source: string, geojson: object): this // сырой: любой GeoJSON-источник в стилеСлой — один именованный GeoJSON-источник; set() заменяет его содержимое. Стиль первичен: если
стиль уже объявляет источник с таким именем, его слои используются как есть и пушатся только данные —
так спроектированный стиль (кластеры со счётчиками по цветам, правила подписей, выражения иконок)
остаётся в JSON, где ему и место. Иначе плагин создаёт источник и слои по умолчанию: точку цвета
color, изображение icon, если задано, подпись title и круги кластеров со счётчиками при
cluster: true. Тап по кластеру приближает; он ничего не эмитит. clusterRadius в px (по
умолчанию 50); width линии в px (по умолчанию 4), её opacity 0–1 (по умолчанию 1).
icon называет изображение в спрайте стиля. Мультиспрайтовый стиль ("sprite": [{ id, url }, …],
поддерживается нативно) адресует его по id спрайта: "place:tag_bear" = изображение tag_bear
спрайта place. Дополнительные ключи элемента становятся свойствами фичи и возвращаются в onTap.
Камера
map.flyTo(center: [lng, lat], options?: { zoom?, bearing?, pitch?, duration? }): this // анимированно, duration в мс (по умолчанию 600)
map.jumpTo(center, options?): this // мгновенно
map.fitPoints(points: [lng, lat][], options?: { padding?, maxZoom?, animate? }): this
map.setPadding({ top?, left?, bottom?, right? }): this // px или "40%" — часть вида, которую закрывает твой UI
map.getCamera(): Promise<{ center, zoom, bearing, pitch }>
map.onMove(cb: (camera) => void): this // камера остановилась (конец жеста или анимации)
map.onTap(cb: (tap: { lngLat, point }) => void): this // тап, не попавший ни в один из твоих слоёв
map.onError(cb: (e: { message }) => void): this // стиль/источник, который карта не смогла загрузить
map.onReady(cb: () => void): this // стиль загружен, вызовы из очереди воспроизведеныonError никогда не фатален — карта остаётся рабочей. Без обработчика сообщение уходит в
console.error, так что незагрузившийся стиль говорит об этом, а не показывает пустую карту.
Вызовы камеры — fire-and-forget и цепочечные; ждёт только getCamera. setPadding — случай
нижнего листа: с { bottom: "40%" } каждый flyTo / fitPoints центрирует в видимых 60 %.
Bearing — градусы по часовой от севера, pitch — градусы от вертикали.
Местоположение пользователя
map.setUserLocation(lngLat: [lng, lat] | null, options?: { accuracy?, heading? }): thisКарта только рисует метку (точка, ореол точности в метрах, клин направления в градусах); откуда
берётся позиция — твоё дело: плагин не трогает железо геолокации, не запрашивает разрешений, а у
приложения без плагина geolocation истории с местоположением нет вовсе.
Geolocation.watch(pos =>
map.setUserLocation([pos.longitude, pos.latitude], { accuracy: pos.accuracy, heading: pos.heading }))
.then(watch => map.onClose(() => { watch.stop(); map.setUserLocation(null) }))Подводные камни
map.flyTo([58.01, 56.24]) // ✗ lat, lng — попадает в Индийский океан
map.flyTo([56.24, 58.01]) // ✓ lng, latСмотрите также
- NativeView — канал под капотом и как достучаться до него напрямую
- UIWidget — шапка / лист над картой (
map.setContent([...])) - Geolocation — позиция для
setUserLocation - Экраны и Router — пуш карты как destination