Добавил анимацию в треку. Добавил 2 локальные js библиотеки
This commit is contained in:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user