Map | Mobile SDK | Urbi Documentation
iOS SDK

Map

To create a map, call the makeMapFactory() method and specify the required map settings as a MapOptions structure.

It is important to specify the correct PPI settings for the device. You can find them in the technical specification of the device. By default PPI settings are DevicePpi.autodetected.

You can also specify the initial camera position, zoom limits, and other settings.

// Map settings object.
var mapOptions = MapOptions.default

// PPI settings.
// By default, mapOptions.devicePPI == DevicePpi.autodetected.
mapOptions.devicePPI = devicePPI

// Create a map factory.
let mapFactory: DGis.IMapFactory = try sdk.makeMapFactory(options: mapOptions)

To get the view of the map, use the mapView property. To get the control of the map, use the map property.

// Map view.
let mapView: UIView & IMapView = mapFactory.mapView

// Map control.
let map = mapFactory.map

Example UIViewController for displaying a map:

import Foundation
import DGis
import UIKit

class MapViewController: UIViewController {
    private lazy var sdk = DGis.Container()
    private var dataLoadingStateCancellable: ICancellable = NoopCancellable()

    override func viewDidLoad() {
        do {
            var mapOptions = MapOptions.default
            mapOptions.devicePPI = .autodetected
            let mapFactory = try sdk.makeMapFactory(options: mapOptions)
            let mapView: UIView & IMapView = mapFactory.mapView
            mapView.frame = self.view.bounds
            self.view.addSubview(mapView)

            /// You can calculate the camera position only after loading the map.
            self.dataLoadingStateCancellable = mapFactory.map.dataLoadingStateChannel.sink { loadingState in
                if loadingState == .loaded {
                    print("Now map is loaded")
                }
            }
        } catch let error as SDKError {
            print(error.description)
        } catch {
            print("System error: \(error)")
        }
    }
}

Important note: the DGis.Container object must be instantiated and stored at the UIViewController level.

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 to a property.

