Карта | Mobile SDK | 2GIS Documentation
iOS SDK

Карта

Чтобы создать карту, нужно вызвать метод makeMapFactory() и передать настройки карты в виде структуры MapOptions.

В настройках важно указать корректное для устройства значение PPI. Его можно найти в спецификации устройства. По умолчанию выставлено значение DevicePpi.autodetected.

Кроме этого в настройках можно указать начальную позицию камеры, границы масштабирования и другие параметры.

// Настройки карты.
var mapOptions = MapOptions.default

// Значение PPI для устройства.
// По умолчанию, mapOptions.devicePPI == DevicePpi.autodetected.
mapOptions.devicePPI = devicePPI

// Создание фабрики объектов карты.
let mapFactory: DGis.IMapFactory = try sdk.makeMapFactory(options: mapOptions)

Получить слой карты можно через свойство mapView. Контроллер карты доступен через свойство map (см. класс Map).

// Слой карты.
let mapView: UIView & IMapView = mapFactory.mapView

// Контроллер карты.
let map = mapFactory.map

Пример UIViewController для отображения карты:

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)

			/// Стартовую позицию камеры можно рассчитывать только после загрузки карты.
			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)")
		}
	}
}

Важное замечание: объект DGis.Container должен создаваться в одном экземпляре и храниться на уровне UIViewController.

Для добавления динамических объектов на карту (маркеров, линий, кругов, многоугольников) нужно создать менеджер объектов (MapObjectManager), указав инстанс карты.

// Сохраняем объект в свойство, так как при удалении менеджера исчезают все связанные с ним объекты на карте.
self.objectManager = MapObjectManager(map: map)

Для добавления объектов используются методы addObject() и addObjects(). Для каждого динамического объекта можно указать поле userData, которое будет хранить произвольные данные, связанные с объектом. Настройки объектов можно менять после их создания.

Для удаления объектов используются методы removeObject() и removeObjects(). Чтобы удалить все объекты, можно использовать метод removeAll().

Чтобы добавить маркер на карту, нужно создать объект Marker, указав нужные настройки (MarkerOptions), и передать его в вызов addObject() менеджера объектов.

Иконку для маркера можно создать с помощью метода make() фабрики изображений (IImageFactory), используя UIImage, PNG-данные или SVG-разметку.

// Иконка на основе UIImage.
let uiImage = UIImage(systemName: "umbrella.fill")!.withTintColor(.systemRed)
let icon = sdk.imageFactory.make(image: uiImage)

// Иконка на основе SVG-данных.
let icon = sdk.imageFactory.make(svgData: imageData, size: imageSize)

// Иконка на основе PNG-данных (быстрее, чем из UIImage).
let icon = sdk.imageFactory.make(pngData: imageData, size: imageSize)

// Настройки маркера.
let options = MarkerOptions(
	position: GeoPointWithElevation(
		latitude: 55.752425,
		longitude: 37.613983
	),
	icon: icon
)

// Создание и добавление маркера.
let marker = Marker(options: options)
objectManager.addObject(object: marker)

Чтобы изменить точку привязки иконки (выравнивание иконки относительно координат на карте), нужно указать параметр anchor.

Чтобы нарисовать на карте линию, нужно создать объект Polyline, указав нужные настройки, и передать его в вызов addObject() менеджера объектов.

Кроме массива координат для точек линии, в настройках можно указать ширину линии, цвет, пунктир, обводку и другие параметры (см. PolylineOptions).

// Координаты вершин ломаной линии.
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)
]

// Настройки линии.
let options = PolylineOptions(
	points: points,
	width: LogicalPixel(value: 2),
	color: DGis.Color.init()
)

// Создание и добавление линии.
let polyline = Polyline(options: options)
objectManager.addObject(object: polyline)

Чтобы нарисовать на карте многоугольник, нужно создать объект Polygon, указав нужные настройки, и передать его в вызов addObject() менеджера объектов.

