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

Карта

Чтобы создать карту, добавьте MapWidget в ваше дерево:

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

class SimpleMapScreen extends StatelessWidget {
  const SimpleMapScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final sdkContext = sdk.DGis.initialize();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: sdk.MapWidget(
        sdkContext: sdkContext,
        mapOptions: sdk.MapOptions(),
      ),
    );
  }
}

Основным объектом для управления картой является MapWidgetController. Внутри MapWidget он будет приватным, поэтому для получения доступа к нему рекомендуется создать его вручную и передать в MapWidget параметром:

class SimpleMapScreen extends StatelessWidget {
  const SimpleMapScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final sdk.Context sdkContext = sdk.DGis.initialize();
    final sdk.MapWidgetController mapWidgetController = sdk.MapWidgetController();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: sdk.MapWidget(
        sdkContext: sdkContext,
        mapOptions: sdk.MapOptions(),
        controller: mapWidgetController,
      ),
    );
  }
}

Объект карты (Map) можно получить, вызвав метод getMapAsync(). Этот метод работает асинхронно, поэтому его безопасно вызывать в любой момент жизненного цикла, начиная с initState().

Другие действия с картой можно выполнить внутри метода getMapAsync(), т. к. это первый момент, когда карта становится доступна. Все операции с картой, которые описаны в этом разделе далее, подразумевают, что карта уже проинициализирована, а все вызовы происходят внутри метода.

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

sdk.Map? sdkMap = null;

@override
initState() {
  super.initState();
  mapWidgetController.getMapAsync((map) {
    sdkMap = map;
  });
}

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

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

В общем случае работа с источниками данных выглядит следующим образом:

// Создание источника данных
final sdk.MyLocationMapObjectSource locationSource = sdk.MyLocationMapObjectSource(sdkContext);

// Добавление источника данных на карту
map?.addSource(locationSource);

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

map?.removeSource(locationSource);

Список активных источников данных можно получить, используя свойство map.sources.

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.MapObjectManager mapObjectManager = sdk.MapObjectManager(map)

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

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

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

В настройках нужно указать координаты маркера (параметр position):

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.Marker marker = sdk.Marker(
    sdk.MarkerOptions(
        position: sdk.GeoPointWithElevation(
          latitude: sdk.Latitude(55.752425),
          longitude: sdk.Longitude(37.613983),
        )
      ),
    );
    mapObjectManager.addObject(marker);

Чтобы изменить иконку маркера, нужно указать объект Image в качестве параметра icon. Создать Image можно с помощью следующих функций:

  • loadLottieFromAsset()
  • loadLottieFromFile()
  • loadPngFromAsset()
  • loadPngFromFile()
  • loadSVGFromAsset()
  • loadSVGFromFile()
import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.ImageLoader loader = sdk.ImageLoader(_sdkContext);
final sdk.Image icon = loader.loadSVGFromAsset("assets/icons/bridge.svg");

final marker = sdk.Marker(
  sdk.MarkerOptions(
    position: sdk.GeoPointWithElevation(
      latitude: sdk.Latitude(55.752425),
      longitude: sdk.Longitude(37.613983),
      icon = icon
    )
  ),
);

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

Дополнительно можно указать текст для маркера и другие настройки (MarkerOptions).

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

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

// Координаты вершин ломаной линии
final sdk.GeoPoint point1 = sdk.GeoPoint(latitude: sdk.Latitude(55.7), longitude: sdk.Longitude(37.6));
final sdk.GeoPoint point2 = sdk.GeoPoint(latitude: sdk.Latitude(55.8), longitude: sdk.Longitude(37.7));
final sdk.GeoPoint point3 = sdk.GeoPoint(latitude: sdk.Latitude(55.9), longitude: sdk.Longitude(37.8));
final sdk.GeoPoint geopoints = <sdk.GeoPoint>[point1,point2,point3]

// Создание линии
final sdk.Polyline polyline = sdk.Polyline(
  sdk.PolylineOptions(
    points: geopoints,
    width: sdk.LogicalPixel(2),
  ),
);

