class SidebarManager {
constructor(mapApp) {
this.mapApp = mapApp;
this.objects = new Map();
this.filteredObjects = new Set();
this.layerGroups = new Map();
this.showVertices = false;
this.currentObject = null;
this.initializeElements();
this.bindEvents();
this.initLayerGroups();
this.initializeDetailsPanel();
}
// Инициализация групп слоев
initLayerGroups() {
Object.entries(LayerGroups).forEach(([groupId, groupConfig]) => {
this.layerGroups.set(groupId, {
...groupConfig,
id: groupId,
visible: groupConfig.visible !== false
});
});
}
// Создание групп в сайдбаре
createLayerGroups() {
this.layerGroups.forEach((group, groupId) => {
this.createGroupListItem(group);
});
}
// Создание элемента группы
createGroupListItem(group) {
const groupItem = document.createElement('div');
groupItem.className = 'group-item';
groupItem.dataset.groupId = group.id;
const layersCount = this.countVisibleLayersInGroup(group);
groupItem.innerHTML = `
`;
const toggleBtn = groupItem.querySelector('.group-toggle-btn');
const toggleIcon = groupItem.querySelector('.toggle-icon');
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleGroupExpansion(group.id);
});
this.toggleGroupVisibility(group.id, group.visible);
const checkbox = groupItem.querySelector('.group-checkbox');
checkbox.addEventListener('change', (e) => {
this.toggleGroupVisibility(group.id, e.target.checked);
});
this.objectsContainer.appendChild(groupItem);
group.listItem = groupItem;
group.layersContainer = groupItem.querySelector('.group-layers');
}
// Переключение видимости группы
toggleGroupVisibility(groupId, visible) {
const group = this.layerGroups.get(groupId);
if (!group) return;
group.visible = visible;
group.layers.forEach(layerName => {
this.objects.forEach((objectData, objectId) => {
if (objectData.type !== 'object') return;
if (!objectData.feature || !objectData.feature.properties) {
console.warn('Объект без feature:', objectId);
return;
}
if (objectData.feature.properties.layers?.includes(layerName)) {
this.toggleObjectVisibility(objectId, visible);
if (objectData.checkbox) {
objectData.checkbox.checked = visible;
}
}
});
});
this.updateGroupCounter(groupId);
}
// Переключение развертывания группы
toggleGroupExpansion(groupId) {
const group = this.layerGroups.get(groupId);
if (!group) return;
const layersContainer = group.listItem.querySelector('.group-layers');
const toggleIcon = group.listItem.querySelector('.toggle-icon');
const wasExpanded = group.expanded;
group.expanded = !group.expanded;
layersContainer.style.display = group.expanded ? 'block' : 'none';
toggleIcon.textContent = group.expanded ? '▼' : '▶';
if (group.expanded && !wasExpanded) {
this.ensureGroupContentVisible(group.listItem);
}
}
// Обеспечение видимости контента группы
ensureGroupContentVisible(groupElement) {
const sidebar = this.objectsContainer;
const groupRect = groupElement.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
const spaceBelow = sidebarRect.bottom - groupRect.bottom;
const estimatedItemHeight = 48;
if (spaceBelow < estimatedItemHeight) {
const scrollAmount = estimatedItemHeight - spaceBelow + 10;
sidebar.scrollTop += scrollAmount;
}
}
// Подсчет объектов в группе (видимых)
countVisibleLayersInGroup(group) {
let count = 0;
group.layers.forEach(layerName => {
this.objects.forEach(objectData => {
if (objectData.type === 'object' &&
objectData.feature.properties.layers?.includes(layerName) &&
objectData.listItem.style.display !== 'none') {
count++;
}
});
});
return count;
}
// Подсчет видимых объектов в группе
countVisibleObjectsInGroup(group) {
let count = 0;
group.layers.forEach(layerName => {
this.objects.forEach(objectData => {
if (objectData.type === 'object' &&
objectData.feature.properties.layers?.includes(layerName) &&
objectData.visible &&
objectData.listItem.style.display !== 'none') {
count++;
}
});
});
return count;
}
// Обновление счетчика группы
updateGroupCounter(groupId) {
const group = this.layerGroups.get(groupId);
if (!group || !group.listItem) return;
const totalCount = this.countVisibleLayersInGroup(group);
const visibleCount = this.countVisibleObjectsInGroup(group);
const counter = group.listItem.querySelector('.group-name');
counter.textContent = `${group.name} (${visibleCount}|${totalCount})`;
}
// Добавление объекта
// Добавление объекта
addObject(feature, mapObject, vertices = null, fileName = null) {
const objectId = fileName || `obj_${Date.now()}_${Math.random()}`;
const objectData = {
id: objectId,
feature: feature,
mapObject: mapObject,
vertices: vertices,
visible: true,
type: 'object'
};
this.objects.set(objectId, objectData);
const groupId = this.findObjectGroup(feature);
this.createObjectInGroup(objectData, groupId);
// ★★★ ДОБАВЛЯЕМ ВЕРШИНЫ ТОЛЬКО ЕСЛИ ВКЛЮЧЕН ЧЕКБОКС ★★★
if (vertices && this.showVertices) {
this.mapApp.map.geoObjects.add(vertices);
}
this.updateObjectsCount();
this.updateAllGroupCounters();
}
// Поиск группы для объекта
findObjectGroup(feature) {
const objectLayers = feature.properties.layers || [];
for (const [groupId, group] of this.layerGroups.entries()) {
if (groupId === 'uncategorized') continue;
if (group.layers.some(layer => objectLayers.includes(layer))) {
return groupId;
}
}
const uncategorizedGroup = this.layerGroups.get('uncategorized');
objectLayers.forEach(layer => {
if (!uncategorizedGroup.layers.includes(layer)) {
uncategorizedGroup.layers.push(layer);
}
});
return 'uncategorized';
}
// Поиск группы для растра
findRasterGroup(layers) {
for (const [groupId, group] of this.layerGroups.entries()) {
if (group.layers.some(layer => layers.includes(layer))) {
return groupId;
}
}
return 'uncategorized';
}
// Создание объекта в группе
createObjectInGroup(objectData, groupId) {
const group = this.layerGroups.get(groupId);
if (!group || !group.layersContainer) return;
const item = document.createElement('div');
item.className = 'object-item group-object';
item.dataset.objectId = objectData.id;
const geometryType = objectData.feature.geometry.type.toLowerCase();
const icon = this.getObjectIcon(geometryType);
item.innerHTML = `
${objectData.feature.properties.name || 'Без названия'}
${icon}
`;
const checkbox = item.querySelector('.object-checkbox');
checkbox.addEventListener('change', (e) => {
this.toggleObjectVisibility(objectData.id, e.target.checked);
this.updateGroupCounter(groupId);
});
item.addEventListener('click', (e) => {
if (e.target !== checkbox) {
this.centerOnObject(objectData.id);
}
});
group.layersContainer.appendChild(item);
objectData.checkbox = checkbox;
objectData.listItem = item;
}
// Создание растрового слоя в группе
createRasterInGroup(rasterData, groupId) {
const group = this.layerGroups.get(groupId);
if (!group || !group.layersContainer) return;
const item = document.createElement('div');
item.className = 'object-item group-object';
item.dataset.objectId = rasterData.id;
item.innerHTML = `
${rasterData.name}
Растровая карта
🗺️
`;
const checkbox = item.querySelector('.object-checkbox');
checkbox.addEventListener('change', (e) => {
this.toggleObjectVisibility(rasterData.id, e.target.checked);
this.updateGroupCounter(groupId);
});
group.layersContainer.appendChild(item);
rasterData.checkbox = checkbox;
rasterData.listItem = item;
}
// Обновление всех счетчиков групп
updateAllGroupCounters() {
this.layerGroups.forEach((group, groupId) => {
this.updateGroupCounter(groupId);
});
}
// Инициализация элементов DOM
initializeElements() {
this.searchInput = document.getElementById('searchInput');
this.dateFrom = document.getElementById('dateFrom');
this.dateTo = document.getElementById('dateTo');
this.objectsContainer = document.getElementById('objectsContainer');
this.objectsCount = document.getElementById('objectsCount');
this.verticesCheckbox = document.getElementById('showVertices');
this.sidebarContent = document.querySelector('.sidebar-content');
setTimeout(() => this.createLayerGroups(), 100);
}
// Инициализация панели деталей
initializeDetailsPanel() {
this.detailsPanel = document.getElementById('object-details-panel');
if (!this.detailsPanel) {
console.warn('Панель деталей не найдена в DOM');
return;
}
}
// Привязка событий
bindEvents() {
this.searchInput.addEventListener('input', () => this.filterObjects());
this.dateFrom.addEventListener('change', () => this.filterObjects());
this.dateTo.addEventListener('change', () => this.filterObjects());
if (this.verticesCheckbox) {
this.verticesCheckbox.addEventListener('change', (e) => {
this.toggleVerticesVisibility(e.target.checked);
});
}
}
// Добавление растрового слоя
addRasterLayer(config, rasterObject) {
const rasterId = `raster_${Date.now()}`;
const rasterData = {
id: rasterId,
name: config.name,
type: 'raster',
object: rasterObject,
visible: config.visible || false,
layers: config.layers || []
};
this.objects.set(rasterId, rasterData);
const groupId = this.findRasterGroup(config.layers);
this.createRasterInGroup(rasterData, groupId);
this.updateObjectsCount();
this.updateAllGroupCounters();
}
// Получение иконки для типа объекта
getObjectIcon(geometryType) {
const icons = {
point: '📍',
linestring: '🛣️',
polygon: '🗺️',
multipoint: '🔍',
multilinestring: '🛣️',
multipolygon: '🗺️'
};
return icons[geometryType] || '📌';
}
// Переключение видимости объекта
toggleObjectVisibility(objectId, visible) {
const objectData = this.objects.get(objectId);
if (!objectData) return;
objectData.visible = visible;
if (objectData.type === 'raster') {
if (visible) {
this.mapApp.map.geoObjects.add(objectData.object);
} else {
this.mapApp.map.geoObjects.remove(objectData.object);
}
} else {
if (visible) {
this.mapApp.map.geoObjects.add(objectData.mapObject);
if (objectData.vertices && this.showVertices) {
this.mapApp.map.geoObjects.add(objectData.vertices);
}
} else {
this.mapApp.map.geoObjects.remove(objectData.mapObject);
if (objectData.vertices) {
this.mapApp.map.geoObjects.remove(objectData.vertices);
}
}
}
this.updateAllGroupCounters();
}
// Переключение видимости вершин
toggleVerticesVisibility(show) {
this.showVertices = show;
this.objects.forEach((objectData) => {
if (objectData.vertices && objectData.visible) {
if (show) {
this.mapApp.map.geoObjects.add(objectData.vertices);
} else {
this.mapApp.map.geoObjects.remove(objectData.vertices);
}
}
});
}
// Центрирование карты на объекте
centerOnObject(objectId) {
const objectData = this.objects.get(objectId);
if (!objectData) return;
if (objectData.type === 'raster') return;
const geometry = objectData.feature.geometry;
try {
switch (geometry.type.toLowerCase()) {
case 'point':
const pointCoords = CoordUtils.geoJsonToYandex(geometry.coordinates);
this.mapApp.map.setCenter(pointCoords, 18, { duration: 500 });
break;
case 'multipoint':
const multiPoints = geometry.coordinates.map(coord =>
CoordUtils.geoJsonToYandex(coord)
);
const centerMulti = this.calculateCenter(multiPoints);
this.mapApp.map.setCenter(centerMulti, 18, { duration: 500 });
break;
case 'linestring':
const linePoints = geometry.coordinates.map(coord =>
CoordUtils.geoJsonToYandex(coord)
);
const lineBounds = this.calculateBounds(linePoints);
this.mapApp.map.setBounds(lineBounds, { duration: 500 });
break;
case 'polygon':
const polyPoints = geometry.coordinates[0].map(coord =>
CoordUtils.geoJsonToYandex(coord)
);
const polyBounds = this.calculateBounds(polyPoints);
this.mapApp.map.setBounds(polyBounds, { duration: 500 });
break;
case 'multilinestring':
const multiLinePoints = geometry.coordinates.flat().map(coord =>
CoordUtils.geoJsonToYandex(coord)
);
const multiLineBounds = this.calculateBounds(multiLinePoints);
this.mapApp.map.setBounds(multiLineBounds, { duration: 500 });
break;
case 'multipolygon':
const multiPolyPoints = geometry.coordinates.flatMap(poly => poly[0]).map(coord =>
CoordUtils.geoJsonToYandex(coord)
);
const multiPolyBounds = this.calculateBounds(multiPolyPoints);
this.mapApp.map.setBounds(multiPolyBounds, { duration: 500 });
break;
default:
console.warn('Неизвестный тип геометрии:', geometry.type);
}
} catch (error) {
console.error('Ошибка центрирования на объекте:', error);
}
}
// Расчет центра для MultiPoint
calculateCenter(points) {
const sum = points.reduce((acc, point) => {
return [acc[0] + point[0], acc[1] + point[1]];
}, [0, 0]);
return [sum[0] / points.length, sum[1] / points.length];
}
// Расчет границ для LineString/Polygon
calculateBounds(points) {
const latitudes = points.map(p => p[0]);
const longitudes = points.map(p => p[1]);
return [
[Math.min(...latitudes), Math.min(...longitudes)],
[Math.max(...latitudes), Math.max(...longitudes)]
];
}
// Фильтрация объектов
filterObjects() {
const searchText = this.searchInput.value.toLowerCase();
const dateFrom = this.dateFrom.value;
const dateTo = this.dateTo.value;
this.objects.forEach((objectData, objectId) => {
if (objectData.type === 'raster') {
objectData.listItem.style.display = 'flex';
return;
}
const matchesSearch = this.matchesSearch(objectData, searchText);
const matchesDate = this.matchesDate(objectData, dateFrom, dateTo);
const shouldShow = matchesSearch && matchesDate;
objectData.listItem.style.display = shouldShow ? 'flex' : 'none';
if (!shouldShow && objectData.visible && objectData.checkbox.checked) {
this.toggleObjectVisibility(objectId, false);
} else if (shouldShow && objectData.visible && objectData.checkbox.checked) {
this.toggleObjectVisibility(objectId, true);
}
});
this.updateObjectsCount();
this.updateAllGroupCounters();
}
// Проверка соответствия поиску
matchesSearch(objectData, searchText) {
if (!searchText) return true;
const name = objectData.feature.properties.name || '';
const description = objectData.feature.properties.description || '';
const category = objectData.feature.properties.category || '';
return name.toLowerCase().includes(searchText) ||
description.toLowerCase().includes(searchText) ||
category.toLowerCase().includes(searchText);
}
// Проверка соответствия диапазону дат
matchesDate(objectData, dateFrom, dateTo) {
if (!dateFrom && !dateTo) return true;
const objectDate = objectData.feature.properties.dateCreated;
if (!objectDate) return true;
const objectDateObj = new Date(objectDate);
const fromDateObj = dateFrom ? new Date(dateFrom) : null;
const toDateObj = dateTo ? new Date(dateTo) : null;
if (fromDateObj && objectDateObj < fromDateObj) return false;
if (toDateObj && objectDateObj > toDateObj) return false;
return true;
}
// Обновление счетчика объектов
updateObjectsCount() {
const visibleCount = Array.from(this.objects.values()).filter(
obj => obj.listItem.style.display !== 'none' && obj.type !== 'raster'
).length;
this.objectsCount.textContent = visibleCount;
}
// Очистка всех объектов
clear() {
this.objects.forEach((objectData) => {
if (objectData.listItem) {
objectData.listItem.remove();
}
});
this.objects.clear();
this.updateObjectsCount();
}
// Применение начальной видимости
applyInitialVisibility() {
this.layerGroups.forEach((group, groupId) => {
this.toggleGroupVisibility(groupId, group.visible);
});
}
// ========== НОВЫЕ МЕТОДЫ ДЛЯ КОНТЕКСТНОГО САЙДБАРА ==========
// Показать детали объекта в сайдбаре
showObjectDetails(objectData) {
if (!this.detailsPanel || !this.sidebarContent) return;
this.currentObject = objectData;
const html = this.generateDetailsHTML(objectData);
this.detailsPanel.innerHTML = html;
this.detailsPanel.style.display = 'block';
this.sidebarContent.style.display = 'none';
const closeBtn = this.detailsPanel.querySelector('.close-details-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hideObjectDetails());
}
const panoramaBtn = this.detailsPanel.querySelector('.panorama-btn');
if (panoramaBtn) {
panoramaBtn.addEventListener('click', (e) => {
if (!panoramaBtn.href || panoramaBtn.href === '#') {
e.preventDefault();
console.log('Ссылка на панораму отсутствует');
}
});
}
}
// Сгенерировать HTML для деталей объекта
generateDetailsHTML(objectData) {
const feature = objectData.feature;
const properties = feature.properties || {};
const pointData = objectData.pointData || {};
let html = `
`;
// ★★★ ОПИСАНИЕ ★★★
if (properties.description) {
html += `
${properties.description}
`;
}
// ★★★ ДАТА СОЗДАНИЯ ★★★
if (properties.dateCreated) {
html += `
Дата: ${properties.dateCreated}
`;
}
// ★★★ КООРДИНАТЫ ★★★
if (pointData.lat && pointData.lon) {
const lat = pointData.lat;
const lon = pointData.lon;
html += `
📍 Координаты
Ш: ${lat.toFixed(6)}
Д: ${lon.toFixed(6)}
📋 Копировать координаты
`;
} else if (objectData.coordinates) {
// Если координаты переданы напрямую
const lat = objectData.coordinates[0];
const lon = objectData.coordinates[1];
html += `
📍 Координаты
Ш: ${lat.toFixed(6)}
Д: ${lon.toFixed(6)}
📋 Копировать координаты
`;
}
// ★★★ ССЫЛОЧНЫЕ АТРИБУТЫ (кнопки) ★★★
const linkAttributes = [];
if (pointData.attributes) {
Object.entries(pointData.attributes).forEach(([key, value]) => {
if (typeof value === 'string' &&
(value.startsWith('http://') || value.startsWith('https://'))) {
linkAttributes.push({ key, value });
}
});
}
if (linkAttributes.length > 0) {
html += ``;
linkAttributes.forEach((link, index) => {
html += `
${link.key}
`;
});
html += `
`;
}
// ★★★ ВСЕ АТРИБУТЫ ТОЧКИ ★★★
if (pointData.attributes && Object.keys(pointData.attributes).length > 0) {
html += `
📋 Атрибуты точки
`;
Object.entries(pointData.attributes).forEach(([key, value]) => {
// Пропускаем ссылки (они уже показаны как кнопки)
if (typeof value === 'string' &&
(value.startsWith('http://') || value.startsWith('https://'))) {
return;
}
html += `
`;
});
html += `
`;
}
return html;
}
// Скрыть детали объекта
hideObjectDetails() {
if (!this.detailsPanel || !this.sidebarContent) return;
// ★★★ ЗАКРЫВАЕМ БАЛУН ЯНДЕКСА ★★★
if (this.currentObject && this.currentObject.mapObject) {
this.currentObject.mapObject.balloon.close();
}
this.detailsPanel.style.display = 'none';
this.sidebarContent.style.display = 'block';
this.currentObject = null;
}
// Проверить, открыта ли панель деталей
isDetailsPanelOpen() {
return this.detailsPanel &&
this.detailsPanel.style.display === 'block' &&
this.currentObject !== null;
}
}