Карта | Mobile SDK | 2GIS Documentation
Android SDK
Личный кабинет

Карта

Чтобы создать карту, добавьте MapView в ваш 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"
/>

Для карты можно указать начальные координаты (cameraTargetLat - широта; cameraTargetLng - долгота) и масштаб (cameraZoom).

MapView также можно создать программно. В таком случае настройки можно указать в виде объекта MapOptions.

Объект карты (Map) можно получить, вызвав метод getMapAsync():

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 ->
        // Действия с картой
        val camera = map.camera
    }
}

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

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

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

// Создание источника данных
val source = MyMapObjectSource(
    sdkContext,
    ...
)

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

// Добавление и удаление объектов в источнике данных
source.addObject(...)
source.removeObject(...)

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

map.removeSource(source)

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

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

mapObjectManager = MapObjectManager(map)

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

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

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

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

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

mapObjectManager.addObject(marker)

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

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

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

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

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

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

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

// Координаты вершин ломаной линии
val points = listOf(
    GeoPoint(latitude = 55.7513, longitude = 37.6236),
    GeoPoint(latitude = 55.7405, longitude = 37.6235),
    GeoPoint(latitude = 55.7439, longitude = 37.6506)
)

// Создание линии
val polyline = Polyline(
    PolylineOptions(
        points = points,
        width = 2.lpx
    )
)

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

Свойство-расширение .lpx преобразует целое число в объект LogicalPixel.

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

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

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

val polygon = Polygon(
    PolygonOptions(
        contours = listOf(
            // Вершины многоугольника
            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)
            ),
            // Координаты для выреза внутри многоугольника
            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)

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

// подготавливаем коллекцию объектов
val markers = mutableListOf<Marker>()
val markerOptions = listOf(
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    MarkerOptions(<params>),
    ...)
markerOptions.forEach({
    markers.add(Marker(it))
})

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

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

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

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

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

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

override fun onTap(point: ScreenPoint) {
    map.getRenderedObjects(point).onResult { renderedObjects ->
        // Получим ближайший к месту нажатия объект внутри установленного радиуса
        val dgisObject = renderedObjects
            .firstOrNull { it.item.source is DgisSource && it.item.item is DgisMapObject }
            ?: return@onResult

        // Сохраним источник данных объекта и его идентификатор
        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.entrances.map { entranceInfo ->
			entranceInfo.id
		} as MutableList<DgisObjectId>
		entrancesIds.add(id)

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

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

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

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

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

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", "Перелёт камеры завершён.")
    }
}

Для указания продолжительности перелёта можно использовать расширение .seconds:

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

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

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

val currentState = map.camera.state

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

// Подписка
val connection = map.camera.stateChannel.connect { state ->
    Log.d("APP", "Состояние камеры изменилось на ${state}")
}

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

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

val currentPosition = map.camera.position

Log.d("APP", "Координаты: ${currentPosition.point}")
Log.d("APP", "Приближение: ${currentPosition.zoom}")
Log.d("APP", "Наклон: ${currentPosition.tilt}")
Log.d("APP", "Поворот: ${currentPosition.bearing}")

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

// Подписка
val connection = map.camera.positionChannel.connect { position ->
    Log.d("APP", "Изменилась позиция камеры или угол наклона/поворота.")
}

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

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

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

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

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

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

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

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

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

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

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

val geometry = ComplexGeometry(listOf(PointGeometry(point1), PointGeometry(point2)))
// задаем активную область только для расчета позиции
val position = calcPosition(map.camera, geometry, padding = Padding(top = 100, bottom = 100))
map.camera.move(position)

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

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

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

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

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

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

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

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

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

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

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

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

Для того, чтобы у SDK был доступ к данным о текущем местоположении, необходимо создать источник этих данных. Можно воспользоваться стандартным источником DefaultLocationSource или создать собственный источник, реализовав интерфейс LocationSource.

Созданный источник нужно зарегистрировать в SDK. Для этого нужно вызвать метод registerPlatformLocationSource и передать в него контекст SDK и источник.

На карту можно добавить специальный маркер, который будет отражать текущее местоположение устройства. Для этого нужно добавить на карту источник данных MyLocationMapObjectSource.

// Создания источника данных о текущем местоположении
val locationSource = DefaultLocationSource(applicationContext)

// Регистрация источника данных в SDK
registerPlatformLocationSource(sdkContext, locationSource)