// Добавление линии на карту
mapObjectManager.addObject(polyline);

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

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

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.GeoPoint point1 = sdk.GeoPoint(latitude: sdk.Latitude(55.7), longitude: sdk.Longitude(37.6));
final sdk.GeoPoint point2 = sdk.GeoPoint(latitude: sdk.Latitude(55.8), longitude: sdk.Longitude(37.7));
final sdk.GeoPoint point3 = sdk.GeoPoint(latitude: sdk.Latitude(55.9), longitude: sdk.Longitude(37.8));
final sdk.GeoPoint countor = <sdk.GeoPoint>[point1,point2,point3];
final sdk.GeoPoint point4 = sdk.GeoPoint(latitude: sdk.Latitude(55.7), longitude: sdk.Longitude(37.6));
final sdk.GeoPoint point5 = sdk.GeoPoint(latitude: sdk.Latitude(55.8), longitude: sdk.Longitude(37.7));
final sdk.GeoPoint point6 = sdk.GeoPoint(latitude: sdk.Latitude(55.9), longitude: sdk.Longitude(37.8));
final sdk.GeoPoint points = <sdk.GeoPoint>[point4,point5,point6];

final sdk.Polygon polygon = sdk.Polygon(
  sdk.PolygonOptions(
    contours: [countor,points],
    strokeWidth: sdk.LogicalPixel(5),
  ),
);
mapObjectManager.addObject(polygon);

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

// Подготавливаем коллекцию объектов
final List<sdk.Marker> markers = [];
final sdk.MarkerOptions markerOptions = sdk.MarkerOptions(
    <params>
  );

markers.add(sdk.Marker(markerOptions));

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

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

class SimpleClusterRendererImpl implements sdk.SimpleClusterRenderer {
  final sdk.Image image;
  int idx = 0;

  SimpleClusterRendererImpl({
    required this.image,
  });

  @override
  sdk.SimpleClusterOptions renderCluster(sdk.SimpleClusterObject cluster) {
    final int objectCount = cluster.objectCount;
    final sdk.MapDirection? iconMapDirection =
        objectCount < 5 ? const sdk.MapDirection(45) : null;
    idx += 1;

    const double baseSize = 30.0;
    final double sizeMultiplier = 1.0 + (objectCount / 50.0);
    final double iconSize = min(baseSize * sizeMultiplier, 100);

    return sdk.SimpleClusterOptions(
      icon: image,
      iconMapDirection: iconMapDirection,
      text: objectCount.toString(),
      iconWidth: sdk.LogicalPixel(iconSize),
      userData: idx,
      zIndex: const sdk.ZIndex(1),
    );
  }
}

clusterRenderer ??= SimpleClusterRendererImpl(
  image: loader.loadSVGFromAsset("assets/icons/bridge.svg"),
);

mapObjectManager = sdk.MapObjectManager.withClustering(map, logicalPixel, maxZoom, clusterRenderer)

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

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

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

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

mapWidgetController
    .addObjectTappedCallback((sdk.RenderedObjectInfo info) async {
  // Получим ближайший к месту нажатия объект внутри установленного радиуса
  final sdk.RenderedObject obj = info.item;

  // В этом примере мы хотим найти информацию о выбранном объекте в справочнике.
  // Для этого мы должны убедиться, что тип этого объекта может быть найден
  if (obj.source is sdk.DgisSource && obj.item is sdk.DgisMapObject) {
    final source = obj.source as sdk.DgisSource;

    // Произведем поиск
    final foundObject =
        await sdk.SearchManager.createOnlineManager(sdkContext)
            .searchByDirectoryObjectId((obj.item as sdk.DgisMapObject).id)
            .value;

    final entranceIds =
        foundObject?.entrances.map((entrance) => entrance.id).toList() ??
            List.empty();

    // Снимаем выделение с выбранных ранее объектов
    source
      ..setHighlighted(source.highlightedObjects, false)
      // Выделяем полученный объект и входы
      ..setHighlighted(entranceIds, true);
  }
});

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

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

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

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.Map map = mapWidgetController.getMapAsync((map) {
  sdkMap = map;
});
final sdk.CameraPosition cameraPosition = const sdk.CameraPosition(
    point: sdk.GeoPoint(
      latitude: sdk.Latitude(55.759909),
      longitude: sdk.Longitude(37.618806),
    ),
    zoom: sdk.Zoom(15),
    tilt: sdk.Tilt(15),
    bearing: sdk.Bearing(115),
  );

map.camera.moveToCameraPosition(
    position,
    const Duration(seconds: 3),
    sdk.CameraAnimationType.linear,
  );

Для более точного контроля над анимацией перелёта можно использовать контроллер перелёта, который будет определять позицию камеры в каждый конкретный момент времени. Для этого нужно реализовать интерфейс CameraMoveController и передать созданный объект в метод move() вместо параметров перелёта.

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.CameraState currentState = map.camera.state;

Подписаться на изменения состояния камеры можно с помощью свойства stateChannel:

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

