Picking объектов

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

В CesiumJS механизм определения объекта под курсором называется Picking. Он позволяет определить, какой объект находится в указанной точке экрана, а также получить ссылку на связанную сущность, примитив или 3D-модель.

Система Picking используется практически во всех интерактивных геоинформационных приложениях:

  • выбор маркеров;
  • выделение полигонов;
  • отображение всплывающих окон;
  • работа с 3D Tiles;
  • взаимодействие с моделями формата glTF;
  • получение координат точки на поверхности Земли;
  • измерительные инструменты;
  • редакторы геометрии.

Координатные пространства и принцип работы Picking

При отображении объектов Cesium выполняет преобразование координат через несколько пространств:

  1. Географические координаты.
  2. Мировые координаты ECEF.
  3. Координаты камеры.
  4. Координаты проекции.
  5. Пиксельные координаты экрана.

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

Например:

{
    x: 450,
    y: 320
}

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


Получение позиции курсора

Координаты мыши обычно извлекаются через объект события.

Пример:

const handler = new Cesium.ScreenSpaceEventHandler(
    viewer.scene.canvas
);

handler.setInputAction(function(click) {

    console.log(click.position);

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Результат:

Cartesian2 {
    x: 450,
    y: 320
}

Объект Cartesian2 содержит экранные координаты курсора.


Метод Scene.pick()

Основной инструмент выбора объектов — метод:

viewer.scene.pick(windowPosition);

Он возвращает объект, находящийся под курсором.

Пример:

handler.setInputAction(function(click) {

    const pickedObject =
        viewer.scene.pick(click.position);

    console.log(pickedObject);

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Если объект найден, будет возвращена структура с информацией о выбранном элементе.

Если под курсором ничего нет:

undefined

Проверка существования объекта

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

const pickedObject =
    viewer.scene.pick(click.position);

if (Cesium.defined(pickedObject)) {

    console.log("Объект найден");

}

Функция:

Cesium.defined()

является стандартным способом проверки значений в CesiumJS.


Выбор Entity

Наиболее распространённый сценарий — работа с сущностями.

Создание сущности:

viewer.entities.add({
    name: "Город",
    position: Cesium.Cartesian3.fromDegrees(
        37.6173,
        55.7558
    ),
    point: {
        pixelSize: 12,
        color: Cesium.Color.RED
    }
});

Получение сущности:

handler.setInputAction(function(click) {

    const picked =
        viewer.scene.pick(click.position);

    if (Cesium.defined(picked)) {

        console.log(picked.id);

    }

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Вывод:

Entity

Свойство:

picked.id

содержит исходную сущность.


Доступ к данным Entity

После получения объекта можно обращаться к его свойствам.

handler.setInputAction(function(click) {

    const picked =
        viewer.scene.pick(click.position);

    if (Cesium.defined(picked)) {

        const entity = picked.id;

        console.log(entity.name);

    }

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Результат:

Город

Работа с описанием объекта

Описание может использоваться для информационных окон.

Создание сущности:

viewer.entities.add({
    name: "Москва",
    description: "Столица России",
    position: Cesium.Cartesian3.fromDegrees(
        37.6173,
        55.7558
    ),
    point: {
        pixelSize: 12
    }
});

Получение описания:

const entity = picked.id;

console.log(
    entity.description.getValue()
);

Использование selectedEntity

Cesium имеет встроенный механизм выбора объекта.

viewer.selectedEntity = entity;

Пример:

handler.setInputAction(function(click) {

    const picked =
        viewer.scene.pick(click.position);

    if (Cesium.defined(picked)) {

        viewer.selectedEntity =
            picked.id;

    }

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

После выбора автоматически открывается информационная панель Viewer.


Получение нескольких объектов

Иногда под курсором располагается несколько элементов.

Для таких случаев используется:

viewer.scene.drillPick()

Пример drillPick

const pickedObjects =
    viewer.scene.drillPick(
        click.position
    );

Результат:

[
    object1,
    object2,
    object3
]

Каждый элемент массива представляет найденный объект.


Ограничение количества результатов

Можно указать максимальное число объектов.

const pickedObjects =
    viewer.scene.drillPick(
        click.position,
        5
    );

Будут возвращены не более пяти элементов.


Выбор объекта под курсором

Для обработки наведения применяется событие:

Cesium.ScreenSpaceEventType.MOUSE_MOVE

Пример:

handler.setInputAction(function(movement) {

    const picked =
        viewer.scene.pick(
            movement.endPosition
        );

    if (Cesium.defined(picked)) {

        console.log("Наведение");

    }

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Подсветка объектов при наведении

Часто требуется визуально показать выбранный элемент.

Создадим сущность:

const city = viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(
        37.6173,
        55.7558
    ),
    point: {
        pixelSize: 12,
        color: Cesium.Color.YELLOW
    }
});

Изменение цвета:

handler.setInputAction(function(movement) {

    const picked =
        viewer.scene.pick(
            movement.endPosition
        );

    if (
        Cesium.defined(picked) &&
        picked.id === city
    ) {

        city.point.color =
            Cesium.Color.RED;

    }

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Снятие подсветки

Обычно хранится ссылка на ранее выделенный объект.

let highlighted = null;

Пример:

handler.setInputAction(function(movement) {

    if (highlighted) {

        highlighted.point.color =
            Cesium.Color.YELLOW;

        highlighted = null;
    }

    const picked =
        viewer.scene.pick(
            movement.endPosition
        );

    if (Cesium.defined(picked)) {

        highlighted = picked.id;

        highlighted.point.color =
            Cesium.Color.RED;
    }

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Метод pickPosition()

Обычный pick() определяет объект.

Для получения координат точки используется:

viewer.scene.pickPosition()

Получение координат точки

const position =
    viewer.scene.pickPosition(
        click.position
    );

Возвращается:

Cartesian3

Пример использования:

if (Cesium.defined(position)) {

    console.log(position);

}

Преобразование координат в долготу и широту

Полученный Cartesian3 можно перевести в географические координаты.

const cartographic =
    Cesium.Cartographic.fromCartesian(
        position
    );

Получение значений:

const longitude =
    Cesium.Math.toDegrees(
        cartographic.longitude
    );

const latitude =
    Cesium.Math.toDegrees(
        cartographic.latitude
    );

Полный пример

handler.setInputAction(function(click) {

    const position =
        viewer.scene.pickPosition(
            click.position
        );

    if (Cesium.defined(position)) {

        const cartographic =
            Cesium.Cartographic.fromCartesian(
                position
            );

        console.log(
            Cesium.Math.toDegrees(
                cartographic.longitude
            ),
            Cesium.Math.toDegrees(
                cartographic.latitude
            )
        );
    }

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Различие между pick() и pickPosition()

Обе функции используются совместно, однако решают разные задачи.

pick()

Возвращает:

Объект сцены

Применяется для:

  • выбора сущностей;
  • выбора примитивов;
  • работы с 3D Tiles;
  • получения объекта интерфейса.

Пример:

const picked =
    viewer.scene.pick(position);

pickPosition()

Возвращает:

Cartesian3

Применяется для:

  • получения координат;
  • измерений;
  • рисования геометрии;
  • определения позиции на модели.

Пример:

const worldPosition =
    viewer.scene.pickPosition(
        position
    );

Метод camera.pickEllipsoid()

Если требуется получить точку на поверхности Земли без анализа объектов сцены, используется:

viewer.camera.pickEllipsoid()

Пример:

const position =
    viewer.camera.pickEllipsoid(
        click.position
    );

Возвращаемое значение:

Cartesian3

Отличия pickEllipsoid и pickPosition

pickEllipsoid

Работает:

  • по поверхности эллипсоида;
  • без учёта моделей;
  • без учёта terrain.
viewer.camera.pickEllipsoid(...)

pickPosition

Работает:

  • по terrain;
  • по 3D Tiles;
  • по моделям;
  • по примитивам.
viewer.scene.pickPosition(...)

Для современных приложений обычно предпочтителен именно pickPosition().


Picking примитивов

Помимо Entity, Cesium позволяет выбирать низкоуровневые примитивы.

Создание примитива:

const primitive =
    viewer.scene.primitives.add(
        new Cesium.PointPrimitiveCollection()
    );

Добавление точки:

primitive.add({
    position:
        Cesium.Cartesian3.fromDegrees(
            30,
            50
        ),
    pixelSize: 10
});

Получение:

const picked =
    viewer.scene.pick(
        click.position
    );

В этом случае:

picked.primitive

будет содержать выбранный примитив.


Picking 3D Tiles

Поддержка выбора объектов встроена в движок 3D Tiles.

const picked =
    viewer.scene.pick(
        click.position
    );

После выбора можно получить информацию:

console.log(
    picked.getProperty("name")
);

Пример чтения атрибутов:

const buildingName =
    picked.getProperty(
        "building_name"
    );

Picking glTF-моделей

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

Создание модели:

const modelEntity =
    viewer.entities.add({
        position:
            Cesium.Cartesian3.fromDegrees(
                30,
                50
            ),
        model: {
            uri: "model.glb"
        }
    });

Выбор:

const picked =
    viewer.scene.pick(
        click.position
    );

Получение сущности:

const entity =
    picked.id;

Ограничение области выбора

Иногда требуется анализировать не один пиксель, а область вокруг курсора.

Для этого используется дополнительный параметр.

viewer.scene.pick(
    position,
    width,
    height
);

Пример:

const picked =
    viewer.scene.pick(
        click.position,
        10,
        10
    );

Будет проверяться область размером 10×10 пикселей.


Производительность Picking

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

Не выполнять сложную логику на каждом движении мыши

Плохо:

MOUSE_MOVE
viewer.scene.pick(...)

с десятками дополнительных операций.

Лучше:

  • использовать кэширование;
  • обрабатывать только изменение объекта под курсором;
  • ограничивать частоту обновления.

Хранить ссылки на выбранные объекты

Вместо повторного поиска:

viewer.entities.getById(...)

лучше сохранять объект сразу после выбора.

selectedEntity = picked.id;

Использовать drillPick только при необходимости

Метод:

drillPick()

анализирует большее количество объектов и требует больше вычислений, чем обычный:

pick()

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


Типовая архитектура системы выбора

Во многих профессиональных приложениях применяется следующая схема:

  1. Пользователь перемещает курсор.
  2. Выполняется pick().
  3. Определяется объект под курсором.
  4. Выполняется подсветка.
  5. При клике объект становится активным.
  6. Информация отображается в отдельной панели.
  7. Координаты извлекаются через pickPosition().
  8. Выполняются дополнительные действия приложения.

Подобная архитектура обеспечивает единый механизм взаимодействия с сущностями, примитивами, 3D Tiles, terrain и трёхмерными моделями, формируя основу большинства инструментов редактирования, анализа и навигации в приложениях на базе CesiumJS.