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 = `
${group.name} (${layersCount})
`; 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 = `

${properties.name || 'Объект'}

`; // ★★★ ОПИСАНИЕ ★★★ 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 += ``; } // ★★★ ВСЕ АТРИБУТЫ ТОЧКИ ★★★ 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 += `
${key}:
${value}
`; }); 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; } }