Map | Mobile SDK | 2GIS Documentation

Map

To display a map, add a MapView to your activity:

<ru.dgis.sdk.map.MapView
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:dgis_cameraTargetLat="55.740444"
    app:dgis_cameraTargetLng="37.619524"
    app:dgis_cameraZoom="16.0"
/>

You can specify starting coordinates (cameraTargetLat for latitude and cameraTargetLng for longitude) and zoom level (cameraZoom).

MapView can also be created programmatically. In that case, you can specify starting coordinates and other settings as a MapOptions object.

To get the Map object, you can call the getMapAsync() method of MapView:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val sdkContext = DGis.initialize(applicationContext, apiKeys)
    setContentView(R.layout.activity_main)

    val mapView = findViewById<MapView>(R.id.mapView)
    lifecycle.addObserver(mapView)

    mapView.getMapAsync { map ->
        // Access map properties
        val camera = map.camera
    }
}

In some cases, to add objects to the map, you need to create a special object - a data source. Data sources act as object managers: instead of adding objects to the map directly, you add a data source to the map and add/remove objects from the data source.

There are different types of data sources: moving markers, routes that display current traffic condition, custom geometric shapes, etc. Each data source type has a corresponding class.

The general workflow of working with data sources looks like this:

// Create a data source
val source = MyMapObjectSource(
    sdkContext,
    ...
)

// Add the data source to the map
map.addSource(source)

// Add and remove objects from the data source
source.addObject(...)
source.removeObject(...)

To remove a data source and all objects associated with it from the map, call the removeSource() method:

map.removeSource(source)

You can get the list of all active data sources using the map.sources property.

To add dynamic objects to the map (such as markers, lines, circles, and polygons), you must first create a MapObjectManager object, specifying the map instance. Deleting an object manager removes all associated objects from the map, so do not forget to save it in activity.

mapObjectManager = MapObjectManager(map)

After you have created an object manager, you can add objects to the map using the addObject() and addObjects() methods. For each dynamic object, you can specify a userData field to store arbitrary data. Object settings can be changed after their creation.

To remove objects from the map, use removeObject() and removeObjects(). To remove all objects, call the removeAll() method.

To add a marker to the map, create a Marker object, specifying the required options, and pass it to the addObject() method of the object manager.

The only required parameter is the coordinates of the marker (position).

val marker = Marker(
    MarkerOptions(
        position = GeoPointWithElevation(
            latitude = 55.752425,
            longitude = 37.613983
        )
    )
)

mapObjectManager.addObject(marker)

To change the marker icon, specify an Image object as the icon parameter. You can create Image using the following functions:

val icon = imageFromResource(sdkContext, R.drawable.ic_marker)

val marker = Marker(
    MarkerOptions(
        position = GeoPointWithElevation(
            latitude = 55.752425,
            longitude = 37.613983
        ),
        icon = icon
    )
)

To change the hotspot of the icon, use the anchor parameter.

You can also set the text for the marker and other options (see MarkerOptions).

To draw a line on the map, create a Polyline object, specifying the required options, and pass it to the addObject() method of the object manager.

In addition to the coordinates of the line points, you can set the line width, color, stroke type, and other options (see PolylineOptions).

// Coordinates of the vertices of the polyline
val points = listOf(
    GeoPoint(latitude = 55.7513, longitude = 37.6236),
    GeoPoint(latitude = 55.7405, longitude = 37.6235),
    GeoPoint(latitude = 55.7439, longitude = 37.6506)
)

// Creating a Polyline object
val polyline = Polyline(
    PolylineOptions(
        points = points,
        width = 2.lpx
    )
)

// Adding the polyline to the map
mapObjectManager.addObject(polyline)

Extension property .lpx in the example above converts an integer to a LogicalPixel object.

To draw a polygon on the map, create a Polygon object, specifying the required options, and pass it to the addObject() method of the object manager.

Coordinates for the polygon are specified as a two-dimensional list. The first sublist must contain the coordinates of the vertices of the polygon itself. The other sublists are optional and can be specified to create a cutout (a hole) inside the polygon (one sublist - one polygonal cutout).

Additionally, you can specify the polygon color and stroke options (see PolygonOptions).

