Drag and drop

Механизм Drag and Drop в CesiumJS предназначен для интерактивного перемещения объектов сцены при помощи мыши или сенсорного ввода. Данная функциональность широко применяется при создании геоинформационных систем, редакторов карт, инструментов планирования маршрутов, систем мониторинга и конструкторов пространственных данных.

В отличие от традиционного HTML Drag and Drop API, работающего с DOM-элементами, в CesiumJS перемещение связано с объектами трехмерной сцены:

  • сущностями (Entity);
  • примитивами (Primitive);
  • моделями (Model);
  • билбордами (Billboard);
  • точками (PointGraphics);
  • полигонами и полилиниями.

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


Архитектура механизма перетаскивания

Процесс Drag and Drop обычно состоит из трех этапов:

  1. Захват объекта.
  2. Перемещение объекта.
  3. Завершение перемещения.

События мыши обрабатываются через класс ScreenSpaceEventHandler.

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

LEFT_DOWN
    ↓
Выбор объекта
    ↓
MOUSE_MOVE
    ↓
Обновление позиции
    ↓
LEFT_UP
    ↓
Завершение перемещения

Создание обработчика событий

Для начала необходимо создать обработчик пользовательского ввода.

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

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


Выбор объекта для перемещения

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

Пример создания сущности:

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

Переменная для хранения выбранного объекта:

let selectedEntity = null;

Обработка нажатия кнопки мыши:

handler.setInputAction(function(click) {

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

    if (
        Cesium.defined(pickedObject) &&
        pickedObject.id
    ) {
        selectedEntity = pickedObject.id;
    }

}, Cesium.ScreenSpaceEventType.LEFT_DOWN);

Метод scene.pick() выполняет поиск объекта под указанными экранными координатами.


Определение координат курсора на поверхности Земли

Во время перемещения курсора необходимо преобразовывать экранные координаты в географическое положение.

Наиболее распространённый способ:

const position =
    viewer.scene.pickPosition(
        movement.endPosition
    );

Однако этот метод требует поддержки глубины сцены и не всегда работает одинаково во всех режимах.

Альтернативный вариант:

const ray = viewer.camera.getPickRay(
    movement.endPosition
);

const cartesian =
    viewer.scene.globe.pick(
        ray,
        viewer.scene
    );

Здесь происходит:

  1. Создание луча из камеры.
  2. Пересечение луча с поверхностью земного шара.
  3. Получение новых координат объекта.

Реализация перемещения сущности

После выбора объекта можно обновлять его позицию при каждом движении мыши.

handler.setInputAction(function(movement) {

    if (!selectedEntity) {
        return;
    }

    const ray =
        viewer.camera.getPickRay(
            movement.endPosition
        );

    const cartesian =
        viewer.scene.globe.pick(
            ray,
            viewer.scene
        );

    if (cartesian) {
        selectedEntity.position =
            cartesian;
    }

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Теперь выбранная сущность будет следовать за курсором.


Завершение перетаскивания

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

handler.setInputAction(function() {

    selectedEntity = null;

}, Cesium.ScreenSpaceEventType.LEFT_UP);

Полный цикл Drag and Drop завершён.


Полный пример Drag and Drop точки

const viewer = new Cesium.Viewer("cesiumContainer");

const point = viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(
        30,
        60
    ),
    point: {
        pixelSize: 15,
        color: Cesium.Color.YELLOW
    }
});

let draggedEntity = null;

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

handler.setInputAction(function(click) {

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

    if (
        Cesium.defined(picked) &&
        picked.id
    ) {
        draggedEntity = picked.id;
    }

}, Cesium.ScreenSpaceEventType.LEFT_DOWN);