self.objectsManager = MapObjectManager(map: 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.

MapObjectManager is an object container. As long as objects must be presented on the map, MapObjectManager must be stored on the class level.

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

You can create an icon for the marker by calling the make() method of the IImageFactory and using UIImage, PNG data, or SVG markup as input.

// UIImage
let uiImage = UIImage(systemName: "umbrella.fill")!.withTintColor(.systemRed)
let icon = sdk.imageFactory.make(image: uiImage)

// SVG markup.
let icon = sdk.imageFactory.make(svgData: imageData, size: imageSize)

// PNG data (faster than UIImage).
let icon = sdk.imageFactory.make(pngData: imageData, size: imageSize)

// Marker settings.
let options = MarkerOptions(
    position: GeoPointWithElevation(
        latitude: 55.752425,
        longitude: 37.613983
    ),
    icon: icon
)

// Create and add the marker to the map.
let marker = try Marker(options: options)
objectManager.addObject(object: marker)

To update settings of an already created marker, set new values to the Marker object parameters: see the full list of available parameters in the Marker description.

// Changing marker coordinates
marker.position = GeoPointWithElevation(latitude: 59.93428, longitude: 30.33510)

// Changing an icon
let uiImage = UIImage(systemName: "bubble.right.fill")!.withTintColor(.systemGreen)
let newIcon = sdk.imageFactory.make(image: uiImage)
marker.icon = newIcon

// Changing the icon anchor point
marker.anchor = Anchor(x: 0.5, y: 0.5)

// Changing the icon opacity
marker.iconOpacity = Opacity(value: 1.0)

// Changing the marker label
marker.text = "New text"

// Changing the label style
marker.textStyle = TextStyle(
    fontSize: LogicalPixel(value: 8),
    color: Color(argb: 4294967295),
    strokeWidth: LogicalPixel(value: 0.3499999940395355),
    strokeColor: Color(argb: 4294967295),
    textPlacement: TextPlacement.bottomCenter,
    textOffset: LogicalPixel(value: 0),
    fontName: nil
)

// Changing the marker draggability flag
marker.isDraggable = true

// Changing the target marker width
marker.iconWidth = LogicalPixel(value: 2.0)

// Changing the marker rotation angle relative to the north direction
marker.iconMapDirection = MapDirection(value: 10)

// Changing the flag of animating the marker appearance
marker.animatedAppearance = true

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

// Coordinates of the vertices of the polyline.
let points = [
    GeoPoint(latitude: 55.7513, longitude: value: 37.6236),
    GeoPoint(latitude: 55.7405, longitude: value: 37.6235),
    GeoPoint(latitude: 55.7439, longitude: value: 37.6506)
]

// Line settings.
let options = PolylineOptions(
    points: points,
    width: LogicalPixel(value: 2),
    color: DGis.Color.init()
)

// Create and add the line to the map.
let polyline = try Polyline(options: options)
objectManager.addObject(object: polyline)

To update settings of an already created line, set new values to the Polyline object parameters: see the full list of available parameters in the Polyline description.

// Changing the coordinates of the line vertices
polyline.points = [
    GeoPoint(latitude: 55.7513, longitude: value: 37.6236),
    GeoPoint(latitude: 55.7405, longitude: value: 37.6235),
    GeoPoint(latitude: 55.7439, longitude: value: 37.6506)
]

// Changing the line width
polyline.width = LogicalPixel(value: 3)

// Changing the line color
polyline.color = Color(argb: 4294967295)

// Changing the erased part
polyline.erasedPart = 0.8

// Changing the parameters of a dashed polyline
polyline.dashedPolylineOptions = DashedPolylineOptions(
    dashLength: LogicalPixel(value: 5),
    dashSpaceLength: LogicalPixel(value: 2)
)

// Changing the parameters of a gradient polyline
polyline.gradientPolylineOptions = GradientPolylineOptions(
    borderWidth: LogicalPixel(value: 0),
    secondBorderWidth: LogicalPixel(value: 0),
    gradientLength: LogicalPixel(value: 1),
    borderColor: Color(argb: 4294967295),
    secondBorderColor: Color(argb: 4294967295),
    colors: [Color],
    colorIndices: Data
)

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

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

// Polygon settings.
let options = PolygonOptions(
    contours: [
        // Vertices of the polygon.
        [
            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.
        [
            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)
        ]
    ],
    color: DGis.Color.init(),
    strokeWidth: LogicalPixel(value: 2)
)

// Create and add the polygon to the map.
let polygon = try Polygon(options: options)
objectManager.addObject(object: polygon)

To update settings of an already created polygon, set new values to the Polygon object parameters: see the full list of available parameters in the Polygon description.

// Changing the coordinates of the polygon vertices
polygon.contours = [
    GeoPoint(latitude: 55.72014932919687, longitude: 37.562599182128906),
    GeoPoint(latitude: 55.72014932919687, longitude: 37.67555236816406),
    GeoPoint(latitude: 55.72014932919687, longitude: 37.562599182128906)
]

// Changing the polygon fill color
polygon.color = Color(argb: 4294967295)

// Changing the polygon stroke width
polygon.strokeWidth = LogicalPixel(value: 2)

// Changing the polygon stroke color
polygon.strokeColor = Color(argb: 4294967295)

To draw a circle on the map, create a Circle object, specifying the required options in CircleOptions, and pass it to the addObject() method of the object manager.

// Configuring circle parameters
let options = CircleOptions(
    position: GeoPoint(latitude: 55.72014932919687, longitude: 37.562599182128906),
    radius: Meter(value: 10)
)

// Creating and adding a circle to the map
let circle = try Circle(options: options)
objectManager.addObject(object: circle)

To update settings of an already created polygon, set new values to the Circle object parameters: see the full list of available parameters in the Circle description.

// Changing the coordinated of a circle center
circle.position = GeoPoint(latitude: 55.74460317215391, longitude: 37.63435363769531)

// Changing the circle radius
circle.radius = Meter(value: 1)

// Changing the circle fill color
circle.color = Color(argb: 4294967295)

// Changing the circle stroke width
circle.strokeWidth = LogicalPixel(value: 2)

// Changing the circle stroke color
circle.strokeColor = Color(argb: 4294967295)

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
let options = [MarkerOptions(<params>), MarkerOptions(<params>)]
var markers: [SimpleMapObject] = []
options.forEach{ option in
    markers.append(Marker(options: option))
}

// adding the collection to the map
mapObjectManager.addObjects(objects: 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.

final class SimpleClusterRendererImpl: SimpleClusterRenderer {
    private let image: DGis.Image
    private var idx = 0

    init(
        image: DGis.Image
    ) {
        self.image = image
    }

    func renderCluster(cluster: SimpleClusterObject) -> SimpleClusterOptions {
        let textStyle = TextStyle(
            fontSize: LogicalPixel(15.0),
            textPlacement: TextPlacement.rightTop
        )
        let objectCount = cluster.objectCount
        let iconMapDirection = objectCount < 5 ? MapDirection(value: 45.0) : nil
        idx += 1
        return SimpleClusterOptions(
            icon: self.image,
            iconMapDirection: iconMapDirection,
            text: String(objectCount),
            textStyle: textStyle,
            iconWidth: LogicalPixel(30.0),
            userData: idx,
            zIndex: ZIndex(value: 6),
            animatedAppearance: false
        )
    }
}

self.objectManager = MapObjectManager.withClustering(
    map: map,
    logicalPixel: LogicalPixel(80.0),
    maxZoom: Zoom(19.0),
    clusterRenderer: SimpleClusterRendererImpl(image: self.icon)
)

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:

private func tap(point: ScreenPoint, tapRadius: ScreenDistance) {
    let scale = UIScreen.main.nativeScale
    let point = ScreenPoint(x: Float(location.x * scale), y: Float(location.y * scale))
    self.getRenderedObjectsCancellable?.cancel()
    let cancel = self.map.getRenderedObjects(centerPoint: point, radius: tapRadius).sink(
        receiveValue: {
            infos in
            // Getting the closest object to the tap spot inside the specified radius
            guard let info = infos.first(
                where: {
                    $0.item.source is DgisSource
                    && $0.item.item is DgisMapObject
                }
            ) else { return }

            // Saving the object data source and the object ID
            let source = info.item.source as! DgisSource
            let id = (info.item.item as! DgisMapObject).id

            // Highlighting the required object and its entrances
            let future = searchManager.searchByDirectoryObjectId(objectId: id)

            self.getDirectoryObjectCancellable = future.sinkOnMainThread(
                receiveValue: {
                    [weak self] directoryObject in
                    guard let self = self else { return }
                    guard let directoryObject = directoryObject else { return }
                    guard let objectId = directoryObject.id else { return }

                    var selectedObjectIds = [objectId]
                    directoryObject.entrances.forEach { entrance in
                        selectedObjectIds.append(entrance.id)
                    }

                    source.setHighlighted(directoryObjectIds: selectedObjectIds, highlighted: true)
                },
                failure: { ... }
            )
        },
        failure: { ... }
    )
    ...
}

You can control the camera by accessing the map.camera property. See the Camera class 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 TimeInterval).
  • animationType - type of animation to use (CameraAnimationType).

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

// New position for camera.
let newCameraPosition = CameraPosition(
    point: GeoPoint(latitude: 55.752425, longitude: 37.613983),
    zoom: Zoom(value: 16)
)

// Start the flight animation.
let future = map.camera.move(
    position: newCameraPosition,
    time: 0.4,
    animationType: .linear
)

// Handle the animation finish event.
let cancellable = future.sink { _ in
    print("Camera flight finished.")
} failure: { error in
    print("An error occurred: \(error.localizedDescription)")
}

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.

let currentState = map.camera.state

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

// Subscribe to camera state changes.
let connection = map.camera.stateChannel.sink { state in
    print("Camera state has changed to \(state)")
}

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

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

let currentPosition = map.camera.position
print("Coordinates: \(currentPosition.point)")
print("Zoom level: \(currentPosition.zoom)")
print("Tilt: \(currentPosition.tilt)")
print("Rotation: \(currentPosition.bearing)")

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

// Subscribe to camera position changes.
let connection = map.camera.positionChannel.sink { position in
    print("Camera position has changed (coordinates, zoom level, tilt, or rotation).")
}

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

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
let geometry = ComplexGeometry(geometries: [PointGeometry(point: point1), PointGeometry(point: point2)])
// calculating required position
let position = calcPosition(camera: map.camera, geometry: geometry)
// using the calculated position
map.camera.move(position: 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:

let geometry = ComplexGeometry(geometries: [PointGeometry(point: point1), PointGeometry(point: point2)])

// setting top and bottom paddings so that markers are displayed in full
map.camera.setPadding(padding: Padding(left: 100, right: 100))
let position = calcPosition(camera: map.camera, geometry: geometry)
map.camera.move(position: 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.

let geometry = ComplexGeometry(geometries: [PointGeometry(point: point1), PointGeometry(point: point2)])
// setting an active area only for the position calculation
let position = calcPosition(camera: map.camera, geometry: geometry, padding: Padding(left: 100, right: 100))
map.camera.move(position: 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
// let marker: Marker
let markerGeometry: Geometry = PointGeometry(point: marker.position)

let intersects: Bool = map.camera.visibleArea.intersects(geometry: markerGeometry)

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 data source MyLocationMapObjectSource() function and pass it to the addSource() method of the map.

// Create a data source.
let source = try MyLocationMapObjectSource(context: sdk.context)

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

To remove the marker, call the removeSource() method. You can get the list of active data sources by using the map.sources property.

map.removeSource(source)

To display the traffic jams layer, create a TrafficSource and pass it to the addSource() method of the map.

let trafficSource = TrafficSource(context: sdk.context)
map.addSource(source: trafficSource)

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 (an array of RenderedObjectInfo).

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

private func tap(location: CGPoint) {
    let scale = UIScreen.main.nativeScale
    let point = ScreenPoint(x: Float(location.x * scale), y: Float(location.y * scale))
    self.getRenderedObjectsCancellable?.cancel()
    let cancel = self.map.getRenderedObjects(centerPoint: point).sink(
        receiveValue: {
            infos in
            // First array object is the closest to the coordinates.
            guard let info = infos.first(
                where: {
                    $0.item.source is DgisSource
                    && $0.item.item is DgisMapObject
                }
            ) else { return }

            // Process the result in the main thread.
            let source = info.item.source as! DgisSource
            let id = (info.item.item as! DgisMapObject).id

            // Select object and entrances.
            let future = searchManager.searchByDirectoryObjectId(objectId: id)

            self.getDirectoryObjectCancellable = future.sinkOnMainThread(
                receiveValue: {
                    [weak self] directoryObject in
                    guard let self = self else { return }
                    guard let directoryObject = directoryObject else { return }
                    guard let objectId = directoryObject.id else { return }

                    var selectedObjectIds = [objectId]
                    directoryObject.buildingEntrances.forEach { entrance in
                        selectedObjectIds.append(entrance.id)
                    }

                    source.setHighlighted(directoryObjectIds: selectedObjectIds, highlighted: true)
                },
                failure: { ... }
            )
        },
        failure: { error in
            print("Error retrieving information: \(error)")
        }
    )
    // Save the result to a property to prevent garbage collection.
    self.getRenderedObjectsCancellable = cancel
}

You can also set the MapObjectTappedCallback callback for tap or long tap in the IMapView using the addObjectTappedCallback and addObjectLongPressCallback methods. This callback will receive RenderedObjectInfo for the object that is closest to the touch point.

...
let mapObjectTappedOrLongPress = MapObjectTappedCallback(callback: { [weak self] objectInfo in
    print("Some object's data: \(objectInfo.item.item.userData)")
})
...

self.mapView.addObjectTappedCallback(callback: mapObjectTappedOrLongPress)
self.mapView.addObjectLongPressCallback(callback: mapObjectTappedOrLongPress)

To customize the map gesture recognizer, you need to set the IMapGestureView implementation in IMapView or IMapGestureViewFactory implementation in MapOptions. If no implementations are specified, the default implementations will be used. An example of such recognizer is available here.

IMarkerViewFactory is a factory for creating a View anchored to geographic coordinates. You can find the fectory in the Container object. Create the IMarkerView using this factory:

let sdk: Container
// Any View (for example, UILabel)
let view: UILabel
// Position on the map to attach the view
let position: GeoPointWithElevation
// Point inside the view to which the position coordinate will be anchored
let anchor: Anchor = Anchor()
// Offset in pixels along the axes
let offsetX: CGFloat = 0.0
let offsetY: CGFloat = 0.0


sdk.markerViewFactory.make(
    view: view,
    position : position,
    anchor: anchor,
    offsetX: offsetX,
    offsetY: offsetY
)

To display the created View, add it using the add() method of the IMarkerViewOverlay object obtained from the IMapFactory.