val polygon = Polygon(
    PolygonOptions(
        contours = listOf(
            // Vertices of the polygon
            listOf(
                GeoPoint(latitude = 55.72014932919687, longitude = 37.562599182128906),
                GeoPoint(latitude = 55.72014932919687, longitude = 37.67555236816406),
                GeoPoint(latitude = 55.78004852149085, longitude = 37.67555236816406),
                GeoPoint(latitude = 55.78004852149085, longitude = 37.562599182128906),
                GeoPoint(latitude = 55.72014932919687, longitude = 37.562599182128906)
            ),
            // Cutout inside the polygon
            listOf(
                GeoPoint(latitude = 55.754167897761, longitude = 37.62422561645508),
                GeoPoint(latitude = 55.74450654680055, longitude = 37.61238098144531),
                GeoPoint(latitude = 55.74460317215391, longitude = 37.63435363769531),
                GeoPoint(latitude = 55.754167897761, longitude = 37.62422561645508)
            )
        ),
        borderWidth = 1.lpx
    )
)

mapObjectManager.addObject(polygon)

Do not add a collection of objects to the map using the addObject method in a loop for the whole collection: this might lead to performance losses. To add a collection of objects, prepare the whole collection and add it using the addObjects method:

// preparing the object collection
val markers = mutableListOf<Marker>()
val markerOptions = listOf(
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    ...)
markerOptions.forEach({
    markers.add(Marker(it))
})

// adding the collection to the map
MapObjectManager.addObjects(markers)

To add markers to the map in clustering mode, you must create a MapObjectManager object using MapObjectManager.withClustering(), specifying the map instance, distance between clusters in logical pixels, maximum value of zoom-level, when MapObjectManager in clustering mode, and user implementation of the protocol SimpleClusterRenderer. SimpleClusterRenderer is used to customize clusters in MapObjectManager.

val clusterRenderer = object : SimpleClusterRenderer {
    override fun renderCluster(cluster: SimpleClusterObject): SimpleClusterOptions {
        val textStyle = TextStyle(
            fontSize = LogicalPixel(15.0f),
            textPlacement = TextPlacement.RIGHT_TOP
        )
        val objectCount = cluster.objectCount
        val iconMapDirection = if (objectCount < 5) MapDirection(45.0) else null
        return SimpleClusterOptions(
            icon,
            iconWidth = LogicalPixel(30.0f),
            text = objectCount.toString(),
            textStyle = textStyle,
            iconMapDirection = iconMapDirection,
            userData = objectCount.toString()
        )
    }
}

mapObjectManager = MapObjectManager.withClustering(map, LogicalPixel(80.0f), Zoom(18.0f), clusterRenderer)

To make objects on the map visually react to selection, configure the styles to use different layer look using the "Add state dependency" function for all required properties (icon, font, color, and others):

To configure properties of the selected object, go to the "Selected state" tab:

First, get information about the objects falling into the tap region using the getRenderedObjects() method like in the example of Getting objects using screen coordinates.

To highlight objects, call the setHighlighted() method that takes a list of IDs from the variable objects directory DgisObjectId. Inside the getRenderedObjects() method, you can get all data required to use this method like the source of objects data and their IDs:

override fun onTap(point: ScreenPoint) {
    map.getRenderedObjects(point).onResult { renderedObjects ->
        // Getting the closest object to the tap spot inside the specified radius
        val dgisObject = renderedObjects
            .firstOrNull { it.item.source is DgisSource && it.item.item is DgisMapObject }
            ?: return@onResult

        // Saving the object data source and the object ID
        val source = dgisObject.item.source as DgisSource
        val id = (dgisObject.item.item as DgisMapObject).id

        searchManager.searchByDirectoryObjectId(id)
	    .onResult onDirectoryObjectReady@ {
	        val obj = it ?: return@onDirectoryObjectReady

		val entrancesIds = obj.buildingEntrances.map { entranceInfo ->
			entranceInfo.id
		} as MutableList<DgisObjectId>
		entrancesIds.add(id)

                // Removing highlight of previously selected objects
		source.setHighlighted(source.highlightedObjects, false)
                // Highlighting the required objects and entrances
		source.setHighlighted(entrancesIds, true)
            }
    }
}

You can control the camera by accessing the map.camera property. See the Camera object for a full list of available methods and properties.

You can change the position of the camera by calling the move() method, which initiates a flight animation. This method has three parameters:

  • position - new camera position (coordinates and zoom level). Additionally, you can specify the camera tilt and rotation (see CameraPosition).
  • time - flight duration in seconds as a Duration object.
  • animationType - type of animation to use (CameraAnimationType).

The call will return a Future object, which can be used to handle the animation finish event.

val mapView = findViewById<MapView>(R.id.mapView)