handler.setInputAction(function(movement) {

    if (!draggedEntity) {
        return;
    }

    const ray =
        viewer.camera.getPickRay(
            movement.endPosition
        );

    const position =
        viewer.scene.globe.pick(
            ray,
            viewer.scene
        );

    if (position) {
        draggedEntity.position =
            position;
    }

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

handler.setInputAction(function() {

    draggedEntity = null;

}, Cesium.ScreenSpaceEventType.LEFT_UP);

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

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

Такой подход позволяет Cesium автоматически обновлять позицию объекта.

let currentPosition =
    Cesium.Cartesian3.fromDegrees(
        30,
        60
    );

const entity = viewer.entities.add({
    position: new Cesium.CallbackProperty(
        function() {
            return currentPosition;
        },
        false
    ),
    point: {
        pixelSize: 12
    }
});

Во время перемещения обновляется только переменная:

currentPosition = newPosition;

Преимущества:

  • более чистая архитектура;
  • снижение количества операций обновления сущности;
  • удобство интеграции с реактивными интерфейсами.

Перемещение объектов над рельефом

Если используется Terrain Provider, простого перемещения по эллипсоиду может оказаться недостаточно.

Пример получения высоты поверхности:

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

После этого можно вычислить высоту:

const height =
    viewer.scene.globe.getHeight(
        cartographic
    );

Обновление координат:

cartographic.height = height;

const terrainPosition =
    Cesium.Cartesian3.fromRadians(
        cartographic.longitude,
        cartographic.latitude,
        cartographic.height
    );

Теперь объект будет следовать рельефу.


Ограничение области перемещения

Во многих приложениях необходимо ограничивать перемещение определённой территорией.

Проверка широты:

if (
    cartographic.latitude <
    Cesium.Math.toRadians(40)
) {
    return;
}

Проверка долготы:

if (
    cartographic.longitude >
    Cesium.Math.toRadians(50)
) {
    return;
}

Также можно создавать более сложные геометрические проверки:

  • попадание в полигон;
  • пересечение границ;
  • контроль административных зон;
  • ограничение строительных площадок.

Отключение вращения камеры во время Drag and Drop

Стандартное поведение камеры может мешать перетаскиванию объектов.

Во время начала перемещения рекомендуется временно отключать управление камерой.

viewer.scene.screenSpaceCameraController
    .enableRotate = false;

После завершения операции:

viewer.scene.screenSpaceCameraController
    .enableRotate = true;

Для полного контроля можно отключить:

const controller =
    viewer.scene.screenSpaceCameraController;

controller.enableRotate = false;
controller.enableTranslate = false;
controller.enableZoom = false;
controller.enableTilt = false;
controller.enableLook = false;

После завершения операции настройки возвращаются обратно.


Drag and Drop билбордов

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

Создание объекта:

viewer.entities.add({
    position:
        Cesium.Cartesian3.fromDegrees(
            10,
            50
        ),
    billboard: {
        image: "marker.png"
    }
});

Механизм перетаскивания остаётся практически идентичным.

Разница заключается только в типе визуального представления объекта.


Перемещение моделей glTF

Для трёхмерных моделей применяется аналогичная схема.

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

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

Во время Drag and Drop изменяется свойство:

modelEntity.position = newPosition;

Модель автоматически обновит своё положение в сцене.


Drag and Drop полигонов

Для полигона обычно перемещаются его вершины.

Пример структуры:

const polygonPositions = [
    p1,
    p2,
    p3,
    p4
];

После захвата вершины:

polygonPositions[index] =
    newPosition;

Геометрия полигона обновляется через CallbackProperty.

hierarchy:
new Cesium.CallbackProperty(
    function() {
        return polygonPositions;
    },
    false
)

Такой подход лежит в основе большинства GIS-редакторов.


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

Для линий используется аналогичный механизм.

const positions = [
    startPoint,
    middlePoint,
    endPoint
];

Обновление вершины:

positions[selectedIndex] =
    newPosition;

Описание полилинии:

polyline: {
    positions:
        new Cesium.CallbackProperty(
            function() {
                return positions;
            },
            false
        ),
    width: 3
}

Линия перестраивается автоматически.


Визуальная индикация захвата объекта

Для улучшения пользовательского опыта часто используются визуальные эффекты.

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

entity.point.color =
    Cesium.Color.LIME;

После завершения:

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

Изменение размера:

entity.point.pixelSize = 20;

Отображение подписи:

entity.label.show = true;

Подобная обратная связь позволяет пользователю понимать текущее состояние объекта.


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

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

let activeEntity = null;

При выборе:

activeEntity = pickedObject.id;

При перемещении:

if (activeEntity) {
    activeEntity.position =
        newPosition;
}

Такой подход масштабируется на тысячи сущностей.


Производительность при массовом редактировании

При работе с большим количеством объектов следует учитывать несколько факторов.

Минимизировать количество вычислений

Не выполнять сложные геометрические операции на каждом событии MOUSE_MOVE.

Использовать CallbackProperty

new Cesium.CallbackProperty(...)

уменьшает количество пересозданий объектов.

Ограничивать частоту обновлений

Например:

let lastUpdate = 0;

if (
    performance.now() - lastUpdate <
    16
) {
    return;
}

Это позволяет обновлять объект примерно 60 раз в секунду.

Избегать постоянного создания объектов

Плохо:

entity.position =
    new Cesium.Cartesian3(
        x,
        y,
        z
    );

Лучше переиспользовать существующие структуры данных, где это возможно.


Типичные проблемы при реализации Drag and Drop

Объект не выбирается

Причины:

  • объект скрыт;
  • отключён pick;
  • используется неверный слой;
  • курсор попадает в другой объект.

Проверка:

console.log(
    viewer.scene.pick(
        movement.position
    )
);

Координаты возвращаются как undefined

Причины:

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

Проверка:

if (!cartesian) {
    return;
}

Камера начинает вращаться

Причина:

enableRotate = true

Во время перетаскивания управление камерой следует отключать.

Объект дёргается

Причины:

  • смешивание нескольких источников координат;
  • ошибки преобразования между Cartesian и Cartographic;
  • частые пересоздания сущностей вместо обновления позиции.

Практические области применения

Drag and Drop является базовым механизмом для построения интерактивных инструментов в CesiumJS:

  • редакторы картографических объектов;
  • системы проектирования инженерных сетей;
  • редакторы маршрутов транспорта;
  • инструменты планирования полётов;
  • геоинформационные панели мониторинга;
  • системы управления беспилотниками;
  • конструкторы цифровых двойников;
  • редакторы 3D-сцен;
  • приложения кадастрового учёта;
  • средства разметки пространственных данных.

Грамотная реализация Drag and Drop позволяет превратить статическую трёхмерную карту в полноценную интерактивную среду редактирования геопространственной информации.