Files
dbstorage/dbapp/mainapp/templates/mainapp/source_with_points_map.html

728 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Карта объекта #{{ source_id }} с точками{% endblock title %}
{% block extra_css %}
<!-- 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">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-playback@1.0.2/dist/LeafletPlayback.css" />
<style>
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px; /* Высота navbar */
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
.legend {
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
font-size: 11px;
}
.legend h6 {
font-size: 12px;
margin: 0 0 6px 0;
}
.legend-item {
margin: 3px 0;
display: flex;
align-items: center;
}
.legend-marker {
width: 18px;
height: 30px;
margin-right: 6px;
background-size: contain;
background-repeat: no-repeat;
}
.legend-section {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #ddd;
}
.legend-section:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.legend-section-title {
font-weight: bold;
font-size: 11px;
margin-bottom: 4px;
}
.playback-control {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 15px;
}
.playback-control button {
padding: 6px 12px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 13px;
min-width: auto;
}
.playback-control button:hover {
background: #0056b3;
}
.playback-control button:disabled {
background: #ccc;
cursor: not-allowed;
}
.playback-control .time-display {
font-size: 13px;
font-weight: bold;
min-width: 160px;
text-align: center;
}
.playback-control input[type="range"] {
width: 250px;
}
.playback-control .speed-control {
display: flex;
align-items: center;
gap: 6px;
}
.playback-control .speed-control label {
font-size: 11px;
margin: 0;
}
.playback-control .speed-control select {
padding: 3px 6px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 12px;
}
.playback-control .visibility-controls {
display: flex;
flex-direction: column;
gap: 4px;
border-left: 1px solid #ddd;
padding-left: 15px;
margin-left: 10px;
}
.playback-control .visibility-controls label {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
margin: 0;
cursor: pointer;
}
.playback-control .visibility-controls input[type="checkbox"] {
cursor: pointer;
}
</style>
{% endblock %}
{% block content %}
<div id="map"></div>
<div class="playback-control" id="playbackControl" style="display: none;">
<button id="playBtn"></button>
<button id="pauseBtn" disabled></button>
<button id="resetBtn"></button>
<input type="range" id="timeSlider" min="0" max="100" value="0" step="1">
<div class="time-display" id="timeDisplay">--</div>
<div class="speed-control">
<label for="speedSelect">Скорость:</label>
<select id="speedSelect">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
<option value="5">5x</option>
<option value="10">10x</option>
</select>
</div>
<div class="visibility-controls">
<label>
<input type="checkbox" id="showPointsCheckbox" checked>
Точки
</label>
<label>
<input type="checkbox" id="showTrackCheckbox" checked>
Трек
</label>
</div>
</div>
{% endblock content %}
{% block extra_js %}
<!-- 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 src="{% static 'leaflet-polylineDecorator/leaflet.polylineDecorator.js' %}"></script>
<script src="{% static 'leaflet-playback/leaflet-playback.js' %}"></script>
<script>
// Инициализация карты
let map = L.map('map').setView([55.75, 37.62], 5);
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: '&copy; <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 &copy; Esri'
});
const street_local = L.tileLayer('http://127.0.0.1:8090/styles/basic-preview/512/{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);
// Функция для создания иконки
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]
});
};
var sourceOverlays = [];
var glPointLayers = [];
var glPointCoordinates = [];
var glPointsData = [];
var trackLayer = L.layerGroup(); // Layer group for track and related elements
var glPointsGroupLayer = L.layerGroup(); // Layer group specifically for GL points
// Сначала собираем все данные о точках ГЛ с временными метками
{% for group in groups %}
{% for point_data in group.points %}
{% if not point_data.source_id %}
glPointsData.push({
name: "{{ point_data.name|escapejs }}",
frequency: "{{ point_data.frequency|escapejs }}",
coords: [{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}],
timestamp: {% if point_data.timestamp %}"{{ point_data.timestamp|escapejs }}"{% else %}null{% endif %}
});
{% endif %}
{% endfor %}
{% endfor %}
// Фильтруем точки с временными метками и сортируем
var glPointsWithTime = glPointsData.filter(p => p.timestamp !== null);
glPointsWithTime.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
// Создаём слои для координат объекта и точек ГЛ
{% for group in groups %}
var groupName = '{{ group.name|escapejs }}';
var colorName = '{{ group.color }}';
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
{% for point_data in group.points %}
{% if point_data.source_id %}
// Это координата объекта
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);
{% else %}
// Это точка ГЛ
var pointName = "{{ point_data.name|escapejs }}";
var pointCoords = [{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}];
var pointNumber = {{ forloop.counter }};
// Определяем цвет маркера: первый - зеленый, последний - оранжевый, остальные - обычный
var markerIcon;
if (pointNumber === 1) {
markerIcon = getColorIcon('green');
} else if (pointNumber === glPointsData.length) {
markerIcon = getColorIcon('orange');
} else {
markerIcon = groupIcon;
}
var marker = L.marker(pointCoords, {
icon: markerIcon
}).bindPopup(pointNumber + '. ' + pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
groupLayer.addLayer(marker);
glPointsGroupLayer.addLayer(marker); // Also add to GL points group layer
// Сохраняем координаты для построения трека
glPointCoordinates.push(pointCoords);
// Добавляем каждую точку ГЛ отдельно в список
glPointLayers.push({
label: pointNumber + " - {{ point_data.name|escapejs }} ({{ point_data.frequency|escapejs }})",
layer: marker
});
{% endif %}
{% endfor %}
// Для координат объекта добавляем как отдельный слой без вложенности
{% if group.color in 'blue,orange,green,violet' %}
sourceOverlays.push({
label: groupName,
layer: groupLayer
});
{% endif %}
// НЕ добавляем слой группы на карту при загрузке - будет управляться через layer control
{% if group.color in 'blue,orange,green,violet' %}
// groupLayer.addTo(map); // Закомментировано - слои скрыты по умолчанию
{% endif %}
{% endfor %}
// Создаём трек между точками ГЛ с номерами на сегментах
if (glPointCoordinates.length > 1) {
// Создаём отдельные сегменты линий между точками
for (var i = 0; i < glPointCoordinates.length - 1; i++) {
var segmentNumber = i + 1;
// Создаем линию с стрелкой
var segment = L.polyline([glPointCoordinates[i], glPointCoordinates[i + 1]], {
color: 'blue',
weight: 3,
opacity: 0.7
});
trackLayer.addLayer(segment);
// Добавляем стрелку через декоратор
setTimeout(function(seg, segLayer, num) {
return function() {
var decorator = L.polylineDecorator(seg, {
patterns: [
{
offset: '65%',
repeat: 0,
symbol: L.Symbol.arrowHead({
pixelSize: 15,
polygon: false,
pathOptions: {
stroke: true,
color: 'blue',
weight: 3,
opacity: 0.7
}
})
}
]
});
segLayer.addLayer(decorator);
};
}(segment, trackLayer, segmentNumber), 100);
// Вычисляем центр сегмента для размещения номера
var midLat = (glPointCoordinates[i][0] + glPointCoordinates[i + 1][0]) / 2;
var midLng = (glPointCoordinates[i][1] + glPointCoordinates[i + 1][1]) / 2;
// Добавляем номер сегмента
var segmentIcon = L.divIcon({
className: 'segment-label',
html: '<div style="background: rgba(0, 0, 255, 0.9); color: white; border: 2px solid white; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.4);">' + segmentNumber + '</div>',
iconSize: [28, 28],
iconAnchor: [14, 14]
});
var segmentMarker = L.marker([midLat, midLng], { icon: segmentIcon });
trackLayer.addLayer(segmentMarker);
}
// НЕ добавляем слой трека на карту при загрузке - будет управляться через playback
// trackLayer.addTo(map);
}
// НЕ добавляем GL точки на карту при загрузке - будет управляться через playback
// if (glPointLayers.length > 0) {
// glPointsGroupLayer.addTo(map);
// }
// Создаём иерархию
var treeOverlays = [];
if (sourceOverlays.length > 0) {
treeOverlays.push({
label: "Координаты объекта #{{ source_id }}",
selectAllCheckbox: true,
children: sourceOverlays,
layer: L.layerGroup()
});
}
if (glPointLayers.length > 0) {
treeOverlays.push({
label: "Точки ГЛ",
selectAllCheckbox: true,
children: glPointLayers,
layer: glPointsGroupLayer
});
}
// Добавляем слой трека в контрол
if (glPointCoordinates.length > 1) {
treeOverlays.push({
label: "Трек между точками ГЛ",
layer: trackLayer
});
}
// Создаём tree control
const layerControl = L.control.layers.tree(baseLayers, treeOverlays, {
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>';
// Координаты объекта
var hasSourceCoords = false;
{% for group in groups %}
{% if group.color in 'blue,orange,green,violet' %}
{% if not hasSourceCoords %}
if (!hasSourceCoords) {
div.innerHTML += '<div class="legend-section-title">Координаты объекта:</div>';
hasSourceCoords = true;
}
{% endif %}
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>
`;
{% endif %}
{% endfor %}
// Точки ГЛ
{% for group in groups %}
{% if group.color not in 'blue,orange,green,violet' %}
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Точки ГЛ:</div></div>';
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>
`;
{% endif %}
{% endfor %}
// Добавляем информацию о треке
if (glPointCoordinates.length > 1) {
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Трек между точками:</div></div>';
div.innerHTML += `
<div class="legend-item">
<div style="width: 18px; height: 3px; background: blue; margin-right: 6px;"></div>
<span>Соединительная линия</span>
</div>
`;
}
// Добавляем информацию о специальных точках
if (glPointCoordinates.length > 1) {
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Специальные точки:</div></div>';
div.innerHTML += `
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-green.png" %}');"></div>
<span>Первая точка</span>
</div>
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-orange.png" %}');"></div>
<span>Последняя точка</span>
</div>
`;
}
// Добавляем информацию о playback (если доступен)
if (glPointsWithTime && glPointsWithTime.length > 1) {
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Режим воспроизведения:</div></div>';
div.innerHTML += `
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-grey.png" %}');"></div>
<span>Пройденные точки</span>
</div>
<div class="legend-item">
<div style="width: 24px; height: 24px; margin-right: 6px; background: rgba(0, 0, 255, 0.9); color: white; border: 2px solid white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px;">1</div>
<span>Номер сегмента трека</span>
</div>
<div class="legend-item">
<div style="width: 18px; height: 18px; margin-right: 6px; background: #007bff; border-radius: 3px; display: flex; align-items: center; justify-content: center; color: white; font-size: 10px;">▶</div>
<span>Используйте панель внизу</span>
</div>
`;
}
return div;
};
legend.addTo(map);
// ============ PLAYBACK FUNCTIONALITY ============
var playbackData = null;
var currentPlaybackIndex = 0;
var playbackInterval = null;
var playbackMarkers = [];
var playbackPolyline = null;
var playbackSpeed = 1000; // milliseconds per step
// Показываем контроллер только если есть точки с временными метками
if (glPointsWithTime.length > 1) {
document.getElementById('playbackControl').style.display = 'flex';
// Инициализация слайдера
var timeSlider = document.getElementById('timeSlider');
timeSlider.max = glPointsWithTime.length - 1;
// Функция для форматирования даты
function formatDate(isoString) {
var date = new Date(isoString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Функция для обновления отображения времени
function updateTimeDisplay(index) {
var timeDisplay = document.getElementById('timeDisplay');
if (index >= 0 && index < glPointsWithTime.length) {
timeDisplay.textContent = formatDate(glPointsWithTime[index].timestamp);
}
}
// Функция для отрисовки точек до указанного индекса
function renderPlaybackState(index) {
// Очищаем предыдущие маркеры
playbackMarkers.forEach(m => map.removeLayer(m));
playbackMarkers = [];
if (playbackPolyline) {
map.removeLayer(playbackPolyline);
}
// Проверяем состояние чекбоксов
var showPoints = document.getElementById('showPointsCheckbox').checked;
var showTrack = document.getElementById('showTrackCheckbox').checked;
// Рисуем все точки до текущего индекса
var coords = [];
for (var i = 0; i <= index; i++) {
var point = glPointsWithTime[i];
coords.push(point.coords);
if (showPoints) {
var markerIcon;
// Первая точка - зеленая, текущая - оранжевая, остальные - серые
if (i === 0) {
markerIcon = getColorIcon('green');
} else if (i === index) {
markerIcon = getColorIcon('orange');
} else {
markerIcon = getColorIcon('grey');
}
var marker = L.marker(point.coords, { icon: markerIcon })
.bindPopup((i + 1) + '. ' + point.name + '<br>' + point.frequency + '<br>' + formatDate(point.timestamp));
marker.addTo(map);
playbackMarkers.push(marker);
}
}
// Рисуем линию трека с номерами сегментов
if (showTrack && coords.length > 1) {
// Рисуем отдельные сегменты с номерами
for (var i = 0; i < coords.length - 1; i++) {
var segmentNumber = i + 1;
// Создаем линию сегмента
var segment = L.polyline([coords[i], coords[i + 1]], {
color: 'blue',
weight: 3,
opacity: 0.7
}).addTo(map);
playbackMarkers.push(segment);
// Добавляем стрелку на последний сегмент
if (i === coords.length - 2) {
var decorator = L.polylineDecorator(segment, {
patterns: [{
offset: '65%',
repeat: 0,
symbol: L.Symbol.arrowHead({
pixelSize: 15,
polygon: false,
pathOptions: {
stroke: true,
color: 'blue',
weight: 3,
opacity: 0.7
}
})
}]
}).addTo(map);
playbackMarkers.push(decorator);
}
// Вычисляем центр сегмента для размещения номера
var midLat = (coords[i][0] + coords[i + 1][0]) / 2;
var midLng = (coords[i][1] + coords[i + 1][1]) / 2;
// Добавляем номер сегмента
var segmentIcon = L.divIcon({
className: 'segment-label',
html: '<div style="background: rgba(0, 0, 255, 0.9); color: white; border: 2px solid white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.4);">' + segmentNumber + '</div>',
iconSize: [24, 24],
iconAnchor: [12, 12]
});
var segmentMarker = L.marker([midLat, midLng], { icon: segmentIcon });
segmentMarker.addTo(map);
playbackMarkers.push(segmentMarker);
}
}
updateTimeDisplay(index);
timeSlider.value = index;
}
// Кнопка воспроизведения
document.getElementById('playBtn').addEventListener('click', function() {
if (playbackInterval) return;
this.disabled = true;
document.getElementById('pauseBtn').disabled = false;
playbackInterval = setInterval(function() {
currentPlaybackIndex++;
if (currentPlaybackIndex >= glPointsWithTime.length) {
// Достигли конца
clearInterval(playbackInterval);
playbackInterval = null;
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
currentPlaybackIndex = glPointsWithTime.length - 1;
} else {
renderPlaybackState(currentPlaybackIndex);
}
}, playbackSpeed);
});
// Кнопка паузы
document.getElementById('pauseBtn').addEventListener('click', function() {
if (playbackInterval) {
clearInterval(playbackInterval);
playbackInterval = null;
document.getElementById('playBtn').disabled = false;
this.disabled = true;
}
});
// Кнопка сброса
document.getElementById('resetBtn').addEventListener('click', function() {
if (playbackInterval) {
clearInterval(playbackInterval);
playbackInterval = null;
}
currentPlaybackIndex = 0;
renderPlaybackState(0);
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
});
// Слайдер времени
timeSlider.addEventListener('input', function() {
if (playbackInterval) {
clearInterval(playbackInterval);
playbackInterval = null;
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
}
currentPlaybackIndex = parseInt(this.value);
renderPlaybackState(currentPlaybackIndex);
});
// Выбор скорости
document.getElementById('speedSelect').addEventListener('change', function() {
var speedMultiplier = parseFloat(this.value);
playbackSpeed = 1000 / speedMultiplier;
// Если воспроизведение активно, перезапускаем с новой скоростью
if (playbackInterval) {
clearInterval(playbackInterval);
document.getElementById('pauseBtn').click();
setTimeout(function() {
document.getElementById('playBtn').click();
}, 100);
}
});
// Обработчики для чекбоксов видимости
document.getElementById('showPointsCheckbox').addEventListener('change', function() {
renderPlaybackState(currentPlaybackIndex);
});
document.getElementById('showTrackCheckbox').addEventListener('change', function() {
renderPlaybackState(currentPlaybackIndex);
});
// Инициализация - показываем первую точку
renderPlaybackState(0);
}
</script>
{% endblock extra_js %}