mapView.getMapAsync { map ->
    val cameraPosition = CameraPosition(
        point = GeoPoint(latitude = 55.752425, longitude = 37.613983),
        zoom = Zoom(16.0),
        tilt = Tilt(25.0),
        bearing = Arcdegree(85.0)
    )

    map.camera.move(cameraPosition, Duration.ofSeconds(2), CameraAnimationType.LINEAR).onResult {
        Log.d("APP", "Camera flight finished.")
    }
}

You can use the .seconds extension to specify the duration of the flight:

map.camera.move(cameraPosition, 2.seconds, CameraAnimationType.LINEAR)

For more precise control over the flight, you can create a flight controller that will determine the camera position at any given moment. To do this, implement the CameraMoveController interface and pass the created object to the move() method instead of the three parameters described previously.

The current state of the camera (i.e., whether the camera is currently in flight) can be obtained using the state property. See CameraState for a list of possible camera states.

val currentState = map.camera.state

You can subscribe to changes of camera state using the stateChannel property.

// Subscribe to camera state changes
val connection = map.camera.stateChannel.connect { state ->
    Log.d("APP", "Camera state has changed to ${state}")
}

// Unsubscribe when it's no longer needed
connection.close()

The current position of the camera can be obtained using the position property (see CameraPosition).

val currentPosition = map.camera.position

Log.d("APP", "Coordinates: ${currentPosition.point}")
Log.d("APP", "Zoom level: ${currentPosition.zoom}")
Log.d("APP", "Tilt: ${currentPosition.tilt}")
Log.d("APP", "Rotation: ${currentPosition.bearing}")

You can subscribe to changes of camera position using the positionChannel property.

// Subscribe to camera position changes
val connection = map.camera.positionChannel.connect { position ->
    Log.d("APP", "Camera position has changed (coordinates, zoom level, tilt, or rotation).")
}

// Unsubscribe when it's no longer needed
connection.close()

To display an object or a group of objects on the map, you can use the calcPosition method to calculate camera position:

// to "see" two markers on the map:

// creating a geometry that covers both objects
val geometry = ComplexGeometry(listOf(PointGeometry(point1), PointGeometry(point2)))
// calculating required position
val position = calcPosition(map.camera, geometry)
// using the calculated position
map.camera.move(position)

The example above returns a result similar to:

As you can see, markers are cut in half. This is because the method has information about geometries, not objects. In this example, the marker position is its center. The method has calculated the position to embed marker centers into the active area. The active area is shown as a red rectangular along the screen edges. To display markers in full, you can set the active area.

For example, you can set paddings from the top and bottom of the screen:

val geometry = ComplexGeometry(listOf(PointGeometry(point1), PointGeometry(point2)))

// setting top and bottom paddings so that markers are displayed in full
map.camera.setPadding(Padding(top = 100, bottom = 100))
val position = calcPosition(map.camera, geometry)
map.camera.move(position)

Result:

You can also set specific parameters for position calculation only. For example, you can set paddings only inside the position calculation method and get the same result.

val geometry = ComplexGeometry(listOf(PointGeometry(point1), PointGeometry(point2)))
// setting an active area only for the position calculation
val position = calcPosition(map.camera, geometry, padding = Padding(top = 100, bottom = 100))
map.camera.move(position)

Result:

As you can see, the active area is not changed but markers are fully embedded. However, this approach may case unexpected behavior. The camera position specifies a geographic coordinate that must be in the camera position spot (red circle in the screen center). Parameters like padding, positionPoint, and size impact the location of this spot.

If parameters that shift the camera position spot are passed to a method during the position calculation, using the result may lead to unexpected behavior. For example, if you set an asymmetric active area, the picture can shift greatly.

Example of setting the same position for different paddings:

The easiest solution is to pass all required settings to the camera and use only the camera and geometry to calculate position. If additional parameters that are not passed to the camera are used, you might need to edit the result and shift the picture in the right direction.

Camera has two properties that both describe geometry of a visible area but in different ways. visibleRect has the GeoRect and is always a rectangle. visibleArea is an arbitrary geometry. You can tell the difference easily by examples of different camera tilt angles (relatively to the map):

  • With 45° tilt, visibleRect and visibleArea are not equal: in this case, visibleRect is larger because it must be a rectangle containing visibleArea.

    visibleArea is displayed in blue, visibleRect - in red.

  • With 0° tilt, visibleArea и visibleRect overlap, as you can tell from the color change.

Using the visibleArea property, you can get the map area covered by the camera as Geometry. Using the intersects() method, you can get the intersection of the camera coverage area with the required geometry:

