Добавил анимацию в треку. Добавил 2 локальные js библиотеки

This commit is contained in:
2025-11-26 11:12:14 +03:00
parent 609fd5a1da
commit 27694a3a7d
4 changed files with 841 additions and 12 deletions

View File

@@ -7,6 +7,7 @@
<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;
@@ -57,11 +58,112 @@
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 %}
@@ -69,7 +171,8 @@
<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="https://cdn.jsdelivr.net/npm/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylineDecorator.min.js"></script>
<script src="{% static 'leaflet-polylineDecorator/leaflet.polylineDecorator.js' %}"></script>
<script src="{% static 'leaflet-playback/leaflet-playback.js' %}"></script>
<script>
// Инициализация карты
@@ -125,18 +228,23 @@
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 }}]
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 %}
@@ -194,9 +302,9 @@
});
{% endif %}
// Добавляем слой группы на карту
// НЕ добавляем слой группы на карту при загрузке - будет управляться через layer control
{% if group.color in 'blue,orange,green,violet' %}
groupLayer.addTo(map);
// groupLayer.addTo(map); // Закомментировано - слои скрыты по умолчанию
{% endif %}
{% endfor %}
@@ -254,14 +362,14 @@
trackLayer.addLayer(segmentMarker);
}
// Добавляем слой трека на карту
trackLayer.addTo(map);
// НЕ добавляем слой трека на карту при загрузке - будет управляться через playback
// trackLayer.addTo(map);
}
// Добавляем GL точки на карту
if (glPointLayers.length > 0) {
glPointsGroupLayer.addTo(map);
}
// НЕ добавляем GL точки на карту при загрузке - будет управляться через playback
// if (glPointLayers.length > 0) {
// glPointsGroupLayer.addTo(map);
// }
// Создаём иерархию
var treeOverlays = [];
@@ -373,9 +481,247 @@
</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 %}