Перейти к содержанию

Карта и камера

MapWindow — это поверхность, Map — то, чем вы управляете. На платформе это mapView.mapWindow.map, а в общем коде — mapWindow.map.

Движение камеры

CameraPosition — value-тип: точка, зум, азимут и наклон.

val moscow = CameraPosition(
    target = Point(55.751225, 37.629540),
    zoom = 15f,
    azimuth = 0f,
    tilt = 0f,
)

map.move(moscow)

С анимацией и CameraCallback, который сообщает, завершилось движение или было отменено следующим:

map.move(
    cameraPosition = moscow,
    animation = Animation(Animation.Type.SMOOTH, 1.seconds),
    cameraCallback = CameraCallback { completed ->
        if (completed) println("arrived")
    },
)

Длительности — это kotlin.time.Duration

MapKit принимает секунды как Float; обёртка принимает 1.seconds, 300.milliseconds и конвертирует сама.

Вписать геометрию в экран

cameraPosition(geometry) вычисляет позицию, которая проецирует геометрию в фокус-прямоугольник, ничего при этом не двигая:

val route = Polyline(points)

map.move(
    map.cameraPosition(route.toGeometry()),
    Animation(Animation.Type.SMOOTH, 500.milliseconds),
)

focusRect, azimuth и tilt необязательны и по умолчанию берутся из текущего состояния карты:

map.cameraPosition(
    geometry = boundingBox.toGeometry(),
    focusRect = ScreenRect(ScreenPoint(0f, 0f), ScreenPoint(width, height * 0.6f)),
    azimuth = 0f,
    tilt = 0f,
)

Любой тип геометрии превращается в Geometry через toGeometry()Point, Polyline, Polygon, MultiPolygon, BoundingBox, Circle.

Ограничить камеру

map.cameraBounds.apply {
    setMinZoomPreference(10f)
    setMaxZoomPreference(18f)
    latLngBounds = BoundingBox(
        southWest = Point(55.55, 37.35),
        northEast = Point(55.95, 37.85),
    )
}

resetMinMaxZoomPreference() возвращает ограничения зума обратно MapKit.

Следить за камерой

class MapController(private val map: Map) {

    private val cameraListener = CameraListener { _, position, reason, finished ->
        if (finished && reason == CameraUpdateReason.GESTURES) {
            reload(position.target)
        }
    }

    init {
        map.addCameraListener(cameraListener.asWeakRef())
    }
}

CameraUpdateReason отличает GESTURES от APPLICATION. Слушателя нужно держать в поле — см. Слушатели и WeakRef.

Видимая область

visibleRegion — это четыре угла того, что сейчас на экране; visibleRegion(cameraPosition) вычисляет их для позиции, до которой камера ещё не доехала.

val region: VisibleRegion = map.visibleRegion

val bounds: BoundingBox = region.getBounds()
val geometry: Geometry = region.toPolygon()

val visible = pois.filter { poi ->
    poi.latitude in bounds.southWest.latitude..bounds.northEast.latitude &&
        poi.longitude in bounds.southWest.longitude..bounds.northEast.longitude
}

Latitude и Longitude — value-классы, реализующие Comparable, поэтому проверка диапазона выше компилируется.

getBounds() есть также у Point, Polyline, LinearRing, Polygon, BoundingBox и у пары bounding box'ов — так наращивают общий прямоугольник по набору объектов.

Экранные и мировые координаты

val screenPoint: ScreenPoint? = mapWindow.worldToScreen(Point(55.751225, 37.629540))
val worldPoint: Point? = mapWindow.screenToWorld(ScreenPoint(100f, 200f))

Оба возвращают null, если точку спроецировать нельзя — она за горизонтом или вне окна.

mapWindow.width и mapWindow.height — размер в физических пикселях, а addSizeChangedListener сообщает об изменениях:

private val sizeChangedListener = SizeChangedListener { _, width, height ->
    println("$width x $height")
}

mapWindow.addSizeChangedListener(sizeChangedListener.asWeakRef())

Фокус и точка обзора

focusRect сужает область окна, о которой рассуждает камера, — это нужно, когда нижнюю половину карты закрывает bottom sheet и «по центру» должно означать по центру видимой части.

mapWindow.focusRect = ScreenRect(
    topLeft = ScreenPoint(0f, 0f),
    bottomRight = ScreenPoint(mapWindow.width.toFloat(), mapWindow.height * 0.6f),
)

focusPoint смещает точку, вокруг которой камера вращается и масштабируется, gestureFocusPoint делает то же самое только для жестов, а gestureFocusPointMode решает, действует ли она всегда или только во время жеста. focusRegion — видимая область фокус-прямоугольника.

pointOfView переключается между SCREEN_CENTER и ADAPT_TO_FOCUS_POINT_HORIZONTALLY.