Координаты для многоугольника указываются в виде двумерного массива. Первый вложенный массив должен содержать координаты основных вершин многоугольника. Остальные вложенные массивы не обязательны и могут быть заданы для того, чтобы создать вырез внутри многоугольника (один дополнительный массив - один вырез в виде многоугольника).

Дополнительно можно указать цвет полигона и параметры обводки (см. PolygonOptions).

// Настройки многоугольника.
let options = PolygonOptions(
	contours: [
		// Вершины многоугольника.
		[
			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)
		],
		// Координаты выреза внутри многоугольника.
		[
			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)
)

// Создание и добавление многоугольника.
let polygon = Polygon(options: options)
objectManager.addObject(object: polygon)

Если на карту необходимо добавить коллекцию объектов, то добавление через метод addObject в цикле по всей коллекции приведет к потере производительности. Для добавления коллекции объектов нужно сначала подготовить всю коллекцию и добавить её через метод addObjects:

// подготавливаем коллекцию объектов
let options = [MarkerOptions(<params>), MarkerOptions(<params>)]
var markers: [SimpleMapObject] = []
options.forEach{ option in
	markers.append(Marker(options: option))
}

// добавляем коллекцию объектов на карту
mapObjectManager.addObjects(objects: markers)

Для добавления маркеров на карту в режиме кластеризации нужно создать менеджер объектов (MapObjectManager) через MapObjectManager.withClustering(), указав инстанс карты, расстояние между кластерами в логических пикселях, максимальный zoom-уровень формирования кластеров и пользовательскую имплементацию протокола SimpleClusterRenderer. SimpleClusterRenderer используется для кастомизации кластеров в 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)
)

Для того, чтобы объекты на карте визуально реагировали на их выделение, в стилях необходимо настроить разный внешний вид слоя с помощью функции "Добавить зависимость от состояния" для всех необходимых свойств (иконка, шрифт, цвет и т. д.):

Настройки свойства выделенного объекта находятся в табе "Выделенное состояние":

Для начала получим информацию об объектах, попадающих в область нажатия, с помощью метода getRenderedObjects(), как в примере Получение объектов по экранным координатам

Выделение объектов происходит с помощью вызова метода setHighlighted(), который получает на вход список идентификаторов справочника изменяемых объектов DgisObjectId. Внутри метода getRenderedObjects() мы можем получить все данные, необходимые для использования этого метода, как то источник данных объектов и их идентификаторы:

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
			// Получим ближайший к месту нажатия объект внутри установленного радиуса
			guard let info = infos.first(
				where: {
					$0.item.source is DgisSource
					&& $0.item.item is DgisMapObject
				}
			) else { return }

			// Сохраним источник данных объекта и его идентификатор
			let source = info.item.source as! DgisSource
			let id = (info.item.item as! DgisMapObject).id

			// Выделяем объект и входы в него.
			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: { ... }
	)
	...
}

Для работы с камерой используется объект Camera, доступный через свойство map.camera.

Чтобы запустить анимацию перелёта камеры, нужно вызвать метод move() и указать параметры перелёта:

  • position - конечная позиция камеры (координаты и уровень приближения). Дополнительно можно указать наклон и поворот камеры (см. CameraPosition).
  • time - продолжительность перелёта в секундах (TimeInterval).
  • animationType - тип анимации (CameraAnimationType).

Функция move() возвращает объект Future, который можно использовать, чтобы обработать событие завершения перелета.

// Новая позиция камеры.
let newCameraPosition = CameraPosition(
	point: GeoPoint(latitude: 55.752425, longitude: 37.613983),
	zoom: Zoom(value: 16)
)

// Запуск перелёта.
let future = map.camera.move(
	position: newCameraPosition,
	time: 0.4,
	animationType: .linear
)

// Получение события завершения перелета.
let cancellable = future.sink { _ in
	print("Перелет камеры завершён.")
} failure: { error in
	print("Возникла ошибка: \(error.localizedDescription)")
}

