Механизм Drag and Drop в CesiumJS предназначен для интерактивного перемещения объектов сцены при помощи мыши или сенсорного ввода. Данная функциональность широко применяется при создании геоинформационных систем, редакторов карт, инструментов планирования маршрутов, систем мониторинга и конструкторов пространственных данных.
В отличие от традиционного HTML Drag and Drop API, работающего с DOM-элементами, в CesiumJS перемещение связано с объектами трехмерной сцены:
Основная задача заключается в определении выбранного объекта, отслеживании перемещения курсора и обновлении координат объекта в режиме реального времени.
Процесс Drag and Drop обычно состоит из трех этапов:
События мыши обрабатываются через класс
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
);
Здесь происходит:
После выбора объекта можно обновлять его позицию при каждом движении мыши.
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 завершён.
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.
Такой подход позволяет 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;
}
Также можно создавать более сложные геометрические проверки:
Стандартное поведение камеры может мешать перетаскиванию объектов.
Во время начала перемещения рекомендуется временно отключать управление камерой.
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;
После завершения операции настройки возвращаются обратно.
Билборды часто используются для отображения маркеров.
Создание объекта:
viewer.entities.add({
position:
Cesium.Cartesian3.fromDegrees(
10,
50
),
billboard: {
image: "marker.png"
}
});
Механизм перетаскивания остаётся практически идентичным.
Разница заключается только в типе визуального представления объекта.
Для трёхмерных моделей применяется аналогичная схема.
Создание модели:
const modelEntity =
viewer.entities.add({
position:
Cesium.Cartesian3.fromDegrees(
20,
40
),
model: {
uri: "aircraft.glb"
}
});
Во время Drag and Drop изменяется свойство:
modelEntity.position = newPosition;
Модель автоматически обновит своё положение в сцене.
Для полигона обычно перемещаются его вершины.
Пример структуры:
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
);
Лучше переиспользовать существующие структуры данных, где это возможно.
Причины:
Проверка:
console.log(
viewer.scene.pick(
movement.position
)
);
Причины:
Проверка:
if (!cartesian) {
return;
}
Причина:
enableRotate = true
Во время перетаскивания управление камерой следует отключать.
Причины:
Drag and Drop является базовым механизмом для построения интерактивных инструментов в CesiumJS:
Грамотная реализация Drag and Drop позволяет превратить статическую трёхмерную карту в полноценную интерактивную среду редактирования геопространственной информации.