// Telling if a marker falls into the visible map area
// val marker: Marker
val markerGeometry: Geometry = PointGeometry(marker.position)

val intersects: Boolean = map.camera.visibleArea.intersects(markerGeometry)

To allow the SDK access to current location data, it is necessary to create a source for this data. You can use the standard source DefaultLocationSource or create a custom source by implementing the LocationSource interface.

The created source needs to be registered with the SDK. To do this, you need to call the registerPlatformLocationSource method and pass the SDK context and the source to it.

You can add a special marker to the map that will be automatically updated to reflect the current location of the device. To do this, create a MyLocationMapObjectSource data source and add it to the map.


// Create a location data source
val locationSource = DefaultLocationSource(applicationContext)

// Register the location data source with the SDK
registerPlatformLocationSource(sdkContext, locationSource)

// Create a my location map object source
val source = MyLocationMapObjectSource(
    sdkContext,
    MyLocationController(BearingSource.SATELLITE)
)

// Add the my location map object source to the map
map.addSource(source)

You can get information about map objects using pixel coordinates. For this, call the getRenderedObjects() method of the map and specify the pixel coordinates and the radius in screen millimeters. The method will return a deferred result (Future) containing information about all found objects within the specified radius on the visible area of the map (a list of RenderedObjectInfo).

An example of a function that takes tap coordinates and passes them to getRenderedObjects():

override fun onTap(point: ScreenPoint) {
    map.getRenderedObjects(point).onResult { renderedObjects ->
        // First list object is the closest to the coordinates
        val dgisObject = renderedObjects
            .firstOrNull { it.item.source is DgisSource && it.item.item is DgisMapObject }
            ?: return@onResult

        // Save source and directory id.
        val source = dgisObject.item.source as DgisSource
        val id = (dgisObject.item.item as DgisMapObject).id

        searchManager.searchByDirectoryObjectId(id)
	    .onResult onDirectoryObjectReady@ {
	        val obj = it ?: return@onDirectoryObjectReady

		val entrancesIds = obj.buildingEntrances.map { entranceInfo ->
			entranceInfo.id
		} as MutableList<DgisObjectId>
		entrancesIds.add(id)

		source.setHighlighted(source.highlightedObjects, false)
                // Select object and entrances.
		source.setHighlighted(entrancesIds, true)
            }
    }
}

In addition to implementing the TouchEventsObserver interface, you can set the MapObjectTappedCallback callback for tap or long tap in the MapView using the addObjectTappedCallback and addObjectLongTouchCallback methods. This callback will receive RenderedObjectInfo for the object that is closest to the touch point.

...
fun onObjectTappedOrLongTouch(objInfo: RenderedObjectInfo) {
     Log.d("APP", "Some object's data: ${objInfo.item.item.userData}")
}
...

mapView.addObjectTappedCallback(::onObjectTappedOrLongTouch)
mapView.addObjectLongTouchCallback(::onObjectTappedOrLongTouch)

You can place native views on the map anchored to a specific position. Use SnapToMapLayout that contains a custom implementation of the LayoutParams class that allows you to set a view position on the map.

Add SnapToMapLayout to the layout:

<ru.dgis.sdk.map.MapView>
    ...
     <ru.dgis.sdk.map.SnapToMapLayout
            android:id="@+id/snapToMapLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
</ru.dgis.sdk.map.MapView>
...

To anchor a view to a position on the map, add this view to SnapToMapLayout. You need to build LayoutParams with a geographic coordinate GeoPointWithElevation and other parameters:

val params = SnapToMapLayout.LayoutParams(
    // Width, use WRAP_CONTENT
    width = ViewGroup.LayoutParams.WRAP_CONTENT,
    // Height, use WRAP_CONTENT
    height = ViewGroup.LayoutParams.WRAP_CONTENT,
    // Point on the map to which the view is anchored
    position = GeoPointWithElevation(55.7, 37.6),
    // Point on the view to which the anchoring is done
    // In this case, the upper-left corner of the view
    anchor = Anchor(0.0f, 0.0f),
    // Offset in in x- and y-axes relative to the upper and left borders respectively
    offsetX = -15 * context.resources.displayMetrics.density,
    offsetY = -15 * context.resources.displayMetrics.density
)

These parameters must be passed, for example, during the call to addView().

You can customize working wth gestures in one of the following ways:

  • Configure existing gestures.
  • Implement your own mechanism of gesture recognition.

