Skip to content

Map objects

Everything you draw on the map lives in a collection. map.mapObjects is the root one — a RootMapObjectCollection — and addCollection() nests more inside it.

val objects = map.mapObjects
val pois = objects.addCollection()

Nesting is worth doing: a collection is a map object itself, so hiding it hides the whole group, zIndex applies to all of it and clear() empties it in one call.

Placemark

val placemark = objects.addPlacemark().apply {
    geometry = Point(55.751225, 37.629540)
    setIcon(icon)
    setIconStyle(IconStyle(anchor = PointF(0.5f, 1.0f), scale = 1.2f))
    userData = poi
}

addPlacemark() takes no arguments

MapKit deprecated the addPlacemark(point, icon) overloads; the object is created first and configured afterwards. PlacemarkCreatedCallback is the exception.

geometry, direction and opacity are properties. setIcon(image, style) sets the icon and its style at once, and setScaleFunction(points) makes the size depend on the zoom — each PointF is a (zoom, scale) pair:

placemark.setScaleFunction(listOf(PointF(10f, 0.5f), PointF(16f, 1f)))

Text

placemark.setText(
    text = "Red Square",
    style = TextStyle(
        size = 10f,
        color = Color.fromArgb(0xFF212121.toInt()),
        placement = TextStyle.Placement.BOTTOM,
        offset = 4f,
    ),
)

placemark.text returns the live PlacemarkText, whose text and style can be changed later.

Presentation objects

setIcon is a shortcut over a placemark's presentation. Asking for the presentation directly gives more: a composite icon with several named parts, a 3D model, or an animation.

val icon: Icon = placemark.useIcon()
icon.setImage(image, IconStyle(zIndex = 10f))
icon.style = IconStyle(scale = 2f)
val composite: CompositeIcon = placemark.useCompositeIcon()
composite.setIcon(name = "pin", image = pinImage, style = IconStyle(anchor = PointF(0.5f, 1f)))
composite.setIcon(name = "badge", image = badgeImage, style = IconStyle(zIndex = 1f))

composite.setIconStyle("badge", IconStyle(isVisible = false))
composite.removeIcon("badge")
val model: Model = placemark.useModel()
model.modelStyle = ModelStyle(
    scale = 1f,
    unitType = ModelStyle.UnitType.NORMALIZED,
    renderMode = ModelStyle.RenderMode.USER_MODEL,
)
model.setData(gltfDataProvider, Callback { println("model loaded") })
val animation: PlacemarkAnimation = placemark.useAnimation()
animation.setIcon(animatedImage, IconStyle())
animation.isReversed = false
animation.play(Callback { println("finished") })

animation.pause()
animation.resume()
animation.stop()

Calling useIcon() after useModel() replaces the presentation — a placemark has exactly one.

Polyline

val polyline = objects.addPolyline(Polyline(points))

polyline.style = LineStyle(
    strokeWidth = 5f,
    outlineWidth = 1f,
    outlineColor = Color.fromArgb(0xFF1B5E20.toInt()),
    dashLength = 8f,
    gapLength = 4f,
)
polyline.setStrokeColor(Color.fromArgb(0xFF4CAF50.toInt()))

Colouring segments

A polyline of n points has n − 1 segments, and each segment carries a palette index, not a colour. Define the palette first, then assign indexes:

polyline.setPaletteColor(0, Color.fromArgb(0xFF4CAF50.toInt()))
polyline.setPaletteColor(1, Color.fromArgb(0xFFFFC107.toInt()))
polyline.setPaletteColor(2, Color.fromArgb(0xFFF44336.toInt()))

polyline.setStrokeColors(listOf(0, 1, 2, 1))

getStrokeColor returns an index

getStrokeColor(segmentIndex) gives back the palette index of that segment; resolve it with getPaletteColor(colorIndex). setStrokeColor(color) is different — it is a real colour, and it writes palette entry 0, which every segment uses by default.

The overload with weights spreads the colours over the polyline by relative length instead of by segment:

polyline.setStrokeColors(paletteIndices = listOf(0, 2), weights = listOf(0.7, 0.3))

Selecting and hiding parts

val part = Subpolyline(
    begin = PolylinePosition(segmentIndex = 0, segmentPosition = 0.0),
    end = PolylinePosition(segmentIndex = 4, segmentPosition = 0.5),
)

polyline.select(Color.fromArgb(0xFF2196F3.toInt()), part)
polyline.hide(part)

Arrows

val arrow = polyline.addArrow(
    position = PolylinePosition(segmentIndex = 2, segmentPosition = 0.0),
    length = 12f,
    fillColor = Color.fromArgb(0xFF212121.toInt()),
)
arrow.outlineWidth = 1f
arrow.isVisible = true

polyline.arrows lists them.

Polygon

val polygon = objects.addPolygon(
    Polygon(
        outerRing = LinearRing(outerPoints),
        innerRings = listOf(LinearRing(holePoints)),
    ),
).apply {
    fillColor = Color.fromArgb(0x334CAF50)
    strokeColor = Color.fromArgb(0xFF4CAF50.toInt())
    strokeWidth = 2f
    isGeodesic = true
}

The fill can be a repeating image, static or animated:

polygon.setPattern(hatchImage, scale = 1f)
polygon.setPattern(animatedHatch, scale = 1f)
polygon.resetPattern()

Circle

objects.addCircle(Circle(center = Point(55.751225, 37.629540), radius = 500f)).apply {
    fillColor = Color.fromArgb(0x332196F3)
    strokeColor = Color.fromArgb(0xFF2196F3.toInt())
    strokeWidth = 2f
}