map.camera.stateChannel.listen((sdk.CameraState cameraState) {
  // Обработка изменений состояния камеры
});

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.CameraPosition currentPosition = sdkMap.camera.position;

Подписаться на изменения позиции камеры (и угла наклона/поворота) можно с помощью свойства positionChannel:

sdkMap.camera.positionChannel.listen((position) {
  // Обработка изменений позиции камеры
});

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

// Хотим «увидеть» два маркера на карте.
// Создаем геометрию, которая охватывает оба объекта
final List<sdk.SimpleMapObject> objects = [marker1, marker2, marker3];
// Рассчитываем нужную позицию
final position = sdk.calcPositionForObjects(camera, objects, styleZoomToTiltRelation, screenArea, tilt, bearing, size)

// Используем рассчитанную позицию
map.camera.moveToCameraPosition(position)

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

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

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

sdkCamera.padding = sdk.Padding(top: 10, bottom: 100);

final position = sdk.calcPositionForObjects(camera, objects, styleZoomToTiltRelation, screenArea, tilt, bearing, size);
map.camera.moveToCameraPosition(position);

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

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

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

final sdk.Geometry geometry = sdk.ComplexGeometry([sdk.PointGeometry(point1), sdk.PointGeometry(point2),])
// Задаем активную область только для расчета позиции
val position = sdk.calcPositionForGeometry(map.camera, geometry, styleZoomToTiltRelation, sdk.Padding({top:100, bottom:100}), tilt, bearing, size,)
map.camera.move(position)

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

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

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

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

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

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

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

    Синим цветом изображена visibleArea, красным — visibleRect.

  • При наклоне 0° visibleArea и visibleRect будут совпадать, как видно из изменения цвета.

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

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

camera.visibleArea.intersects(geometry)

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

Пример функции, которая принимает координаты нажатия на экран и передаёт их в метод getRenderedObjects():

import 'package:dgis_mobile_sdk_map/dgis.dart' as sdk;

map.getRenderedObjects(sdk.ScreenPoint()).then((List<sdk.RenderedObjectInfo> info) {
  // Обработка
});

Кроме использования напрямую, можно установить коллбэк для тапа (addObjectTappedCallback) или лонгтапа (addObjectLongTouchCallback) в MapWidgetController, как показано в примере с выделением объектов по касанию на карту.

Кастомизировать работу с жестами можно двумя способами:

  • Настроить существующие жесты.
  • Реализовать собственный механизм распознавания жестов.

Из коробки управлять картой можно при помощи следующих жестов:

  • сдвиг карты в любом направлении одним пальцем;
  • сдвиг карты в любом направлении несколькими пальцами;
  • вращение карты двумя пальцами;
  • масштабирование карты двумя пальцами (щипок);
  • приближение карты двойным тапом;
  • отдаление карты тапом двумя пальцами;
  • масштабирование карты при помощи тап-тап-свайп жеста одним пальцем;
  • наклон камеры свайпом двумя пальцами вверх или вниз.

По умолчанию включены все жесты. При необходимости можно отключить какие-либо жесты при помощи GestureManager.

Получить GestureManager можно напрямую у MapWidgetController:

mapWidgetController.getMapAsync((map) {
  gestureManager = mapWidgetController.gestureManager;
})

Для жестов доступны методы:

  • enableGesture — включение жестов;
  • disableGesture — отключение жестов;
  • gestureEnabled — проверка, включен или отключен жест.

Для изменения настроек или получения информации о нескольких жестах за раз можно работать напрямую со свойством enabledGestures.

Конкретный жест задаётся при помощи значений из Gesture. Значение scaling отвечает за всю группу жестов масштабирования карты. Отключить эти жесты по одному нельзя.

У некоторых жестов есть свой перечень настроек:

  • MultiTouchShiftSettings для сдвига несколькими пальцами;
  • RotationSettings для вращения;
  • ScalingSettings для масштабирования;
  • TiltSettings для наклона.

Подробнее о настройках можно прочитать на соответствующих страницах в документации. Объекты этих настроек доступны через свойства GestureManager.

Кроме настроек конкретного жеста, также существуют настройки поведения масштабирования и поворота карты. Можно настроить точку, относительно которой будут происходить операции поворота и масштабирования карты. По умолчанию эти операции работают относительно «центра масс» точек постановки пальцев. Это поведение можно сменить при помощи настройки EventsProcessingSettings. Настройку можно установить при помощи метода setSettingsAboutMapPositionPoint().

Для управления одновременным срабатыванием нескольких жестов используется метод setMutuallyExclusiveGestures.