Skip to content

Layers and tiles

A layer is a horizontal slice of the map with its own place in the render order. MapKit's own layers are named in LayerIds; you add your own next to them, either with tiles of your own or with a separate collection of map objects.

A custom tile layer

Map.addTileLayer takes an id, the layer options and a callback that configures the data source. The callback runs once, when the layer is created.

val layer: Layer = map.addTileLayer(
    layerId = "weather",
    layerOptions = LayerOptions(
        transparent = true,
        nightModeAvailable = false,
        cacheable = true,
    ),
) { builder ->
    builder.setTileUrlProvider(
        UrlProvider { tileId, _, _ ->
            "https://tiles.example.com/${tileId.z}/${tileId.x}/${tileId.y}.png"
        },
    )
    builder.setProjection(Projections.wgs84Mercator)
    builder.setZoomRanges(listOf(ZoomRange(zMin = 0, zMax = 19)))
    builder.setTileFormat(TileFormat.PNG)
}

layerId also decides where the layer sits: MapKit orders layers by id against its own, so an id compared against LayerIds is how you get above or below the traffic layer, the buildings or the map objects.

LayerOptions

Option Default What it does
active true an inactive layer draws nothing and requests no tiles
nightModeAvailable false the layer has a night variant
cacheable true tiles are kept on disk
animateOnActivation true activation fades in
tileAppearingAnimationDuration 150.milliseconds how long a tile fades in
overzoomMode DISABLED ENABLED / WITH_PREFETCH fill gaps from adjacent zoom levels
transparent false lower layers show through
versionSupport false the layer is versioned

Serving tiles yourself

setTileProvider replaces the URL with a function that returns the bytes, which is how you serve tiles rendered on the device or read from a bundled file:

builder.setTileProvider(
    TileProvider { tileId, version, _, etag ->
        RawTile(
            version = version,
            features = emptyMap(),
            etag = etag,
            useCache = RawTile.UseCache.YES,
            state = RawTile.State.OK,
            rawData = render(tileId),
        )
    },
)

Return RawTile.State.NOT_MODIFIED when etag says the tile has not changed, and RawTile.State.ERROR when it cannot be produced.

Driving the layer afterwards

Layer itself only removes; everything else is on its DataSourceLayer:

val dataSource = layer.dataSourceLayer()

dataSource.isActive = false
dataSource.setStyle(id = 0, style = """[{"stylers": {"opacity": 0.5}}]""")
dataSource.resetStyles()
dataSource.clear()

layer.remove()

Two subscriptions report what the layer is doing. Both take a single listener, and null clears it:

private val layerLoadedListener = LayerLoadedListener { println("tiles on screen") }
private val dataSourceListener = DataSourceListener { println("data source updated") }

dataSource.setLayerLoadedListener(layerLoadedListener.asWeakRef())
dataSource.setDataSourceListener(dataSourceListener.asWeakRef())

Projections

Projections.wgs84Mercator is what Yandex tiles use; Projections.sphericalMercator is the web-mercator most third-party tile servers serve. map.projection() returns the projection of the map itself, and converts between world coordinates and tile coordinates:

val xy: XYPoint = map.projection().worldToXY(Point(55.751225, 37.629540), zoom = 15)
val back: Point = map.projection().xyToWorld(xy, zoom = 15)

A layer of map objects

Map.addMapObjectLayer gives back a RootMapObjectCollection that is not the map's default one and sits at the layer id you name — the way to put a group of objects above or below something MapKit draws.

val routeObjects = map.addMapObjectLayer("my-routes")
routeObjects.addPolyline(Polyline(points))

One collection per id, for the lifetime of the map

MapKit links a collection to a layer id and cannot unlink it, so asking twice for the same id throws. Create it once and keep it; to empty it, use clear().

The Compose MapObjectLayer handles this for you — it caches the collection per id and only clears it when the composable leaves.

Traffic

class TrafficController(mapKit: MapKit, mapWindow: MapWindow) {

    private val layer = mapKit.createTrafficLayer(mapWindow)

    private val trafficListener = TrafficListener(
        onTrafficChanged = { level ->
            if (level != null) show(level.level, level.color)
        },
        onTrafficLoading = { showSpinner() },
        onTrafficExpired = { showStale() },
    )

    init {
        layer.isTrafficVisible = true
        layer.addTrafficListener(trafficListener.asWeakRef())
    }
}

TrafficLevel is a number 0..10 plus a TrafficColor (GREEN, YELLOW, RED). onTrafficChanged is called with null while the level is unknown.

The layer takes JSON styles too:

layer.setTrafficStyle("""[{"stylers": {"opacity": 0.6}}]""")
layer.setTrafficStyle(id = 1, style = dimJams)
layer.resetTrafficStyles()

Both style setters return whether the style was accepted.

In Compose

Both kinds of layer are composables that remove themselves when they leave the composition:

YandexMap(modifier = Modifier.fillMaxSize()) {
    TrafficLayer(onTrafficChanged = { level = it })

    TileLayer(
        layerId = "weather",
        options = LayerOptions(transparent = true),
        onLayerLoaded = { println("weather tiles are on screen") },
    ) { builder ->
        builder.setTileUrlProvider(
            UrlProvider { tileId, _, _ ->
                "https://tiles.example.com/${tileId.z}/${tileId.x}/${tileId.y}.png"
            },
        )
        builder.setProjection(Projections.wgs84Mercator)
        builder.setZoomRanges(listOf(ZoomRange(zMin = 0, zMax = 19)))
        builder.setTileFormat(TileFormat.PNG)
    }

    MapObjectLayer(layerId = "my-routes") {
        Polyline(state = rememberPolylineState(route))
    }
}

See Compose overview for the parameters.