1113 lines
41 KiB
JavaScript
1113 lines
41 KiB
JavaScript
class CesiumMapEditor {
|
||
constructor() {
|
||
this.viewer = null;
|
||
this.currentMode = 'select'; // select, marker, polygon
|
||
this.tempEntities = [];
|
||
this.selectedEntity = null;
|
||
this.drawingHandler = null;
|
||
this.editPoints = [];
|
||
this.currentPolygonPoints = [];
|
||
this.currentPolylinePoints = [];
|
||
this.isDrawing = false;
|
||
|
||
const fileInput = document.getElementById('fileInput');
|
||
if (fileInput) {
|
||
fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||
}
|
||
|
||
this.initCesium();
|
||
this.initUI();
|
||
this.initEventListeners();
|
||
}
|
||
|
||
initCesium() {
|
||
// Инициализация Cesium Viewer :cite[1]
|
||
Cesium.Ion.defaultAccessToken = undefined;
|
||
const osmProvider = new Cesium.OpenStreetMapImageryProvider({
|
||
url: 'https://tile.openstreetmap.org/'
|
||
});
|
||
const esriImagery = new Cesium.ArcGisMapServerImageryProvider({
|
||
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
|
||
});
|
||
|
||
|
||
|
||
|
||
this.viewer = new Cesium.Viewer('cesiumContainer', {
|
||
terrainProvider: Cesium.createWorldTerrain,
|
||
imageryProvider: esriImagery,
|
||
baseLayerPicker: false,
|
||
homeButton: true,
|
||
sceneModePicker: true,
|
||
navigationHelpButton: false,
|
||
animation: false,
|
||
timeline: false,
|
||
fullscreenButton: false,
|
||
geocoder: false
|
||
});
|
||
|
||
// this.viewer.imageryLayers.addImageryProvider(esriImagery);
|
||
|
||
// this.viewer.scene.transitioner.enableTransitions = false;
|
||
|
||
// Начальная позиция камеры :cite[1]
|
||
// this.viewer.camera.setView({
|
||
// destination: Cesium.Cartesian3.fromDegrees(38.5238, 60.4547, 10000),
|
||
// orientation: {
|
||
// heading: 0.0,
|
||
// pitch: -0.5,
|
||
// roll: 0.0
|
||
// }
|
||
// });
|
||
this.viewer.imageryLayers.removeAll();
|
||
this.viewer.imageryLayers.addImageryProvider(osmProvider);
|
||
}
|
||
|
||
initUI() {
|
||
this.updateModeStatus();
|
||
}
|
||
|
||
initEventListeners() {
|
||
// Обработчики кнопок режимов
|
||
document.getElementById('selectMode').addEventListener('click', () => this.setMode('select'));
|
||
document.getElementById('markerMode').addEventListener('click', () => this.setMode('marker'));
|
||
document.getElementById('polygonMode').addEventListener('click', () => this.setMode('polygon'));
|
||
document.getElementById('polylineMode').addEventListener('click', () => this.setMode('polyline'));
|
||
|
||
// Кнопки действий
|
||
document.getElementById('deleteSelected').addEventListener('click', () => this.deleteSelected());
|
||
document.getElementById('clearAll').addEventListener('click', () => this.clearAll());
|
||
|
||
// Модальное окно
|
||
document.getElementById('confirmDescription').addEventListener('click', () => this.confirmDescription());
|
||
document.getElementById('cancelDescription').addEventListener('click', () => this.cancelDescription());
|
||
|
||
// Обработка клавиатуры
|
||
document.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
||
|
||
// Обработка кликов по карте
|
||
this.setupMapClickHandler();
|
||
this.setupMouseMoveHandler();
|
||
|
||
document.getElementById('importBtn').addEventListener('click', () => this.triggerFileImport());
|
||
document.getElementById('exportBtn').addEventListener('click', () => this.showExportModal());
|
||
document.getElementById('exportGeoJson').addEventListener('click', () => this.exportAsGeoJson());
|
||
document.getElementById('exportKml').addEventListener('click', () => this.exportAsKml());
|
||
document.getElementById('cancelExport').addEventListener('click', () => this.hideExportModal());
|
||
}
|
||
|
||
setupMapClickHandler() {
|
||
this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
||
|
||
// Обработка одинарного клика
|
||
this.drawingHandler.setInputAction((click) => {
|
||
const pickedObject = this.viewer.scene.pick(click.position);
|
||
|
||
if (this.currentMode === 'select') {
|
||
this.handleEntitySelection(pickedObject);
|
||
} else if (this.currentMode === 'marker') {
|
||
this.addMarker(click.position);
|
||
} else if (this.currentMode === 'polygon') {
|
||
this.handlePolygonDrawing(click.position);
|
||
} else if (this.currentMode === 'polyline') {
|
||
this.handlePolylineDrawing(click.position);
|
||
}
|
||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||
|
||
// Обработка двойного клика для завершения полигона
|
||
this.drawingHandler.setInputAction((click) => {
|
||
if ((this.currentMode === 'polygon') &&
|
||
this.isDrawing && this.currentPolygonPoints.length >= 3) {
|
||
this.finishPolygonDrawing();
|
||
} else if (this.currentMode === 'polyline' && this.isDrawing && this.currentPolylinePoints.length >= 2) {
|
||
this.finishPolylineDrawing();
|
||
}
|
||
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
|
||
}
|
||
|
||
handlePolylineDrawing(clickPosition) {
|
||
const cartesian = this.viewer.camera.pickEllipsoid(clickPosition, this.viewer.scene.globe.ellipsoid);
|
||
if (!cartesian) return;
|
||
|
||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||
const point = {
|
||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||
height: cartographic.height
|
||
};
|
||
|
||
if (!this.isDrawing) {
|
||
this.startPolylineDrawing(point);
|
||
} else {
|
||
this.addPointToCurrentPolyline(point);
|
||
}
|
||
}
|
||
|
||
startPolylineDrawing(firstPoint) {
|
||
this.isDrawing = true;
|
||
this.currentPolylinePoints = [firstPoint];
|
||
|
||
// Временная точка
|
||
const firstTempPoint = this.viewer.entities.add({
|
||
position: Cesium.Cartesian3.fromDegrees(firstPoint.longitude, firstPoint.latitude, firstPoint.height),
|
||
point: {
|
||
pixelSize: 8,
|
||
color: Cesium.Color.YELLOW,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2
|
||
}
|
||
});
|
||
this.tempEntities.push(firstTempPoint);
|
||
|
||
// Временная линия (полилиния)
|
||
this.tempPolyline = this.viewer.entities.add({
|
||
polyline: {
|
||
positions: new Cesium.CallbackProperty(() => {
|
||
return this.currentPolylinePoints.map(p =>
|
||
Cesium.Cartesian3.fromDegrees(p.longitude, p.latitude, p.height)
|
||
);
|
||
}, false),
|
||
width: 3,
|
||
material: Cesium.Color.RED.withAlpha(0.8),
|
||
clampToGround: true
|
||
}
|
||
});
|
||
}
|
||
|
||
addPointToCurrentPolyline(point) {
|
||
this.currentPolylinePoints.push(point);
|
||
|
||
const tempPoint = this.viewer.entities.add({
|
||
position: Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height),
|
||
point: {
|
||
pixelSize: 8,
|
||
color: Cesium.Color.YELLOW,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2
|
||
}
|
||
});
|
||
this.tempEntities.push(tempPoint);
|
||
}
|
||
|
||
finishPolylineDrawing() {
|
||
if (this.currentPolylinePoints.length < 2) {
|
||
alert('Линия должна иметь минимум 2 точки');
|
||
return;
|
||
}
|
||
|
||
this.showDescriptionModal().then(description => {
|
||
if (description !== null) {
|
||
this.viewer.entities.add({
|
||
polyline: {
|
||
positions: this.currentPolylinePoints.map(p =>
|
||
Cesium.Cartesian3.fromDegrees(p.longitude, p.latitude, p.height)
|
||
),
|
||
width: 4,
|
||
material: Cesium.Color.RED.withAlpha(0.9),
|
||
clampToGround: true
|
||
},
|
||
description: description
|
||
});
|
||
}
|
||
this.cleanupPolylineDrawing();
|
||
this.setMode('select');
|
||
});
|
||
}
|
||
|
||
cleanupPolylineDrawing() {
|
||
this.isDrawing = false;
|
||
this.currentPolylinePoints = [];
|
||
if (this.tempPolyline) {
|
||
this.viewer.entities.remove(this.tempPolyline);
|
||
this.tempPolyline = null;
|
||
}
|
||
this.clearTempEntities();
|
||
}
|
||
|
||
setupMouseMoveHandler() {
|
||
const mouseMoveHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
||
|
||
mouseMoveHandler.setInputAction((movement) => {
|
||
const ellipsoid = this.viewer.scene.globe.ellipsoid;
|
||
const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, ellipsoid);
|
||
|
||
if (cartesian) {
|
||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||
const lon = Cesium.Math.toDegrees(cartographic.longitude);
|
||
const lat = Cesium.Math.toDegrees(cartographic.latitude);
|
||
|
||
// Форматируем до 6 знаков после запятой (примерно 10 см точность)
|
||
const lonStr = lon.toFixed(6);
|
||
const latStr = lat.toFixed(6);
|
||
|
||
document.getElementById('coordinates').textContent = `${latStr}°, ${lonStr}°`;
|
||
} else {
|
||
// Мышь вне земли (океан, край карты и т.д.)
|
||
document.getElementById('coordinates').textContent = '';
|
||
}
|
||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||
}
|
||
|
||
handleEntitySelection(pickedObject) {
|
||
// Снимаем предыдущее выделение
|
||
if (this.selectedEntity) {
|
||
this.selectedEntity.outline = false;
|
||
this.selectedEntity = null;
|
||
}
|
||
|
||
// Удаляем точки редактирования
|
||
this.clearEditPoints();
|
||
|
||
if (pickedObject && pickedObject.id) {
|
||
this.selectedEntity = pickedObject.id;
|
||
this.selectedEntity.outline = true;
|
||
this.selectedEntity.outlineColor = Cesium.Color.YELLOW;
|
||
this.selectedEntity.outlineWidth = 2;
|
||
|
||
// Если это полигон, показываем точки для редактирования
|
||
if (this.selectedEntity.polygon) {
|
||
this.showEditPointsForPolygon(this.selectedEntity);
|
||
}
|
||
}
|
||
}
|
||
|
||
triggerFileImport() {
|
||
document.getElementById('fileInput').click();
|
||
}
|
||
|
||
handleFileSelect(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const fileName = file.name.toLowerCase();
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = (e) => {
|
||
const content = e.target.result;
|
||
try {
|
||
if (fileName.endsWith('.kml')) {
|
||
this.loadKml(content);
|
||
} else if (fileName.endsWith('.geojson') || fileName.endsWith('.json')) {
|
||
this.loadGeoJson(content);
|
||
} else {
|
||
alert('Поддерживаются только файлы: .geojson, .json, .kml');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка импорта:', error);
|
||
alert('Не удалось загрузить файл. Проверьте его содержимое.');
|
||
}
|
||
// Сброс для повторного выбора того же файла
|
||
event.target.value = '';
|
||
};
|
||
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
loadGeoJson(geoJsonString) {
|
||
const geoJson = JSON.parse(geoJsonString);
|
||
const dataSource = new Cesium.GeoJsonDataSource();
|
||
|
||
dataSource.load(geoJson).then(() => {
|
||
// Переносим все сущности в viewer.entities
|
||
const entities = dataSource.entities.values;
|
||
for (let i = 0; i < entities.length; i++) {
|
||
const entity = entities[i];
|
||
// Копируем описание, если нужно
|
||
this.viewer.entities.add(entity);
|
||
}
|
||
|
||
// Удаляем dataSource — теперь объекты управляются напрямую
|
||
this.viewer.dataSources.remove(dataSource);
|
||
}).catch(error => {
|
||
console.error('Ошибка GeoJSON:', error);
|
||
alert('Ошибка при загрузке GeoJSON');
|
||
});
|
||
}
|
||
|
||
loadKml(kmlString) {
|
||
const blob = new Blob([kmlString], { type: 'application/vnd.google-earth.kml+xml' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
Cesium.KmlDataSource.load(url, {
|
||
camera: this.viewer.camera,
|
||
canvas: this.viewer.scene.canvas
|
||
}).then(dataSource => {
|
||
const entities = dataSource.entities.values;
|
||
for (let i = 0; i < entities.length; i++) {
|
||
this.viewer.entities.add(entities[i]);
|
||
}
|
||
this.viewer.dataSources.remove(dataSource);
|
||
URL.revokeObjectURL(url);
|
||
}).catch(error => {
|
||
console.error('Ошибка KML:', error);
|
||
alert('Ошибка при загрузке KML');
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
}
|
||
|
||
showExportModal() {
|
||
document.getElementById('exportModal').style.display = 'block';
|
||
}
|
||
|
||
hideExportModal() {
|
||
document.getElementById('exportModal').style.display = 'none';
|
||
}
|
||
|
||
// Вспомогательная функция: получить все сущности как массив
|
||
getAllEntities() {
|
||
return this.viewer.entities.values;
|
||
}
|
||
|
||
// Экспорт в GeoJSON
|
||
exportAsGeoJson() {
|
||
const entities = this.getAllEntities();
|
||
if (entities.length === 0) {
|
||
alert('Нет объектов для экспорта');
|
||
this.hideExportModal();
|
||
return;
|
||
}
|
||
|
||
const features = [];
|
||
|
||
for (const entity of entities) {
|
||
let geometry = null;
|
||
let properties = {
|
||
name: entity.name || 'Объект',
|
||
description: entity.description?.getValue?.() || 'Без описания'
|
||
};
|
||
|
||
// --- Точка (маркер) ---
|
||
if (entity.position) {
|
||
const pos = entity.position.getValue(Cesium.JulianDate.now());
|
||
if (pos) {
|
||
const cart = Cesium.Cartographic.fromCartesian(pos);
|
||
const lon = Cesium.Math.toDegrees(cart.longitude);
|
||
const lat = Cesium.Math.toDegrees(cart.latitude);
|
||
geometry = {
|
||
type: "Point",
|
||
coordinates: [lon, lat]
|
||
};
|
||
}
|
||
}
|
||
|
||
// --- Полигон ---
|
||
if (entity.polygon) {
|
||
const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
|
||
if (hierarchy && hierarchy.positions) {
|
||
const coords = hierarchy.positions.map(p => {
|
||
const cart = Cesium.Cartographic.fromCartesian(p);
|
||
return [
|
||
Cesium.Math.toDegrees(cart.longitude),
|
||
Cesium.Math.toDegrees(cart.latitude)
|
||
];
|
||
});
|
||
// Замыкаем полигон
|
||
if (coords.length > 0 && !Cesium.Cartesian3.equals(coords[0], coords[coords.length - 1])) {
|
||
coords.push([...coords[0]]);
|
||
}
|
||
geometry = {
|
||
type: "Polygon",
|
||
coordinates: [coords]
|
||
};
|
||
}
|
||
}
|
||
|
||
if (geometry) {
|
||
features.push({
|
||
type: "Feature",
|
||
properties: properties,
|
||
geometry: geometry
|
||
});
|
||
}
|
||
}
|
||
|
||
if (features.length === 0) {
|
||
alert('Не удалось экспортировать объекты: не поддерживаемые типы');
|
||
this.hideExportModal();
|
||
return;
|
||
}
|
||
|
||
const geoJson = {
|
||
type: "FeatureCollection",
|
||
features: features
|
||
};
|
||
|
||
this.downloadFile(JSON.stringify(geoJson, null, 2), 'map_export.geojson', 'application/json');
|
||
this.hideExportModal();
|
||
}
|
||
|
||
// Экспорт в KML
|
||
exportAsKml() {
|
||
const entities = this.getAllEntities();
|
||
if (entities.length === 0) {
|
||
alert('Нет объектов для экспорта');
|
||
this.hideExportModal();
|
||
return;
|
||
}
|
||
|
||
let kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||
<Document>
|
||
<name>Экспорт из CesiumMapEditor</name>
|
||
`;
|
||
|
||
for (const entity of entities) {
|
||
const name = (entity.name || 'Объект').replace(/</g, '<').replace(/>/g, '>');
|
||
const desc = (entity.description?.getValue?.() || 'Без описания')
|
||
.replace(/</g, '<').replace(/>/g, '>');
|
||
|
||
// --- Точка ---
|
||
if (entity.position) {
|
||
const pos = entity.position.getValue(Cesium.JulianDate.now());
|
||
if (pos) {
|
||
const cart = Cesium.Cartographic.fromCartesian(pos);
|
||
const lon = Cesium.Math.toDegrees(cart.longitude);
|
||
const lat = Cesium.Math.toDegrees(cart.latitude);
|
||
kml += `
|
||
<Placemark>
|
||
<name>${name}</name>
|
||
<description>${desc}</description>
|
||
<Point>
|
||
<coordinates>${lon},${lat}</coordinates>
|
||
</Point>
|
||
</Placemark>`;
|
||
}
|
||
}
|
||
|
||
// --- Полигон ---
|
||
if (entity.polygon) {
|
||
const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
|
||
if (hierarchy && hierarchy.positions) {
|
||
const coords = hierarchy.positions.map(p => {
|
||
const cart = Cesium.Cartographic.fromCartesian(p);
|
||
return `${Cesium.Math.toDegrees(cart.longitude)},${Cesium.Math.toDegrees(cart.latitude)}`;
|
||
});
|
||
// Замыкаем полигон
|
||
if (coords.length > 0) {
|
||
coords.push(coords[0]);
|
||
}
|
||
kml += `
|
||
<Placemark>
|
||
<name>${name}</name>
|
||
<description>${desc}</description>
|
||
<Polygon>
|
||
<outerBoundaryIs>
|
||
<LinearRing>
|
||
<coordinates>
|
||
${coords.join(' ')}
|
||
</coordinates>
|
||
</LinearRing>
|
||
</outerBoundaryIs>
|
||
</Polygon>
|
||
</Placemark>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
kml += `
|
||
</Document>
|
||
</kml>`;
|
||
|
||
this.downloadFile(kml, 'map_export.kml', 'application/vnd.google-earth.kml+xml');
|
||
this.hideExportModal();
|
||
}
|
||
|
||
// Вспомогательная функция: скачать строку как файл
|
||
downloadFile(content, filename, mimeType) {
|
||
const blob = new Blob([content], { type: mimeType });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
addMarker(clickPosition) {
|
||
// Получаем декартовы координаты на сфере
|
||
const cartesian = this.viewer.camera.pickEllipsoid(clickPosition, this.viewer.scene.globe.ellipsoid);
|
||
if (!cartesian) return;
|
||
|
||
// Преобразуем декартовы координаты в картографические (в радианах)
|
||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||
|
||
// Преобразуем радианы в градусы
|
||
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
|
||
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
|
||
const height = cartographic.height;
|
||
|
||
this.showDescriptionModal().then(description => {
|
||
if (description !== null) {
|
||
// Используем правильные градусы для создания позиции
|
||
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
|
||
|
||
const pinBuilder = new Cesium.PinBuilder();
|
||
|
||
const entity = this.viewer.entities.add({
|
||
position: position,
|
||
billboard: {
|
||
image: pinBuilder.fromColor(Cesium.Color.RED, 48).toDataURL(), // 48px
|
||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||
scale: 1.0
|
||
},
|
||
label: {
|
||
text: description,
|
||
font: '12pt sans-serif',
|
||
pixelOffset: new Cesium.Cartesian2(0, -10),
|
||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||
fillColor: Cesium.Color.WHITE,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2,
|
||
showBackground: true,
|
||
backgroundColor: Cesium.Color.BLACK.withAlpha(0.6)
|
||
},
|
||
description: description
|
||
});
|
||
|
||
this.viewer.scene.requestRender();
|
||
}
|
||
});
|
||
}
|
||
|
||
handlePolygonDrawing(clickPosition) {
|
||
const cartesian = this.viewer.camera.pickEllipsoid(clickPosition, this.viewer.scene.globe.ellipsoid);
|
||
if (!cartesian) return;
|
||
|
||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||
const point = {
|
||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||
height: cartographic.height
|
||
};
|
||
|
||
if (!this.isDrawing) {
|
||
// Начинаем новое рисование
|
||
this.startPolygonDrawing(point);
|
||
} else {
|
||
// Добавляем точку к текущему полигону
|
||
this.addPointToCurrentPolygon(point);
|
||
}
|
||
}
|
||
|
||
startPolygonDrawing(firstPoint) {
|
||
this.isDrawing = true;
|
||
this.currentPolygonPoints = [firstPoint];
|
||
|
||
// Добавляем первую временную точку
|
||
const firstTempPoint = this.viewer.entities.add({
|
||
position: Cesium.Cartesian3.fromDegrees(firstPoint.longitude, firstPoint.latitude, firstPoint.height),
|
||
point: {
|
||
pixelSize: 8,
|
||
color: Cesium.Color.YELLOW,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2
|
||
}
|
||
});
|
||
this.tempEntities.push(firstTempPoint);
|
||
|
||
// Создаём временный полигон
|
||
this.tempPolygon = this.viewer.entities.add({
|
||
polygon: {
|
||
hierarchy: new Cesium.CallbackProperty(() => {
|
||
return new Cesium.PolygonHierarchy(
|
||
this.currentPolygonPoints.map(p =>
|
||
Cesium.Cartesian3.fromDegrees(p.longitude, p.latitude, p.height)
|
||
)
|
||
);
|
||
}, false),
|
||
material: Cesium.Color.BLUE.withAlpha(0.5),
|
||
outline: true,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
extrudedHeight: 0,
|
||
height: 0
|
||
}
|
||
});
|
||
}
|
||
|
||
addPointToCurrentPolygon(point) {
|
||
this.currentPolygonPoints.push(point);
|
||
|
||
// Добавляем временную точку для визуализации
|
||
const tempPoint = this.viewer.entities.add({
|
||
position: Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height),
|
||
point: {
|
||
pixelSize: 8,
|
||
color: Cesium.Color.YELLOW,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2
|
||
}
|
||
});
|
||
this.tempEntities.push(tempPoint);
|
||
}
|
||
|
||
finishPolygonDrawing() {
|
||
if (this.currentPolygonPoints.length < 3) {
|
||
alert('Полигон должен иметь 3 точки');
|
||
return;
|
||
}
|
||
|
||
this.showDescriptionModal().then(description => {
|
||
if (description !== null) {
|
||
// Создаем финальный полигон
|
||
const polygonEntity = this.viewer.entities.add({
|
||
polygon: {
|
||
hierarchy: this.currentPolygonPoints.map(p =>
|
||
Cesium.Cartesian3.fromDegrees(p.longitude, p.latitude, p.height)
|
||
),
|
||
material: Cesium.Color.BLUE.withAlpha(0.7),
|
||
outline: true,
|
||
outlineColor: Cesium.Color.BLACK,
|
||
outlineWidth: 2,
|
||
extrudedHeight: 0,
|
||
height: 0
|
||
},
|
||
description: description
|
||
});
|
||
}
|
||
|
||
// Сбрасываем состояние рисования
|
||
this.cleanupDrawing();
|
||
this.setMode('select');
|
||
});
|
||
}
|
||
|
||
showEditPointsForPolygon(polygonEntity) {
|
||
const hierarchy = polygonEntity.polygon.hierarchy.getValue();
|
||
const positions = hierarchy.positions;
|
||
|
||
positions.forEach((position, index) => {
|
||
const editPoint = this.viewer.entities.add({
|
||
position: position,
|
||
point: {
|
||
pixelSize: 10,
|
||
color: Cesium.Color.RED,
|
||
outlineColor: Cesium.Color.WHITE,
|
||
outlineWidth: 2
|
||
},
|
||
polygonEntity: polygonEntity,
|
||
pointIndex: index
|
||
});
|
||
|
||
this.editPoints.push(editPoint);
|
||
|
||
// В реальной реализации здесь нужно добавить логику перетаскивания точек
|
||
});
|
||
}
|
||
|
||
showDescriptionModal() {
|
||
return new Promise((resolve) => {
|
||
const modal = document.getElementById('descriptionModal');
|
||
const input = document.getElementById('descriptionInput');
|
||
|
||
modal.style.display = 'block';
|
||
input.value = '';
|
||
input.focus();
|
||
|
||
this.descriptionResolve = resolve;
|
||
});
|
||
}
|
||
|
||
confirmDescription() {
|
||
const input = document.getElementById('descriptionInput');
|
||
const description = input.value.trim();
|
||
|
||
document.getElementById('descriptionModal').style.display = 'none';
|
||
|
||
if (this.descriptionResolve) {
|
||
this.descriptionResolve(description || 'Без описания');
|
||
this.descriptionResolve = null;
|
||
}
|
||
}
|
||
|
||
cancelDescription() {
|
||
document.getElementById('descriptionModal').style.display = 'none';
|
||
|
||
if (this.descriptionResolve) {
|
||
this.descriptionResolve(null);
|
||
this.descriptionResolve = null;
|
||
this.cleanupDrawing();
|
||
}
|
||
}
|
||
|
||
setMode(mode) {
|
||
// Важно: проверяем, не пытаемся ли установить тот же режим
|
||
if (this.currentMode === mode) {
|
||
return;
|
||
}
|
||
|
||
// Сначала очищаем текущую операцию
|
||
this.cleanupCurrentOperation();
|
||
|
||
// Затем устанавливаем новый режим
|
||
this.currentMode = mode;
|
||
this.updateUI();
|
||
this.updateModeStatus();
|
||
this.clearTempEntities();
|
||
|
||
if (mode === 'select') {
|
||
this.setupSelectionMode();
|
||
}
|
||
}
|
||
|
||
cancelCurrentOperation() {
|
||
this.cleanupCurrentOperation();
|
||
// Устанавливаем режим select через setMode, но с проверкой
|
||
if (this.currentMode !== 'select') {
|
||
this.setMode('select');
|
||
}
|
||
}
|
||
|
||
cleanupCurrentOperation() {
|
||
if (this.isDrawing) {
|
||
if (this.currentMode === 'polygon') {
|
||
this.cleanupDrawing();
|
||
} else if (this.currentMode === 'polyline') {
|
||
this.cleanupPolylineDrawing();
|
||
}
|
||
}
|
||
this.clearTempEntities();
|
||
}
|
||
|
||
cleanupDrawing() {
|
||
this.isDrawing = false;
|
||
this.currentPolygonPoints = [];
|
||
|
||
if (this.tempPolygon) {
|
||
this.viewer.entities.remove(this.tempPolygon);
|
||
this.tempPolygon = null;
|
||
}
|
||
|
||
this.clearTempEntities();
|
||
}
|
||
|
||
// Остальные методы остаются без изменений...
|
||
clearTempEntities() {
|
||
this.tempEntities.forEach(entity => this.viewer.entities.remove(entity));
|
||
this.tempEntities = [];
|
||
}
|
||
|
||
updateUI() {
|
||
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
||
|
||
const modeButtons = {
|
||
'select': 'selectMode',
|
||
'marker': 'markerMode',
|
||
'polygon': 'polygonMode',
|
||
'polyline': 'polylineMode',
|
||
};
|
||
|
||
if (modeButtons[this.currentMode]) {
|
||
document.getElementById(modeButtons[this.currentMode]).classList.add('active');
|
||
}
|
||
}
|
||
|
||
updateModeStatus() {
|
||
const modeNames = {
|
||
'select': 'Выделение',
|
||
'marker': 'Добавление маркеров',
|
||
'polygon': 'Рисование полигонов',
|
||
'polyline': 'Рисование линий'
|
||
};
|
||
|
||
document.getElementById('modeStatus').textContent =
|
||
`Режим: ${modeNames[this.currentMode]}`;
|
||
}
|
||
|
||
handleKeyDown(event) {
|
||
switch (event.key) {
|
||
case 'Escape':
|
||
this.cancelCurrentOperation();
|
||
break;
|
||
case 'Delete':
|
||
if (this.currentMode === 'select' && this.selectedEntity) {
|
||
this.deleteSelected();
|
||
}
|
||
break;
|
||
case 's':
|
||
case 'S':
|
||
event.preventDefault();
|
||
this.setMode('select');
|
||
break;
|
||
case 'm':
|
||
case 'M':
|
||
event.preventDefault();
|
||
this.setMode('marker');
|
||
break;
|
||
case 'p':
|
||
case 'P':
|
||
event.preventDefault();
|
||
this.setMode('polygon');
|
||
break;
|
||
case 'l':
|
||
case 'L':
|
||
event.preventDefault();
|
||
this.setMode('polyline');
|
||
break;
|
||
}
|
||
}
|
||
|
||
clearEditPoints() {
|
||
this.editPoints.forEach(point => this.viewer.entities.remove(point));
|
||
this.editPoints = [];
|
||
}
|
||
|
||
deleteSelected() {
|
||
if (this.selectedEntity) {
|
||
this.viewer.entities.remove(this.selectedEntity);
|
||
this.selectedEntity = null;
|
||
this.clearEditPoints();
|
||
}
|
||
}
|
||
|
||
clearAll() {
|
||
if (confirm('Вы уверены, что хотите удалить все объекты?')) {
|
||
this.viewer.entities.removeAll();
|
||
this.selectedEntity = null;
|
||
this.clearEditPoints();
|
||
this.cleanupDrawing();
|
||
}
|
||
}
|
||
|
||
setupSelectionMode() {
|
||
// Дополнительная настройка для режима выделения
|
||
this.clearEditPoints();
|
||
}
|
||
}
|
||
|
||
|
||
window.mapEditor = null;
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.mapEditor = new CesiumMapEditor();
|
||
document.getElementById('showAllFootprints')?.addEventListener('click', showAllFootprints);
|
||
document.getElementById('hideAllFootprints')?.addEventListener('click', hideAllFootprints);
|
||
});
|
||
|
||
document.getElementById('loadObjectBtn').addEventListener('click', function () {
|
||
const viewer = window.mapEditor.viewer;
|
||
const select = document.getElementById('objectSelector');
|
||
const sat_id = select.value;
|
||
|
||
if (!sat_id) {
|
||
alert('Пожалуйста, выберите объект.');
|
||
return;
|
||
}
|
||
|
||
// Формируем URL с параметром name
|
||
const url = `/api/locations/${encodeURIComponent(sat_id)}/geojson`;
|
||
|
||
fetch(url)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Сервер вернул ошибку: ' + response.status);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
if (!data.features || data.features.length === 0) {
|
||
alert('Объекты с таким именем не найдены.');
|
||
return;
|
||
}
|
||
|
||
viewer.entities.removeAll();
|
||
const freqColorMap = new Map();
|
||
|
||
// Вспомогательная функция: получить цвет для частоты
|
||
function getColorForFreq(freq) {
|
||
if (freqColorMap.has(freq)) {
|
||
return freqColorMap.get(freq);
|
||
}
|
||
// Генерируем новый цвет (но одинаковый для одинаковой freq)
|
||
// Чтобы цвет был воспроизводимым, можно хешировать freq → RGB
|
||
const hue = Math.abs(hashCode(String(freq))) % 360;
|
||
const color = Cesium.Color.fromHsl(hue / 360, 0.7, 0.6); // насыщенный, но не слишком тёмный
|
||
freqColorMap.set(freq, color);
|
||
return color;
|
||
}
|
||
|
||
// Простая хеш-функция для строки → число
|
||
function hashCode(str) {
|
||
let hash = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash; // Преобразуем в 32-битное целое
|
||
}
|
||
return hash;
|
||
}
|
||
const pinBuilder = new Cesium.PinBuilder();
|
||
data.features.forEach(feature => {
|
||
const [lon, lat] = feature.geometry.coordinates;
|
||
let freq = feature.properties.freq;
|
||
|
||
// Получаем цвет для этой частоты
|
||
const color = getColorForFreq(freq);
|
||
|
||
viewer.entities.add({
|
||
position: Cesium.Cartesian3.fromDegrees(lon, lat),
|
||
billboard: {
|
||
image: pinBuilder.fromColor(color, 36).toDataURL(),
|
||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||
scale: 1.0
|
||
},
|
||
description: feature.properties.name,
|
||
// label: {
|
||
// text: feature.properties.name,
|
||
// font: '12px sans-serif',
|
||
// pixelOffset: new Cesium.Cartesian2(0, -25),
|
||
// showBackground: true,
|
||
// backgroundColor: new Cesium.Color(0, 0, 0, 0.6),
|
||
// disableDepthTestDistance: Number.POSITIVE_INFINITY
|
||
// },
|
||
// Добавим ID для возможного управления позже
|
||
id: `db-marker-${feature.properties.id}`
|
||
});
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.error('Ошибка:', err);
|
||
alert('Не удалось загрузить объекты: ' + err.message);
|
||
});
|
||
});
|
||
|
||
|
||
let tileLayers = {};
|
||
let currentFootprintLayers = []; // массив активных слоёв
|
||
const togglesContainer = document.getElementById('footprintToggles');
|
||
|
||
// Функция: очистить текущие footprint-слои
|
||
function clearFootprintUIAndLayers(viewer) {
|
||
// Удаляем слои из Cesium
|
||
Object.values(tileLayers).forEach(layer => {
|
||
viewer.imageryLayers.remove(layer);
|
||
});
|
||
tileLayers = {};
|
||
|
||
// Очищаем чекбоксы
|
||
togglesContainer.innerHTML = '';
|
||
currentFootprintLayers = [];
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, m => map[m]);
|
||
}
|
||
|
||
// Функция: загрузить и отобразить footprint'ы для спутника
|
||
function loadFootprintsForSatellite(satId) {
|
||
const viewer = window.mapEditor.viewer;
|
||
if (!satId) {
|
||
clearFootprintUIAndLayers(viewer);
|
||
return;
|
||
}
|
||
const url = `/api/footprint-names/${encodeURIComponent(satId)}`;
|
||
|
||
fetch(url)
|
||
.then(response => {
|
||
if (!response.ok) throw new Error('Ошибка загрузки footprint\'ов');
|
||
return response.json();
|
||
})
|
||
.then(footprints => {
|
||
if (!Array.isArray(footprints)) {
|
||
throw new Error('Ожидался массив footprint\'ов');
|
||
}
|
||
|
||
// Очищаем старое
|
||
clearFootprintUIAndLayers(viewer);
|
||
currentFootprintLayers = footprints;
|
||
|
||
// Создаём новые слои и чекбоксы
|
||
footprints.forEach(fp => {
|
||
// 1. Создаём тайловый слой
|
||
const layer = new Cesium.ImageryLayer(
|
||
new Cesium.UrlTemplateImageryProvider({
|
||
url: `/tiles/${fp.name}/{z}/{x}/{y}.png`,
|
||
minimumLevel: 0,
|
||
maximumLevel: 21,
|
||
credit: 'SatBeams Rendered',
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
hasAlphaChannel: true,
|
||
// tileDiscardPolicy: new Cesium.DiscardMissingTileImagePolicy({
|
||
// missingImageUrl: Cesium.buildModuleUrl('static/cesium/Assets/Textures/transparent.png')
|
||
// }),
|
||
})
|
||
);
|
||
|
||
// Слои изначально ВИДИМЫ (можно изменить на false)
|
||
layer.show = true;
|
||
viewer.imageryLayers.add(layer);
|
||
tileLayers[fp.name] = layer;
|
||
|
||
const safeNameAttr = encodeURIComponent(fp.name); // для data-атрибута
|
||
const safeFullName = escapeHtml(fp.fullname); // для отображения
|
||
|
||
const label = document.createElement('label');
|
||
label.style.display = 'block';
|
||
label.style.margin = '4px 0';
|
||
label.innerHTML = `
|
||
<input type="checkbox"
|
||
data-footprint="${safeNameAttr}"
|
||
checked>
|
||
${safeFullName}
|
||
`;
|
||
togglesContainer.appendChild(label);
|
||
|
||
// Связываем чекбокс со слоем
|
||
const checkbox = label.querySelector('input');
|
||
checkbox.addEventListener('change', function () {
|
||
// Декодируем обратно при получении
|
||
const footprintName = decodeURIComponent(this.dataset.footprint);
|
||
const layer = tileLayers[footprintName];
|
||
if (layer) {
|
||
layer.show = this.checked;
|
||
}
|
||
});
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.error('Footprints error:', err);
|
||
alert('Не удалось загрузить области покрытия: ' + err.message);
|
||
clearFootprintUIAndLayers(viewer); // на случай ошибки — чистим
|
||
});
|
||
}
|
||
|
||
function showAllFootprints() {
|
||
Object.values(tileLayers).forEach(layer => {
|
||
layer.show = true;
|
||
});
|
||
// Синхронизируем чекбоксы
|
||
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||
cb.checked = true;
|
||
});
|
||
}
|
||
|
||
// Скрыть все footprint-слои
|
||
function hideAllFootprints() {
|
||
Object.values(tileLayers).forEach(layer => {
|
||
layer.show = false;
|
||
});
|
||
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||
cb.checked = false;
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const select = document.getElementById('objectSelector');
|
||
|
||
// Загружаем footprint'ы при смене выбора
|
||
select.addEventListener('change', function () {
|
||
const satId = this.value;
|
||
console.log(satId);
|
||
loadFootprintsForSatellite(satId);
|
||
});
|
||
|
||
// Также можно вызвать при первом выборе (если нужно)
|
||
// Но обычно сначала выбирают → потом нажимают кнопку
|
||
}); |