Текущее состояние камеры (находится ли камера в полёте) можно получить, используя свойство state. См. CameraState для списка возможных состояний камеры.

let currentState = map.camera.state

Подписаться на изменения состояния камеры можно, используя stateChannel.sink.

// Подписка.
let connection = map.camera.stateChannel.sink { state in
	print("Состояние камеры изменилось на \(state)")
}

// Отписка.
connection.cancel()

Текущую позицию камеры можно получить, используя свойство position (см. структуру CameraPosition).

let currentPosition = map.camera.position
print("Координаты: \(currentPosition.point)")
print("Приближение: \(currentPosition.zoom)")
print("Наклон: \(currentPosition.tilt)")
print("Поворот: \(currentPosition.bearing)")

Подписаться на изменения позиции камеры (и угла наклона/поворота) можно, используя positionChannel.sink.

// Подписка.
let connection = map.camera.positionChannel.sink { position in
	print("Изменилась позиция камеры или угол наклона/поворота.")
}

// Отписка.
connection.cancel()

Чтобы отобразить на экране какой-то объект или группу объектов, можно использовать метод расчёта позиции камеры calcPosition:

// хотим "увидеть" два маркера на карте.

// создаем геометрию, которая охватывает оба объекта
let geometry = ComplexGeometry(geometries: [PointGeometry(point: point1), PointGeometry(point: point2)])
// рассчитываем нужную позицию
let position = calcPosition(camera: map.camera, geometry: geometry)
// используем рассчитанную позицию
map.camera.move(position: position)

Пример выше выдаст результат похожий на такой:

Маркеры обрезаются наполовину. Это происходит из-за того, что метод ничего не знает об объектах, а только о геометриях. В данном примере позиция маркера - это его центр. Метод рассчитал позицию так, чтобы вписать центры маркеров в активную область. Активная область отображается в виде красного прямоугольника по краям экрана. Чтобы отобразить маркеры целиком, можно воспользоваться заданием активной области.

Например, можно задать отступы сверху и снизу экрана:

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

// задаем отступы сверху и снизу чтобы маркер отображался полностью
map.camera.setPadding(padding: Padding(left: 100, right: 100))
let position = calcPosition(camera: map.camera, geometry: geometry)
map.camera.move(position: position)

В результате получим:

Помимо задания настроек в камеру, можно задавать определенные параметры только для расчета позиции. Например, указанные отступы можно задать только в методе расчета позиции и получить такой же результат.

let geometry = ComplexGeometry(geometries: [PointGeometry(point: point1), PointGeometry(point: point2)])
// задаем активную область только для расчета позиции
let position = calcPosition(camera: map.camera, geometry: geometry, padding: Padding(left: 100, right: 100))
map.camera.move(position: position)

В результате получим:

На изображении видно, что активная область не изменилась, но маркеры вписаны полностью, но такой подход может приводить к неожиданному поведению. Дело в том, что позиция камеры задаёт геокоординату, которая должна быть в точке позиции камеры (красный кружок в центре экрана). Такие настройки как padding, positionPoint и size влияют на положение данной точки.

Если при вычислении позиции в метод будут переданы параметры, которые сместят точку позиции камеры, то использование результата приведет к неожиданностям. Например, если вы задаёте ассиметричную активную область, то картинка может сильно смещаться.

Пример установки одной и той же позиции для разных отступов:

Проще всего задавать все необходимые настройки в камеру и использовать для расчёта позиции только камеру и геометрию. В случае, если используются дополнительные параметры, которые не устанавливаются в камеру, может потребоваться коррекция результата, чтобы сдвинуть картинку в нужную сторону.

