Fly Between Different Locations - Kotlin SDK

This example leverages the mobile SDKs to animate transitions between major U.S. locations and visualize school distribution using a single, high-performance cluster layer. The FlyTo feature ensures smooth, clear movement between locations.


import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.maptiler.maptilersdk.MTConfig
import com.maptiler.maptilersdk.events.MTEvent
import com.maptiler.maptilersdk.map.LngLat
import com.maptiler.maptilersdk.map.MTMapOptions
import com.maptiler.maptilersdk.map.MTMapView
import com.maptiler.maptilersdk.map.MTMapViewController
import com.maptiler.maptilersdk.map.MTMapViewDelegate
import com.maptiler.maptilersdk.map.options.MTCameraOptions
import com.maptiler.maptilersdk.map.options.MTFlyToOptions
import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
import com.maptiler.maptilersdk.map.style.MTMapStyleVariant
import com.maptiler.maptilersdk.map.style.MTStyle
import com.maptiler.maptilersdk.map.style.dsl.Expression
import com.maptiler.maptilersdk.map.style.dsl.Filter
import com.maptiler.maptilersdk.map.style.dsl.MTFeatureKey
import com.maptiler.maptilersdk.map.style.dsl.PropertyValue
import com.maptiler.maptilersdk.map.style.layer.circle.MTCircleLayer
import com.maptiler.maptilersdk.map.style.layer.circle.colorExpr
import com.maptiler.maptilersdk.map.style.layer.circle.radiusExpr
import com.maptiler.maptilersdk.map.style.source.MTGeoJSONSource
import com.maptiler.maptilersdk.map.types.MTData
import java.net.URL

class FlyToAnimationsComposeActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Replace with your MapTiler API key
    MTConfig.apiKey = "MY_API_KEY"
    setContent {
      FlyToAnimationsComposeMap()
    }
  }
}

@Composable
fun FlyToAnimationsComposeMap() {
  val controller = remember { MTMapViewController(baseContext) }

  LaunchedEffect(controller) {
    controller.delegate =
      object : MTMapViewDelegate {
        override fun onMapViewInitialized() {
          setupSchoolLayers(controller.style!!)

          // Start simple camera animation after layers are added
          startLocationAnimation(controller)
        }

        override fun onEventTriggered(event: MTEvent, data: MTData?) {
          // no-op for this example
        }
      }
  }

  DisposableEffect(controller) { onDispose { controller.delegate = null } }

  MTMapView(
    referenceStyle = MTMapReferenceStyle.DATAVIZ,
    options = MTMapOptions(),
    controller = controller,
    modifier = Modifier.fillMaxSize(),
    styleVariant = MTMapStyleVariant.LIGHT,
  )
}

private fun setupSchoolLayers(style: MTStyle) {
  val src =
      MTGeoJSONSource.fromUrl(
          identifier = "schools",
          url = URL("https://docs.maptiler.com/sdk-js/assets/Public_School_Characteristics_2020-21_no_prop.geojson"),
      ).apply {
          isCluster = true
          clusterRadius = 90.0
          clusterMaxZoom = 12.0
      }
  style.addSource(src)

  val clusters =
    MTCircleLayer(identifier = "schools-clusters", sourceIdentifier = src.identifier)
      .apply {
        withFilter(
          PropertyValue.array(
            PropertyValue.Str("all"),
            Filter.clusters(),
            PropertyValue.array(
              PropertyValue.Str(">="),
              Expression.get(MTFeatureKey.POINT_COUNT),
              PropertyValue.Num(100.0),
            ),
          ),
        )
        maxZoom = 10.5
        radiusExpr(
          Expression.step(
            input = Expression.get(MTFeatureKey.POINT_COUNT),
            default = PropertyValue.Num(16.0),
            stops = listOf(
              100.0 to PropertyValue.Num(22.0),
              750.0 to PropertyValue.Num(30.0),
            ),
          ),
        )
        colorExpr(
          Expression.step(
            input = Expression.get(MTFeatureKey.POINT_COUNT),
            default = PropertyValue.Color(Color.parseColor("#f1f075")), // yellow (low)
            stops = listOf(
              100.0 to PropertyValue.Color(Color.parseColor("#f59e0b")), // orange (mid)
              750.0 to PropertyValue.Color(Color.parseColor("#ef4444")), // red (high)
            ),
          ),
        )
      }
  style.addLayer(clusters)

  style.setPaintProperty(
    layerId = clusters.identifier,
    name = "circle-opacity",
    value =
      PropertyValue.array(
        PropertyValue.Str("interpolate"),
        PropertyValue.array(PropertyValue.Str("linear")),
        PropertyValue.array(PropertyValue.Str("zoom")),
        PropertyValue.Num(3.0), PropertyValue.Num(0.85),
        PropertyValue.Num(10.0), PropertyValue.Num(0.85),
        PropertyValue.Num(10.5), PropertyValue.Num(0.0),
      ),
  )
}

private fun startLocationAnimation(controller: MTMapViewController) {
  data class Location(val zoom: Double, val center: LngLat)

  val locations = listOf(
    Location(11.37, LngLat(-73.9757, 40.7684)), // NYC
    Location(11.82, LngLat(-71.07515, 42.37131)), // Boston
    Location(11.48, LngLat(-87.6922, 41.8699)), // Chicago
    Location(3.9, LngLat(-97.04, 38.17)), // USA overview
    Location(10.62, LngLat(-122.3241, 47.6127)), // Seattle
    Location(10.3, LngLat(-122.3634, 37.6689)), // SF Bay
    Location(11.15, LngLat(-95.3843, 29.75)), // Houston
    Location(3.9, LngLat(-97.04, 38.17)), // USA overview
  )

  val handler = Handler(Looper.getMainLooper())
  var idx = 0

  val flyOptions = MTFlyToOptions(curve = null, minZoom = null, speed = 0.2, screenSpeed = null, maxDuration = null)

  val runnable = object : Runnable {
    override fun run() {
      val loc = locations[idx % locations.size]
      controller.flyTo(
        cameraOptions = MTCameraOptions(center = loc.center, zoom = loc.zoom),
        flyToOptions = flyOptions,
      )
      idx++

      handler.postDelayed(this, 5000L)
    }
  }

  // Initial delay to ensure map is fully idle
  handler.postDelayed(runnable, 2500L)
}