Добавил объединение источников. Вернул норм карту. Удалил ненужные либы
This commit is contained in:
@@ -97,6 +97,22 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add to List Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-success btn-sm" type="button" onclick="addSelectedToList()">
|
||||
<i class="bi bi-plus-circle"></i> Добавить к
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selected Sources Counter Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#selectedSourcesOffcanvas" aria-controls="selectedSourcesOffcanvas">
|
||||
<i class="bi bi-list-check"></i> Список
|
||||
<span id="selectedSourcesCounter" class="badge bg-info" style="display: none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
@@ -106,23 +122,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Polygon Filter Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-success btn-sm" type="button"
|
||||
onclick="openPolygonFilterMap()">
|
||||
<i class="bi bi-pentagon"></i> Фильтр по полигону
|
||||
{% if polygon_coords %}
|
||||
<span class="badge bg-success">✓</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% if polygon_coords %}
|
||||
<button class="btn btn-outline-danger btn-sm" type="button"
|
||||
onclick="clearPolygonFilter()" title="Очистить фильтр по полигону">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Column visibility toggle button -->
|
||||
<div>
|
||||
<div class="dropdown">
|
||||
@@ -182,6 +181,30 @@
|
||||
<input type="hidden" name="polygon" value="{{ polygon_coords }}">
|
||||
{% endif %}
|
||||
|
||||
<!-- Polygon Filter Section -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">
|
||||
<i class="bi bi-pentagon"></i> Фильтр по полигону
|
||||
</label>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-success btn-sm"
|
||||
onclick="openPolygonFilterMap()">
|
||||
<i class="bi bi-pentagon"></i> Нарисовать полигон
|
||||
{% if polygon_coords %}
|
||||
<span class="badge bg-success ms-1">✓ Активен</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% if polygon_coords %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
onclick="clearPolygonFilter()" title="Очистить фильтр по полигону">
|
||||
<i class="bi bi-x-circle"></i> Очистить полигон
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
@@ -1165,8 +1188,95 @@ function toggleColumnWithoutSave(checkbox) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize selected sources array from localStorage
|
||||
function loadSelectedSourcesFromStorage() {
|
||||
try {
|
||||
const storedSources = localStorage.getItem('selectedSources');
|
||||
if (storedSources) {
|
||||
window.selectedSources = JSON.parse(storedSources);
|
||||
} else {
|
||||
window.selectedSources = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading selected sources from storage:', e);
|
||||
window.selectedSources = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to save selected sources to localStorage
|
||||
window.saveSelectedSourcesToStorage = function () {
|
||||
try {
|
||||
localStorage.setItem('selectedSources', JSON.stringify(window.selectedSources));
|
||||
} catch (e) {
|
||||
console.error('Error saving selected sources to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update the selected sources counter
|
||||
window.updateSelectedSourcesCounter = function () {
|
||||
const counterElement = document.getElementById('selectedSourcesCounter');
|
||||
if (window.selectedSources && window.selectedSources.length > 0) {
|
||||
counterElement.textContent = window.selectedSources.length;
|
||||
counterElement.style.display = 'inline';
|
||||
} else {
|
||||
counterElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add selected sources to the list
|
||||
window.addSelectedToList = function () {
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для добавления в список');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the data for each selected row and add to the selectedSources array
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
const row = checkbox.closest('tr');
|
||||
const sourceId = checkbox.value;
|
||||
|
||||
const sourceExists = window.selectedSources.some(source => source.id === sourceId);
|
||||
if (!sourceExists) {
|
||||
const rowData = {
|
||||
id: sourceId,
|
||||
name: row.cells[2].textContent.trim(),
|
||||
satellite: row.cells[3].textContent.trim(),
|
||||
info: row.cells[4].textContent.trim(),
|
||||
ownership: row.cells[5].textContent.trim(),
|
||||
coords_average: row.cells[6].textContent.trim(),
|
||||
objitem_count: row.cells[7].textContent.trim()
|
||||
};
|
||||
|
||||
window.selectedSources.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
// Update the counter
|
||||
updateSelectedSourcesCounter();
|
||||
|
||||
// Save selected sources to localStorage
|
||||
saveSelectedSourcesToStorage();
|
||||
|
||||
// Clear selections in the main table
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
updateRowHighlight(checkbox);
|
||||
});
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load selected sources from localStorage
|
||||
loadSelectedSourcesFromStorage();
|
||||
updateSelectedSourcesCounter();
|
||||
|
||||
// Setup select-all checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
@@ -1227,8 +1337,221 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Initialize column visibility
|
||||
setTimeout(initColumnVisibility, 100);
|
||||
|
||||
// Update the selected sources table when the offcanvas is shown
|
||||
const selectedSourcesOffcanvas = document.getElementById('selectedSourcesOffcanvas');
|
||||
if (selectedSourcesOffcanvas) {
|
||||
selectedSourcesOffcanvas.addEventListener('show.bs.offcanvas', function () {
|
||||
populateSelectedSourcesTable();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to populate the selected sources table in the offcanvas
|
||||
function populateSelectedSourcesTable() {
|
||||
const tableBody = document.getElementById('selected-sources-table-body');
|
||||
const noDataDiv = document.getElementById('selectedSourcesNoData');
|
||||
const table = tableBody.closest('.table-responsive');
|
||||
const offcanvasCounter = document.getElementById('selectedSourcesOffcanvasCounter');
|
||||
|
||||
if (!tableBody) return;
|
||||
|
||||
// Clear existing rows
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (!window.selectedSources || window.selectedSources.length === 0) {
|
||||
// Show no data message
|
||||
if (table) table.style.display = 'none';
|
||||
if (noDataDiv) noDataDiv.style.display = 'block';
|
||||
if (offcanvasCounter) offcanvasCounter.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide no data message and show table
|
||||
if (table) table.style.display = 'block';
|
||||
if (noDataDiv) noDataDiv.style.display = 'none';
|
||||
if (offcanvasCounter) offcanvasCounter.textContent = window.selectedSources.length;
|
||||
|
||||
// Add rows for each selected source
|
||||
window.selectedSources.forEach((source, index) => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input selected-source-checkbox" value="${source.id}">
|
||||
</td>
|
||||
<td>${source.id}</td>
|
||||
<td>${source.name}</td>
|
||||
<td>${source.satellite}</td>
|
||||
<td>${source.info}</td>
|
||||
<td>${source.ownership}</td>
|
||||
<td>${source.coords_average}</td>
|
||||
<td class="text-center">${source.objitem_count}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to remove selected sources from the list
|
||||
function removeSelectedSources() {
|
||||
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox:checked');
|
||||
if (checkboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для удаления из списка');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get IDs of sources to remove
|
||||
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
|
||||
|
||||
// Remove sources from the selectedSources array
|
||||
window.selectedSources = window.selectedSources.filter(source => !idsToRemove.includes(source.id));
|
||||
|
||||
// Save selected sources to localStorage
|
||||
saveSelectedSourcesToStorage();
|
||||
|
||||
// Update the counter and table
|
||||
updateSelectedSourcesCounter();
|
||||
populateSelectedSourcesTable();
|
||||
}
|
||||
|
||||
// Function to show selected sources on map
|
||||
function showSelectedSourcesOnMap() {
|
||||
if (!window.selectedSources || window.selectedSources.length === 0) {
|
||||
alert('Список источников пуст');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = window.selectedSources.map(source => source.id);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const polygonParam = urlParams.get('polygon');
|
||||
|
||||
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||||
if (polygonParam) {
|
||||
url += '&polygon=' + encodeURIComponent(polygonParam);
|
||||
}
|
||||
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// Function to merge selected sources
|
||||
function mergeSelectedSources() {
|
||||
if (!window.selectedSources || window.selectedSources.length < 2) {
|
||||
alert('Для объединения необходимо выбрать минимум 2 источника');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show merge modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('mergeSourcesModal'));
|
||||
|
||||
// Populate target source info
|
||||
const targetSource = window.selectedSources[0];
|
||||
document.getElementById('targetSourceInfo').innerHTML = `
|
||||
<strong>ID:</strong> ${targetSource.id}<br>
|
||||
<strong>Имя:</strong> ${targetSource.name}<br>
|
||||
<strong>Спутник:</strong> ${targetSource.satellite}<br>
|
||||
<strong>Количество точек:</strong> ${targetSource.objitem_count}
|
||||
`;
|
||||
|
||||
// Populate sources to merge list
|
||||
const sourcesToMergeList = document.getElementById('sourcesToMergeList');
|
||||
sourcesToMergeList.innerHTML = '';
|
||||
for (let i = 1; i < window.selectedSources.length; i++) {
|
||||
const source = window.selectedSources[i];
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item';
|
||||
li.innerHTML = `
|
||||
<strong>ID ${source.id}:</strong> ${source.name}
|
||||
<span class="badge bg-secondary">${source.objitem_count} точек</span>
|
||||
`;
|
||||
sourcesToMergeList.appendChild(li);
|
||||
}
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Function to confirm merge
|
||||
function confirmMerge() {
|
||||
const infoId = document.getElementById('mergeInfoSelect').value;
|
||||
const ownershipId = document.getElementById('mergeOwnershipSelect').value;
|
||||
const note = document.getElementById('mergeNoteTextarea').value;
|
||||
|
||||
if (!infoId) {
|
||||
alert('Пожалуйста, выберите тип объекта');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ownershipId) {
|
||||
alert('Пожалуйста, выберите принадлежность объекта');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare data
|
||||
const sourceIds = window.selectedSources.map(source => source.id);
|
||||
|
||||
// Get CSRF token
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
// Send AJAX request
|
||||
fetch('{% url "mainapp:merge_sources" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrftoken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source_ids: sourceIds,
|
||||
info_id: infoId,
|
||||
ownership_id: ownershipId,
|
||||
note: note
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
|
||||
// Clear selected sources
|
||||
window.selectedSources = [];
|
||||
saveSelectedSourcesToStorage();
|
||||
updateSelectedSourcesCounter();
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('mergeSourcesModal'));
|
||||
modal.hide();
|
||||
|
||||
// Reload page
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при объединении источников');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to toggle all checkboxes in the selected sources table
|
||||
function toggleAllSelectedSources(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
}
|
||||
|
||||
// Show source details in modal
|
||||
function showSourceDetails(sourceId) {
|
||||
// Update modal title
|
||||
@@ -1724,6 +2047,132 @@ function showTransponderModal(transponderId) {
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
<!-- Selected Sources Offcanvas -->
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="selectedSourcesOffcanvas" aria-labelledby="selectedSourcesOffcanvasLabel" style="width: 80%;">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="selectedSourcesOffcanvasLabel">
|
||||
Выбранные источники
|
||||
<span class="badge bg-info" id="selectedSourcesOffcanvasCounter">0</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<div class="mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="showSelectedSourcesOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm" onclick="mergeSelectedSources()">
|
||||
<i class="bi bi-union"></i> Объединить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeSelectedSources()">
|
||||
<i class="bi bi-trash"></i> Удалить из списка
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm table-bordered">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all-sources" class="form-check-input" onchange="toggleAllSelectedSources(this)">
|
||||
</th>
|
||||
<th class="text-center" style="min-width: 60px;">ID</th>
|
||||
<th style="min-width: 150px;">Имя</th>
|
||||
<th style="min-width: 120px;">Спутник</th>
|
||||
<th style="min-width: 120px;">Тип объекта</th>
|
||||
<th style="min-width: 150px;">Принадлежность</th>
|
||||
<th style="min-width: 150px;">Координаты ГЛ</th>
|
||||
<th class="text-center" style="min-width: 100px;">Кол-во точек</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="selected-sources-table-body">
|
||||
<!-- Data will be loaded here via JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="selectedSourcesNoData" class="text-center text-muted py-4" style="display: none;">
|
||||
Список пуст. Добавьте источники из основной таблицы.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Merge Sources Modal -->
|
||||
<div class="modal fade" id="mergeSourcesModal" tabindex="-1" aria-labelledby="mergeSourcesModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title" id="mergeSourcesModalLabel">
|
||||
<i class="bi bi-union"></i> Объединение источников
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Внимание:</strong> Все точки из выбранных источников будут присвоены первому источнику в списке.
|
||||
Остальные источники будут удалены.
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<strong>Целевой источник (все точки будут присвоены ему):</strong>
|
||||
</div>
|
||||
<div class="card-body" id="targetSourceInfo">
|
||||
<!-- Target source info will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<strong>Источники для объединения (будут удалены):</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group" id="sourcesToMergeList">
|
||||
<!-- Sources to merge will be populated here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="mergeInfoSelect" class="form-label">Тип объекта <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="mergeInfoSelect" required>
|
||||
<option value="">Выберите тип объекта</option>
|
||||
{% for info in object_infos %}
|
||||
<option value="{{ info.id }}">{{ info.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="mergeOwnershipSelect" class="form-label">Принадлежность объекта <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="mergeOwnershipSelect" required>
|
||||
<option value="">Выберите принадлежность</option>
|
||||
{% for ownership in object_ownerships %}
|
||||
<option value="{{ ownership.id }}">{{ ownership.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="mergeNoteTextarea" class="form-label">Примечание</label>
|
||||
<textarea class="form-control" id="mergeNoteTextarea" rows="3" placeholder="Введите примечание (необязательно)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-success" onclick="confirmMerge()">
|
||||
<i class="bi bi-check-circle"></i> Объединить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Polygon Filter Map Modal -->
|
||||
<div class="modal fade" id="polygonFilterModal" tabindex="-1" aria-labelledby="polygonFilterModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
|
||||
@@ -3,169 +3,46 @@
|
||||
{% block title %}Карта объектов{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- MapLibre GL CSS -->
|
||||
<link href="{% static 'maplibre/maplibre-gl.css' %}" rel="stylesheet">
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Легенда */
|
||||
.maplibregl-ctrl-legend {
|
||||
.legend {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
font-size: 11px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-legend h6 {
|
||||
.legend h6 {
|
||||
font-size: 12px;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
margin: 4px 0;
|
||||
margin: 3px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.legend-marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.legend-section {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.legend-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Слои контрол */
|
||||
.maplibregl-ctrl-layers {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 250px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-layers h6 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
margin: 5px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.layer-item label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.layer-item input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Кастомные кнопки контролов */
|
||||
.maplibregl-ctrl-projection .maplibregl-ctrl-icon,
|
||||
.maplibregl-ctrl-3d .maplibregl-ctrl-icon,
|
||||
.maplibregl-ctrl-style .maplibregl-ctrl-icon {
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
margin-right: 6px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-projection .maplibregl-ctrl-icon {
|
||||
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="6" fill="none" stroke="%23333" stroke-width="1.5"/><ellipse cx="10" cy="10" rx="3" ry="6" fill="none" stroke="%23333" stroke-width="1.5"/><line x1="4" y1="10" x2="16" y2="10" stroke="%23333" stroke-width="1.5"/></svg>');
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-3d .maplibregl-ctrl-icon {
|
||||
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path d="M 5 13 L 5 8 L 10 5 L 15 8 L 15 13 L 10 16 Z M 5 8 L 10 10.5 M 10 10.5 L 15 8 M 10 10.5 L 10 16" fill="none" stroke="%23333" stroke-width="1.5" stroke-linejoin="round"/></svg>');
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-style .maplibregl-ctrl-icon {
|
||||
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="3" y="3" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="11" y="3" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="3" y="11" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="11" y="11" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/></svg>');
|
||||
}
|
||||
|
||||
/* Popup стили */
|
||||
.maplibregl-popup-content {
|
||||
padding: 10px 15px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.popup-info {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Стили меню */
|
||||
.style-menu {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
||||
padding: 5px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.style-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.style-menu button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.style-menu button.active {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -174,508 +51,181 @@
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- MapLibre GL JavaScript -->
|
||||
<script src="{% static 'maplibre/maplibre-gl.js' %}"></script>
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Цвета для маркеров
|
||||
const markerColors = {
|
||||
'blue': '#3388ff',
|
||||
'orange': '#ff8c00',
|
||||
'green': '#28a745',
|
||||
'violet': '#9c27b0'
|
||||
};
|
||||
|
||||
// Данные групп из Django
|
||||
const groups = [
|
||||
{% for group in groups %}
|
||||
{
|
||||
name: '{{ group.name|escapejs }}',
|
||||
color: '{{ group.color }}',
|
||||
points: [
|
||||
{% for point_data in group.points %}
|
||||
{
|
||||
id: '{{ point_data.source_id|escapejs }}',
|
||||
coordinates: [{{ point_data.point.0|safe }}, {{ point_data.point.1|safe }}]
|
||||
}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
// Полигон фильтра
|
||||
const polygonCoords = {% if polygon_coords %}{{ polygon_coords|safe }}{% else %}null{% endif %};
|
||||
|
||||
// Стили карт
|
||||
const mapStyles = {
|
||||
streets: {
|
||||
version: 8,
|
||||
sources: {
|
||||
'osm': {
|
||||
type: 'raster',
|
||||
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenStreetMap'
|
||||
}
|
||||
},
|
||||
layers: [{
|
||||
id: 'osm',
|
||||
type: 'raster',
|
||||
source: 'osm'
|
||||
}]
|
||||
},
|
||||
satellite: {
|
||||
version: 8,
|
||||
sources: {
|
||||
'satellite': {
|
||||
type: 'raster',
|
||||
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
||||
tileSize: 256,
|
||||
attribution: '© Esri'
|
||||
}
|
||||
},
|
||||
layers: [{
|
||||
id: 'satellite',
|
||||
type: 'raster',
|
||||
source: 'satellite'
|
||||
}]
|
||||
},
|
||||
local: {
|
||||
version: 8,
|
||||
sources: {
|
||||
'local': {
|
||||
type: 'raster',
|
||||
tiles: ['http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: 'Local'
|
||||
}
|
||||
},
|
||||
layers: [{
|
||||
id: 'local',
|
||||
type: 'raster',
|
||||
source: 'local'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
// Инициализация карты
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: mapStyles.streets,
|
||||
center: [37.62, 55.75],
|
||||
zoom: 10,
|
||||
maxZoom: 18,
|
||||
minZoom: 0
|
||||
let map = L.map('map').setView([55.75, 37.62], 10);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
let currentStyle = 'streets';
|
||||
let is3DEnabled = false;
|
||||
let isGlobeProjection = false;
|
||||
const allMarkers = [];
|
||||
|
||||
// Добавляем стандартные контролы
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
|
||||
map.addControl(new maplibregl.FullscreenControl(), 'top-right');
|
||||
|
||||
// Кастомный контрол для переключения проекции
|
||||
class ProjectionControl {
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-projection';
|
||||
this._container.innerHTML = '<button type="button" title="Переключить проекцию"><span class="maplibregl-ctrl-icon"></span></button>';
|
||||
this._container.onclick = () => {
|
||||
if (isGlobeProjection) {
|
||||
map.setProjection({ type: 'mercator' });
|
||||
isGlobeProjection = false;
|
||||
} else {
|
||||
map.setProjection({ type: 'globe' });
|
||||
isGlobeProjection = true;
|
||||
}
|
||||
};
|
||||
return this._container;
|
||||
}
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Кастомный контрол для переключения стилей
|
||||
class StyleControl {
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-style';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.title = 'Стиль карты';
|
||||
button.innerHTML = '<span class="maplibregl-ctrl-icon"></span>';
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'style-menu';
|
||||
menu.style.display = 'none';
|
||||
menu.style.position = 'absolute';
|
||||
menu.style.top = '100%';
|
||||
menu.style.right = '0';
|
||||
menu.style.marginTop = '5px';
|
||||
|
||||
const styles = [
|
||||
{ id: 'streets', name: 'Улицы' },
|
||||
{ id: 'satellite', name: 'Спутник' },
|
||||
{ id: 'local', name: 'Локально' }
|
||||
];
|
||||
|
||||
styles.forEach(style => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = style.name;
|
||||
btn.className = style.id === currentStyle ? 'active' : '';
|
||||
btn.onclick = () => {
|
||||
this.switchStyle(style.id);
|
||||
menu.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
menu.style.display = 'none';
|
||||
};
|
||||
menu.appendChild(btn);
|
||||
});
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
menu.style.display = 'none';
|
||||
});
|
||||
|
||||
this._container.appendChild(button);
|
||||
this._container.appendChild(menu);
|
||||
this._container.style.position = 'relative';
|
||||
|
||||
return this._container;
|
||||
}
|
||||
|
||||
switchStyle(styleName) {
|
||||
const center = this._map.getCenter();
|
||||
const zoom = this._map.getZoom();
|
||||
const bearing = this._map.getBearing();
|
||||
const pitch = this._map.getPitch();
|
||||
|
||||
this._map.setStyle(mapStyles[styleName]);
|
||||
currentStyle = styleName;
|
||||
|
||||
this._map.once('styledata', () => {
|
||||
this._map.setCenter(center);
|
||||
this._map.setZoom(zoom);
|
||||
this._map.setBearing(bearing);
|
||||
this._map.setPitch(pitch);
|
||||
|
||||
addMarkersToMap();
|
||||
addFilterPolygon();
|
||||
});
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Кастомный контрол для слоев
|
||||
class LayersControl {
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-layers';
|
||||
|
||||
const title = document.createElement('h6');
|
||||
title.textContent = 'Слои точек';
|
||||
this._container.appendChild(title);
|
||||
|
||||
groups.forEach((group, groupIndex) => {
|
||||
const layerId = `points-layer-${groupIndex}`;
|
||||
|
||||
const layerItem = document.createElement('div');
|
||||
layerItem.className = 'layer-item';
|
||||
|
||||
const label = document.createElement('label');
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = true;
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const visibility = e.target.checked ? 'visible' : 'none';
|
||||
if (map.getLayer(layerId)) {
|
||||
map.setLayoutProperty(layerId, 'visibility', visibility);
|
||||
}
|
||||
// Также скрываем/показываем внутренний круг
|
||||
const innerLayerId = `${layerId}-inner`;
|
||||
if (map.getLayer(innerLayerId)) {
|
||||
map.setLayoutProperty(innerLayerId, 'visibility', visibility);
|
||||
}
|
||||
});
|
||||
|
||||
const colorSpan = document.createElement('span');
|
||||
colorSpan.className = 'legend-marker';
|
||||
colorSpan.style.backgroundColor = markerColors[group.color];
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.textContent = `${group.name} (${group.points.length})`;
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(colorSpan);
|
||||
label.appendChild(nameSpan);
|
||||
layerItem.appendChild(label);
|
||||
this._container.appendChild(layerItem);
|
||||
});
|
||||
|
||||
return this._container;
|
||||
}
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Кастомный контрол для легенды
|
||||
class LegendControl {
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-legend';
|
||||
|
||||
const title = document.createElement('h6');
|
||||
title.textContent = 'Легенда';
|
||||
this._container.appendChild(title);
|
||||
|
||||
groups.forEach(group => {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'legend-section';
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'legend-item';
|
||||
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'legend-marker';
|
||||
marker.style.backgroundColor = markerColors[group.color];
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = `${group.name} (${group.points.length})`;
|
||||
|
||||
item.appendChild(marker);
|
||||
item.appendChild(text);
|
||||
section.appendChild(item);
|
||||
this._container.appendChild(section);
|
||||
});
|
||||
|
||||
if (polygonCoords && polygonCoords.length > 0) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'legend-section';
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'legend-item';
|
||||
|
||||
const marker = document.createElement('div');
|
||||
marker.style.width = '18px';
|
||||
marker.style.height = '18px';
|
||||
marker.style.marginRight = '8px';
|
||||
marker.style.backgroundColor = 'rgba(51, 136, 255, 0.2)';
|
||||
marker.style.border = '2px solid #3388ff';
|
||||
marker.style.borderRadius = '2px';
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = 'Область фильтра';
|
||||
|
||||
item.appendChild(marker);
|
||||
item.appendChild(text);
|
||||
section.appendChild(item);
|
||||
this._container.appendChild(section);
|
||||
}
|
||||
|
||||
return this._container;
|
||||
}
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем кастомные контролы
|
||||
map.addControl(new ProjectionControl(), 'top-right');
|
||||
map.addControl(new StyleControl(), 'top-right');
|
||||
map.addControl(new LayersControl(), 'top-left');
|
||||
map.addControl(new LegendControl(), 'bottom-left');
|
||||
|
||||
// Добавление маркеров на карту
|
||||
function addMarkersToMap() {
|
||||
groups.forEach((group, groupIndex) => {
|
||||
const sourceId = `points-${groupIndex}`;
|
||||
const layerId = `points-layer-${groupIndex}`;
|
||||
|
||||
// Создаем GeoJSON для группы
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
features: group.points.map(point => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: point.coordinates
|
||||
},
|
||||
properties: {
|
||||
id: point.id,
|
||||
groupName: group.name,
|
||||
color: markerColors[group.color]
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
// Добавляем источник данных
|
||||
if (!map.getSource(sourceId)) {
|
||||
map.addSource(sourceId, {
|
||||
type: 'geojson',
|
||||
data: geojson
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем слой с кругами (основной маркер)
|
||||
if (!map.getLayer(layerId)) {
|
||||
map.addLayer({
|
||||
id: layerId,
|
||||
type: 'circle',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'circle-radius': 10,
|
||||
'circle-color': ['get', 'color'],
|
||||
'circle-stroke-width': 3,
|
||||
'circle-stroke-color': '#ffffff',
|
||||
'circle-opacity': 1
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем внутренний круг
|
||||
map.addLayer({
|
||||
id: `${layerId}-inner`,
|
||||
type: 'circle',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'circle-radius': 4,
|
||||
'circle-color': '#ffffff',
|
||||
'circle-opacity': 1
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем popup при клике
|
||||
map.on('click', layerId, (e) => {
|
||||
const coordinates = e.features[0].geometry.coordinates.slice();
|
||||
const { id, groupName } = e.features[0].properties;
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(`<div class="popup-title">${id}</div><div class="popup-info">Группа: ${groupName}</div>`)
|
||||
.addTo(map);
|
||||
});
|
||||
|
||||
// Меняем курсор при наведении
|
||||
map.on('mouseenter', layerId, () => {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
map.on('mouseleave', layerId, () => {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
}
|
||||
// Цвета для маркеров
|
||||
var markerColors = {
|
||||
'blue': 'blue',
|
||||
'orange': 'orange',
|
||||
'green': 'green',
|
||||
'violet': 'violet'
|
||||
};
|
||||
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Добавление полигона фильтра
|
||||
function addFilterPolygon() {
|
||||
if (!polygonCoords || polygonCoords.length === 0) return;
|
||||
|
||||
try {
|
||||
const polygonGeoJSON = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygonCoords]
|
||||
}
|
||||
};
|
||||
|
||||
if (!map.getSource('filter-polygon')) {
|
||||
map.addSource('filter-polygon', {
|
||||
type: 'geojson',
|
||||
data: polygonGeoJSON
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getLayer('filter-polygon-fill')) {
|
||||
map.addLayer({
|
||||
id: 'filter-polygon-fill',
|
||||
type: 'fill',
|
||||
source: 'filter-polygon',
|
||||
paint: {
|
||||
'fill-color': '#3388ff',
|
||||
'fill-opacity': 0.2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getLayer('filter-polygon-outline')) {
|
||||
map.addLayer({
|
||||
id: 'filter-polygon-outline',
|
||||
type: 'line',
|
||||
source: 'filter-polygon',
|
||||
paint: {
|
||||
'line-color': '#3388ff',
|
||||
'line-width': 2,
|
||||
'line-dasharray': [2, 2]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map.on('click', 'filter-polygon-fill', (e) => {
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML('<div class="popup-title">Область фильтра</div><div class="popup-info">Отображаются только источники с точками в этой области</div>')
|
||||
.addTo(map);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Ошибка при отображении полигона фильтра:', e);
|
||||
}
|
||||
}
|
||||
var overlays = [];
|
||||
|
||||
// Подгонка карты под все маркеры
|
||||
function fitMapToBounds() {
|
||||
const allCoordinates = [];
|
||||
|
||||
groups.forEach(group => {
|
||||
group.points.forEach(point => {
|
||||
allCoordinates.push(point.coordinates);
|
||||
// Создаём слои для каждого типа координат
|
||||
{% for group in groups %}
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = '{{ group.color }}';
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ point_data.source_id|escapejs }}";
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName);
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.source_id|escapejs }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: groupName,
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Добавляем легенду в левый нижний угол
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
||||
|
||||
{% for group in groups %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
|
||||
<span>{{ group.name|escapejs }}</span>
|
||||
</div>
|
||||
`;
|
||||
{% endfor %}
|
||||
|
||||
{% if polygon_coords %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd;">
|
||||
<div style="width: 18px; height: 18px; margin-right: 6px; background-color: rgba(51, 136, 255, 0.2); border: 2px solid #3388ff;"></div>
|
||||
<span>Область фильтра</span>
|
||||
</div>
|
||||
`;
|
||||
{% endif %}
|
||||
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
|
||||
// Добавляем полигон фильтра на карту, если он есть
|
||||
{% if polygon_coords %}
|
||||
try {
|
||||
const polygonCoords = {{ polygon_coords|safe }};
|
||||
if (polygonCoords && polygonCoords.length > 0) {
|
||||
polygonCoords.forEach(coord => {
|
||||
allCoordinates.push(coord);
|
||||
});
|
||||
}
|
||||
|
||||
if (allCoordinates.length > 0) {
|
||||
const bounds = allCoordinates.reduce((bounds, coord) => {
|
||||
return bounds.extend(coord);
|
||||
}, new maplibregl.LngLatBounds(allCoordinates[0], allCoordinates[0]));
|
||||
// Преобразуем координаты из [lng, lat] в [lat, lng] для Leaflet
|
||||
const latLngs = polygonCoords.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
map.fitBounds(bounds, { padding: 50 });
|
||||
// Создаем полигон
|
||||
const filterPolygon = L.polygon(latLngs, {
|
||||
color: '#3388ff',
|
||||
fillColor: '#3388ff',
|
||||
fillOpacity: 0.2,
|
||||
weight: 2,
|
||||
dashArray: '5, 5'
|
||||
});
|
||||
|
||||
// Добавляем полигон на карту
|
||||
filterPolygon.addTo(map);
|
||||
|
||||
// Добавляем popup с информацией
|
||||
filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только источники с точками в этой области');
|
||||
|
||||
// Если нет других точек, центрируем карту на полигоне
|
||||
{% if not groups %}
|
||||
map.fitBounds(filterPolygon.getBounds());
|
||||
{% endif %}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка при отображении полигона фильтра:', e);
|
||||
}
|
||||
|
||||
// Инициализация после загрузки карты
|
||||
map.on('load', () => {
|
||||
addMarkersToMap();
|
||||
addFilterPolygon();
|
||||
fitMapToBounds();
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
{% endblock extra_js %}
|
||||
Reference in New Issue
Block a user