У камеры есть два свойства, которые описывают геометрию видимой области, но делают это по-разному. visibleRect имеет тип GeoRect и всегда является прямоугольником. visibleArea представляет из себя произвольную геометрию. Проще всего увидеть на примере с разными углами наклона камеры относительно карты:

  • При наклоне 45° visibleRect и visibleArea не будут равны: visibleRect, в данном случае, будет больше, т.к. он должен быть прямоугольником и должен содержать в себе visibleArea Синим цветом изображена visibleArea, красным – visibleRect
  • При наклоне 0° visibleArea и visibleRect будут совпадать, как видно из изменения цвета.

С помощью свойства visibleArea мы можем получить область карты, которая попадает в камеру, в виде Geometry. С помощью метода intersects() мы можем получить пересечение видимой области камеры с нужной нам геометрией:

//Допустим, у нас есть маркер и нужно узнать, попадает ли он в видимую область карты
// let marker: Marker
let markerGeometry: Geometry = PointGeometry(point: marker.position)

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

На карту можно добавить специальный маркер, который будет отражать текущее местоположение устройства. Для этого нужно создать источник данных MyLocationMapObjectSource() и указав контейнер объектов SDK (sdk.context). Созданный источник нужно передать в метод карты addSource().

// Создание источника данных.
let source = MyLocationMapObjectSource(
	context: sdk.context,
	controller: MyLocationController(bearingSource: .satellite)
)

// Добавление маркера на карту.
map.addSource(source: source)

Чтобы удалить маркер, нужно вызвать метод removeSource(). Список активных источников данных можно получить, используя свойство map.sources.

map.removeSource(source)

Для отображения слоя пробок необходимо создать TrafficSource и передать его в метод карты addSource().

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

Информацию об объектах на карте можно получить, используя пиксельные координаты. Для этого нужно вызвать метод карты getRenderedObjects(), указав координаты в пикселях и радиус в экранных миллиметрах. Метод вернет отложенный результат, содержащий информацию обо всех найденных объектах в указанном радиусе на видимой области карты (массив RenderedObjectInfo).

Пример функции, которая принимает координаты нажатия на экран и передает их в метод 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 = map.getRenderedObjects(centerPoint: point).sink(
		receiveValue: {
			infos in
			// Первый объект в массиве - самый близкий к координатам.
			guard let info = infos.first else { return }
			// Обработка результата в главной очереди.
			DispatchQueue.main.async {
				[weak self] in
				self?.handle(selectedObject: info)
			}
		},
		failure: { error in
			print("Ошибка получения информации об объектах: \(error)")
		}
	)
	// Сохраняем результат вызова, так как его удаление отменяет обработку.
	self.getRenderedObjectsCancellable = cancel
}

Также можно установить коллбэк MapObjectTappedCallback для тапа или лонгтапа в IMapView с помощью методов addObjectTappedCallback и addObjectLongPressCallback. В этот коллбэк придет RenderedObjectInfo для объекта, который находится ближе всего к точке касания.

...
let mapObjectTappedOrLongPress = MapObjectTappedCallback(callback: { [weak self] objectInfo in
	print("Произвольные данные объекта: \(objectInfo.item.item.userData)")
})
...

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

Для кастомизации распознавателя жестов карты, необходимо задать реализацию протокола IMapGestureView в IMapView или реализацию IMapGestureViewFactory в MapOptions. Если ни одна из этих имплементаций задана не будет, то будет использована реализация по умолчанию. Пример такой кастомизации распознавателя можно посмотреть здесь.

IMarkerViewFactory – фабрика для создания View, привязанных к геокоординатам, находится в объекте Container. Попробуем создать IMarkerView с помощью этой фабрики:

let sdk: Container
// Любой View, для примера возьмем UILabel
let view: UILabel
// Позиция на карте, к которой прикрепится view
let position: GeoPointWithElevation
// Точка внутри view, к которой будет привязана координата position
let anchor: Anchor = Anchor()
// Смещение в пикселях по осям
let offsetX: CGFloat = 0.0
let offsetY: CGFloat = 0.0


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

Для отображения полученного View, необходимо добавить его методом add() объекта IMarkerViewOverlay, полученного из фабрики IMapFactory.