scaleFactor масштабирует содержимое карты — подписи, иконки, толщину линий — относительно плотности экрана.

Жесты и внешний вид

map.isRotateGesturesEnabled = false
map.isTiltGesturesEnabled = false
map.isScrollGesturesEnabled = true
map.isZoomGesturesEnabled = true

map.isFastTapEnabled = true
map.isNightModeEnabled = isDarkTheme
map.poiLimit = 5
map.mapType = MapType.VECTOR_MAP
map.set2DMode(true)

mapType выбирает NONE, MAP, SATELLITE, HYBRID или VECTOR_MAP. mode выбирает назначение карты — MAP, TRANSIT, DRIVING, ADMIN, LEGACY_MAP, FUTURE_MAP, HYBRID — и меняет то, что подчёркивает базовая карта.

isHdModeEnabled, isAwesomeModelsEnabled, isTransparentBackgroundEnabled и isBuildingsAboveIndoorEnabled — оставшиеся переключатели отрисовки. poiLimit = null возвращает ограничение обратно MapKit.

Стили карты

JSON-трансформация стиля в формате, описанном официальной документацией:

val applied = map.setMapStyle(
    """[{"tags": {"any": ["building"]}, "stylers": {"visibility": "off"}}]""",
)

Несколько стилей могут сосуществовать под разными идентификаторами, а resetMapStyles() снимает все:

map.setMapStyle(id = 1, style = hideBuildings)
map.setMapStyle(id = 2, style = dimRoads)
map.resetMapStyles()

Нажатия на карту

InputListener сообщает о нажатиях, которые попали в саму карту, а не в объект на ней.

class MapController(private val map: Map) {

    private val inputListener = InputListener(
        onMapTap = { _, point -> addPlacemark(point) },
        onMapLongTap = { _, point -> openMenu(point) },
    )

    init {
        map.addInputListener(inputListener.asWeakRef())
    }
}

Нажатие на POI, здание или топоним сначала уходит в GeoObjectTapListener — см. Объекты базовой карты.

Дождаться отрисовки карты

MapLoadedListener срабатывает, когда видимые тайлы отрисованы, и приносит тайминги и количество объектов:

private val mapLoadedListener = MapLoadedListener { statistics ->
    println("fully loaded in ${statistics.fullyLoaded}, ${statistics.renderObjectCount} objects")
}

map.setMapLoadedListener(mapLoadedListener.asWeakRef())

Передайте null, чтобы перестать слушать.

Планы помещений

Когда камера над зданием с планом помещений, MapKit сообщает об этом и позволяет переключать этажи:

class IndoorController(private val map: Map) {

    private var plan: IndoorPlan? = null

    private val indoorStateListener = IndoorStateListener(
        onActivePlanFocused = { plan = it },
        onActivePlanLeft = { plan = null },
        onActiveLevelChanged = { levelId -> println("level $levelId") },
    )

    init {
        map.isIndoorEnabled = true
        map.addIndoorStateListener(indoorStateListener.asWeakRef())
    }

    fun selectLevel(levelId: String) {
        plan?.activeLevelId = levelId
    }
}

IndoorPlan.levels перечисляет IndoorLevel текущего плана.

Логотип Яндекса

MapKit требует, чтобы логотип оставался видимым; его можно только переместить.

map.logo.setAlignment(
    LogoAlignment(
        horizontalAlignment = LogoHorizontalAlignment.LEFT,
        verticalAlignment = LogoVerticalAlignment.BOTTOM,
    ),
)
map.logo.setPadding(LogoPadding(horizontalPadding = 16, verticalPadding = 16))

Оверлеи

addRasterScreenOverlay() кладёт растровое изображение поверх карты в экранных координатах — легенду, компас, шкалу погоды:

val overlay = mapWindow.addRasterScreenOverlay()
overlay.setImage(legendImage, ScreenRect(ScreenPoint(16f, 16f), ScreenPoint(216f, 96f)))

overlay.remove()

Геометрические помощники

Geo покрывает вычисления, которые отдаёт MapKit:

val metres = Geo.distance(from, to)
val bearing = Geo.course(from, to)
val nearest = Geo.closestPoint(point, Segment(from, to))
val midpoint = Geo.pointOnSegmentByFactor(Segment(from, to), factor = 0.5)

PolylineUtils и SubpolylineHelper работают с ломаными и позициями на них, а Projection (из map.projection()) переводит мировые координаты в тайловые XYPoint и обратно.

Частота кадров

mapWindow.setMaxFps(30)

Очистка карты

map.wipe() удаляет всё, что MapKit нарисовал и закешировал для этой карты. Это тяжёлая операция, рассчитанная на момент разрушения карты, а не на скрытие объектов — для этого есть mapObjects.clear().