The radius is in metres.

Every object shares

mapObject.isVisible = false
mapObject.zIndex = 10f
mapObject.isDraggable = true
mapObject.userData = poi

parent is the collection the object belongs to, and isValid turns false once it has been removed.

Hiding can be animated, with a callback when the animation ends:

mapObject.setVisible(
    visible = false,
    animation = Animation(Animation.Type.SMOOTH, 300.milliseconds),
    onFinished = Callback { println("hidden") },
)

userData

userData is a plain Any? that MapKit stores next to the object and hands back on every callback. getCastedUserData<T>() reads it typed:

data class Poi(val id: String, val title: String)

placemark.userData = Poi("1", "Red Square")

val tapListener = MapObjectTapListener { mapObject, _ ->
    val poi = mapObject.getCastedUserData<Poi>() ?: return@MapObjectTapListener false
    open(poi)
    true
}

Taps and drags

class ObjectsController(private val objects: MapObjectCollection) {

    private val tapListener = MapObjectTapListener { mapObject, point ->
        select(mapObject)
        true
    }

    private val dragListener = MapObjectDragListener(
        onMapObjectDragStart = { it.opacity = 0.5f },
        onMapObjectDrag = { _, point -> preview(point) },
        onMapObjectDragEnd = { it.opacity = 1f },
    )

    fun add(point: Point, icon: ImageProvider) {
        objects.addPlacemark().apply {
            geometry = point
            setIcon(icon)
            isDraggable = true
            addTapListener(tapListener.asWeakRef())
            setDragListener(dragListener.asWeakRef())
        }
    }
}

Returning true from a tap listener stops the event; returning false lets it reach the map's InputListener. Both listeners are fields, because subscriptions are weak — see Listeners and WeakRef.

PlacemarkMapObject.opacity is a property of the placemark; the drag example above uses it to fade the object while it is being moved.

Working with a collection

objects.remove(mapObject)
objects.clear()

addListener reports what is being added and removed — MapKit adds objects asynchronously when clustering is involved:

private val collectionListener = MapObjectCollectionListener(
    onMapObjectAdded = { println("added $it") },
    onMapObjectRemoved = { println("removed $it") },
)

objects.addListener(collectionListener.asWeakRef())

placemarksStyler sets a scale function shared by every placemark of the collection, instead of setting it on each one:

objects.placemarksStyler.setScaleFunction(listOf(PointF(10f, 0.5f), PointF(16f, 1f)))

Walking the tree

traverse visits everything in the collection, descending into nested collections when the start callback returns true:

private val visitor = MapObjectVisitor(
    onPlacemarkVisited = { println("placemark at ${it.geometry}") },
    onPolylineVisited = { },
    onPolygonVisited = { },
    onCircleVisited = { },
    onCollectionVisitStart = { true },
    onCollectionVisitEnd = { },
    onClusterizedCollectionVisitStart = { true },
    onClusterizedCollectionVisitEnd = { },
)

objects.traverse(visitor.asWeakRef())

Label conflicts

The root collection decides what happens when labels overlap:

map.mapObjects.conflictResolutionMode = ConflictResolutionMode.MAJOR

Clustering

A ClusterizedPlacemarkCollection groups nearby placemarks into one icon. The ClusterListener is called for every cluster MapKit builds, and it is where the cluster icon is set.

class ClusterController(
    private val objects: MapObjectCollection,
    private val clusterIcon: ImageProvider,
) {

    private val clusterTapListener = ClusterTapListener { cluster ->
        zoomTo(cluster.placemarks.map { it.geometry })
        true
    }

    private val clusterListener = ClusterListener { cluster ->
        cluster.appearance.setIcon(clusterIcon)
        cluster.appearance.setText("${cluster.size}")
        cluster.addClusterTapListener(clusterTapListener.asWeakRef())
    }

    private val collection = objects.addClusterizedPlacemarkCollection(clusterListener.asWeakRef())

    fun show(points: List<Point>, icon: ImageProvider) {
        collection.addPlacemarks(points, icon, IconStyle())
        collection.clusterPlacemarks(clusterRadius = 60.0, minZoom = 15)
    }
}

clusterPlacemarks has to be called again after the content changes — MapKit does not recluster on its own. clusterRadius is in units of the placemark size, minZoom is the zoom above which clustering stops.

cluster.appearance is a PlacemarkMapObject, so everything on this page applies to it.

Creating placemarks in bulk

addEmptyPlacemarks(points) adds the objects without icons, which is much cheaper for a large set; you then fill them in.

val placemarks = collection.addEmptyPlacemarks(points)
placemarks.forEachIndexed { index, placemark ->
    placemark.setIcon(icons[index])
    placemark.userData = pois[index]
}

PlacemarkCreatedCallback is the other bulk path: MapKit creates the object and calls you back with it, which lets it defer the work.

private val createdCallback = PlacemarkCreatedCallback { placemark ->
    placemark.setIcon(icon)
}

collection.addPlacemark(createdCallback)

Animated images

AnimatedImageProvider feeds animated icons and polygon patterns. It comes either from data or from frames:

val fromData = AnimatedImageProvider.fromByteArray(bytes)
val fromFile = AnimatedImageProvider.fromFile(path)

val built = AnimatedImageProvider.fromAnimatedImage(
    AnimatedImage(loopCount = 0).apply {
        addFrame(frameOne, 100.milliseconds)
        addFrame(frameTwo, 100.milliseconds)
    },
)

loopCount = 0 loops forever. Where the static ImageProvider comes from is described on Image resources.