You can control the map using the following standard gestures:

  • shift the map in any direction with one finger
  • shift the map in any direction with multiple fingers
  • rotate the map with two fingers
  • scale the map with two fingers (pinch)
  • zoom the map in by double-tapping
  • zoom the map out with two fingers
  • scale the map with the tap-tap-swipe gesture sequence using one finger
  • tilt the map by swiping up or down with two fingers

By default, all gestures are enabled. If needed, you can disable selected gestures using the GestureManager.

You can get the GestureManager directly from the MapView.

mapView.getMapAsync {
    gestureManager = mapView.gestureManager!!
}

To activate gestures, use the enableGesture method.

To deactivate gestures, use the disableGesture method.

To check whether a gesture is enabled or not, use the gestureEnabled method.

To change the settings or get information about multiple gestures at once, use the enabledGestures property.

A specific gesture is set using Gesture properties. The SCALING property is responsible for the whole group of map scaling gestures. You cannot disable these gestures one by one.

// example of disabling scaling the map with one finger
gestureManager.disableGesture(Gesture.SHIFT)
Log.d("GestureExample", "${gestureManager.enabledGestures}") // result 'D/GestureExample: [SCALING, ROTATION, MULTI_TOUCH_SHIFT, TILT]'

Some gestures have specific lists of settings:

For more information about settings, see the documentation. Objects of these settings are accessible via GestureManager properties.

You can also configure the behavior of map scaling and rotation. You can set the point relatively to which map rotation and scaling will be done. By default, these operations are done relatively to the "center of mass" of the finger placement points. You can change this behavior using EventsProcessingSettings. To implement the setting, use the setSettingsAboutMapPositionPoint.

To control simultaneous activation of multiple gestures, use the setMutuallyExclusiveGestures method.

You can replace the standard mechanism of gesture processing with a custom one. To do it, pass the implementation of the MapGestureRecognitionEngine interface to the useCustomGestureRecognitionEngine method of MapView.

The interface has four methods:

  • resetRecognitionState: when called, resets the engine state to initial. Called when some events (for example, camera position change) finish working with map control events.
  • onDevicePpiChanged: notifies about PPI change. The value can be used to convert the distance from screen points to millimeters.
  • setMapEventSender: called to provide the engine with an object through which generated map control events will be sent.
  • processMotionEvent called to provide the engine with screen touch points in the standard format.
// Simplified implementation of a gesture recognizer, which can control map shift only.
class ExampleGestureRecognitionEngine : MapGestureRecognitionEngine {
    private var mapEventSender: MapEventSender? = null
    private var oldTouchPoint: ScreenPoint? = null
    private var origin: ScreenPoint? = null
    private fun findTouchPoint(event: MotionEvent): ScreenPoint? =
        if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
            // Cancelling a gesture
            null
        } else if (event.actionMasked == MotionEvent.ACTION_UP && event.pointerCount == 1) {
            // The last finger is risen
            null
        } else if (event.pointerCount <= 0) {
            // No touch registered
            null
        } else {
            ScreenPoint(event.getX(0), event.getY(0))
        }
    override fun processMotionEvent(event: MotionEvent) : Boolean{
        val newTouchPoint = findTouchPoint(event)
        if (newTouchPoint != null) {
            if (oldTouchPoint != null) {
                mapEventSender!!.sendEvent(
                    DirectMapShiftEvent(
                        ScreenShift(
                            newTouchPoint.x - oldTouchPoint!!.x,
                            newTouchPoint.y - oldTouchPoint!!.y
                        ),
                        origin!!,
                        Duration.now()
                    )
                )
            } else {
                origin = newTouchPoint
                mapEventSender!!.sendEvent(DirectMapControlBeginEvent())
            }
        } else {
            origin = null
            mapEventSender!!.sendEvent(DirectMapControlEndEvent(Duration.now()))
        }
        oldTouchPoint = newTouchPoint
        return true
    }
    override fun resetRecognitionState() {
        origin = null
        oldTouchPoint = null
    }
    override fun onDevicePpiChanged(devicePpi: DevicePpi) {}
    override fun setMapEventSender(mapEventSender: MapEventSender) {
        this.mapEventSender = mapEventSender
    }
    override fun close() {}
}

The engine task is to convert screen touches to map control events. The engine works with direct map control events only. All such events are represented by six classes with names starting with DirectMap:

You can get more information about an event from its description. Remember that to work with direct map control events, you need to use signal events for the beginning and end of an event sequence (even if only one event is generated).