// Создание источника маркера геопозиции
val source = MyLocationMapObjectSource(sdkContext)

// Добавление источника маркера геопозиции на карту
map.addSource(source)

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

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

override fun onTap(point: ScreenPoint) {
    map.getRenderedObjects(point, ScreenDistance(5f)).onResult { renderedObjectInfos ->
        // Первый объект в списке - самый близкий к координатам
        for (renderedObjectInfo in renderedObjectInfos) {
            Log.d("APP", "Произвольные данные объекта: ${renderedObjectInfo.item.item.userData}")
        }
    }
}

Кроме реализации интерфейса TouchEventsObserver, можно установить коллбэк MapObjectTappedCallback для тапа или лонгтапа в MapView с помощью методов addObjectTappedCallback и addObjectLongTouchCallback. В этот коллбэк придет RenderedObjectInfo для объекта, который находится ближе всего к точке касания.

...
fun onObjectTappedOrLongTouch(objInfo: RenderedObjectInfo) {
     Log.d("APP", "Произвольные данные объекта: ${objInfo.item.item.userData}")
}
...

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

Существует возможность разместить нативные view на карте с привязкой к конкретной позиции. Для этого используется SnapToMapLayout, который содержит собственную реализацию класса LayoutParams, позволяющую установить позицию view на карте.

Добавим SnapToMapLayout в макет:

<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>
...

Для привязки view к позиции на карте, эту view необходимо добавлять внутрь SnapToMapLayout. При добавлении необходимо сконструировать LayoutParams с геокоординатой GeoPointWithElevation и некоторыми другими параметрами:

val params = SnapToMapLayout.LayoutParams(
    // Ширина, используем WRAP_CONTENT
    width = ViewGroup.LayoutParams.WRAP_CONTENT,
    // Высота, используем WRAP_CONTENT
    height = ViewGroup.LayoutParams.WRAP_CONTENT,
    // Точка на карте, к которой осуществляется привязка
    position = GeoPointWithElevation(55.7, 37.6),
    // Точка на view, к которой осуществляется привязка
    // В данном случае – левый верхний угол View
    anchor = Anchor(0.0f, 0.0f),
    // Смещение по оси X или Y относительно верхней и левой границы соответственно
    offsetX = -15 * context.resources.displayMetrics.density,
    offsetY = -15 * context.resources.displayMetrics.density
)

Эти параметры необходимо передать, например, при вызове addView().

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

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

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

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

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

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

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

Для активации жестов нужно использовать метод enableGesture.

Для выключения жестов нужно использовать метод disableGesture.

Для проверки выключен жест или нет использовать метод gestureEnabled.

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

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

// пример отключения сдвига карты одним пальцем
gestureManager.disableGesture(Gesture.SHIFT)
Log.d("GestureExample", "${gestureManager.enabledGestures}") // result 'D/GestureExample: [SCALING, ROTATION, MULTI_TOUCH_SHIFT, TILT]'

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

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

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

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

Есть возможность заменить стандартный механизм распознавания жестов на пользовательский. Для этого нужно передать в MapView реализацию интерфейса MapGestureRecognitionEngine в метод useCustomGestureRecognitionEngine.

У интерфейса есть 4 метода:

  • resetRecognitionState, при вызове которого движок должен сбросить своё состояние на начальное. Вызывается в случаях, если какие-либо события (например, перелёты камеры) завершают работу с событиями управления карты.
  • onDevicePpiChanged оповещает об изменении ppi. Значение может использоваться для перевода дистанции из экранных точек в миллиметры.
  • setMapEventSender вызывается для передачи движку объекта, через который будут отправляться генерируемые события управления картой.
  • processMotionEvent вызывается для передачи в движок точек касания к экрану в стандартом виде.
// Упрощённая реализация распознавателя жестов, умеющая определять только перемещение карты.
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) {
            // Отмена жеста
            null
        } else if (event.actionMasked == MotionEvent.ACTION_UP && event.pointerCount == 1) {
            // Поднят последний палец
            null
        } else if (event.pointerCount <= 0) {
            // Не зарегистрировано ни одного касания
            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() {}
}

Задача движка состоит в том, чтобы преобразовывать касания к экрану в события управления картой. Движок работает с событиями прямого управления картой. Все такие события представлены классами, имя которых начинается с DirectMap. Всего таких событий шесть:

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