Добавил анимацию в треку. Добавил 2 локальные js библиотеки
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'leaflet-measure/leaflet-measure.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 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>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -57,11 +58,112 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-bottom: 4px;
|
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>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="map"></div>
|
<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 %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -69,7 +171,8 @@
|
|||||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.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-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>
|
<script>
|
||||||
// Инициализация карты
|
// Инициализация карты
|
||||||
@@ -125,18 +228,23 @@
|
|||||||
var trackLayer = L.layerGroup(); // Layer group for track and related elements
|
var trackLayer = L.layerGroup(); // Layer group for track and related elements
|
||||||
var glPointsGroupLayer = L.layerGroup(); // Layer group specifically for GL points
|
var glPointsGroupLayer = L.layerGroup(); // Layer group specifically for GL points
|
||||||
|
|
||||||
// Сначала собираем все данные о точках ГЛ
|
// Сначала собираем все данные о точках ГЛ с временными метками
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
{% for point_data in group.points %}
|
{% for point_data in group.points %}
|
||||||
{% if not point_data.source_id %}
|
{% if not point_data.source_id %}
|
||||||
glPointsData.push({
|
glPointsData.push({
|
||||||
name: "{{ point_data.name|escapejs }}",
|
name: "{{ point_data.name|escapejs }}",
|
||||||
frequency: "{{ point_data.frequency|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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% 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 %}
|
{% for group in groups %}
|
||||||
@@ -194,9 +302,9 @@
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
// Добавляем слой группы на карту
|
// НЕ добавляем слой группы на карту при загрузке - будет управляться через layer control
|
||||||
{% if group.color in 'blue,orange,green,violet' %}
|
{% if group.color in 'blue,orange,green,violet' %}
|
||||||
groupLayer.addTo(map);
|
// groupLayer.addTo(map); // Закомментировано - слои скрыты по умолчанию
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
@@ -254,14 +362,14 @@
|
|||||||
trackLayer.addLayer(segmentMarker);
|
trackLayer.addLayer(segmentMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем слой трека на карту
|
// НЕ добавляем слой трека на карту при загрузке - будет управляться через playback
|
||||||
trackLayer.addTo(map);
|
// trackLayer.addTo(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем GL точки на карту
|
// НЕ добавляем GL точки на карту при загрузке - будет управляться через playback
|
||||||
if (glPointLayers.length > 0) {
|
// if (glPointLayers.length > 0) {
|
||||||
glPointsGroupLayer.addTo(map);
|
// glPointsGroupLayer.addTo(map);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Создаём иерархию
|
// Создаём иерархию
|
||||||
var treeOverlays = [];
|
var treeOverlays = [];
|
||||||
@@ -373,9 +481,247 @@
|
|||||||
</div>
|
</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;
|
return div;
|
||||||
};
|
};
|
||||||
legend.addTo(map);
|
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>
|
</script>
|
||||||
{% endblock extra_js %}
|
{% endblock extra_js %}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
|||||||
"parameter_obj", "geo_obj"
|
"parameter_obj", "geo_obj"
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Собираем все точки ГЛ в одну группу
|
# Собираем все точки ГЛ в одну группу с сортировкой по времени
|
||||||
all_gl_points = []
|
all_gl_points = []
|
||||||
for obj in gl_points:
|
for obj in gl_points:
|
||||||
if (
|
if (
|
||||||
@@ -250,9 +250,13 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
|||||||
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||||
"name": obj.name,
|
"name": obj.name,
|
||||||
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||||
|
"timestamp": obj.geo_obj.timestamp.isoformat() if obj.geo_obj.timestamp else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Сортируем точки по времени (от старой к новой)
|
||||||
|
all_gl_points.sort(key=lambda x: x["timestamp"] if x["timestamp"] else "")
|
||||||
|
|
||||||
# Добавляем все точки ГЛ одним цветом (красный)
|
# Добавляем все точки ГЛ одним цветом (красный)
|
||||||
if all_gl_points:
|
if all_gl_points:
|
||||||
groups.append(
|
groups.append(
|
||||||
|
|||||||
1
dbapp/static/leaflet-playback/leaflet-playback.js
Normal file
1
dbapp/static/leaflet-playback/leaflet-playback.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,478 @@
|
|||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('leaflet')) :
|
||||||
|
typeof define === 'function' && define.amd ? define(['leaflet'], factory) :
|
||||||
|
(factory(global.L));
|
||||||
|
}(this, (function (L$1) { 'use strict';
|
||||||
|
|
||||||
|
L$1 = L$1 && L$1.hasOwnProperty('default') ? L$1['default'] : L$1;
|
||||||
|
|
||||||
|
// functional re-impl of L.Point.distanceTo,
|
||||||
|
// with no dependency on Leaflet for easier testing
|
||||||
|
function pointDistance(ptA, ptB) {
|
||||||
|
var x = ptB.x - ptA.x;
|
||||||
|
var y = ptB.y - ptA.y;
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var computeSegmentHeading = function computeSegmentHeading(a, b) {
|
||||||
|
return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI + 90 + 360) % 360;
|
||||||
|
};
|
||||||
|
|
||||||
|
var asRatioToPathLength = function asRatioToPathLength(_ref, totalPathLength) {
|
||||||
|
var value = _ref.value,
|
||||||
|
isInPixels = _ref.isInPixels;
|
||||||
|
return isInPixels ? value / totalPathLength : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseRelativeOrAbsoluteValue(value) {
|
||||||
|
if (typeof value === 'string' && value.indexOf('%') !== -1) {
|
||||||
|
return {
|
||||||
|
value: parseFloat(value) / 100,
|
||||||
|
isInPixels: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var parsedValue = value ? parseFloat(value) : 0;
|
||||||
|
return {
|
||||||
|
value: parsedValue,
|
||||||
|
isInPixels: parsedValue > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointsEqual = function pointsEqual(a, b) {
|
||||||
|
return a.x === b.x && a.y === b.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
function pointsToSegments(pts) {
|
||||||
|
return pts.reduce(function (segments, b, idx, points) {
|
||||||
|
// this test skips same adjacent points
|
||||||
|
if (idx > 0 && !pointsEqual(b, points[idx - 1])) {
|
||||||
|
var a = points[idx - 1];
|
||||||
|
var distA = segments.length > 0 ? segments[segments.length - 1].distB : 0;
|
||||||
|
var distAB = pointDistance(a, b);
|
||||||
|
segments.push({
|
||||||
|
a: a,
|
||||||
|
b: b,
|
||||||
|
distA: distA,
|
||||||
|
distB: distA + distAB,
|
||||||
|
heading: computeSegmentHeading(a, b)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectPatternOnPointPath(pts, pattern) {
|
||||||
|
// 1. split the path into segment infos
|
||||||
|
var segments = pointsToSegments(pts);
|
||||||
|
var nbSegments = segments.length;
|
||||||
|
if (nbSegments === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalPathLength = segments[nbSegments - 1].distB;
|
||||||
|
|
||||||
|
var offset = asRatioToPathLength(pattern.offset, totalPathLength);
|
||||||
|
var endOffset = asRatioToPathLength(pattern.endOffset, totalPathLength);
|
||||||
|
var repeat = asRatioToPathLength(pattern.repeat, totalPathLength);
|
||||||
|
|
||||||
|
var repeatIntervalPixels = totalPathLength * repeat;
|
||||||
|
var startOffsetPixels = offset > 0 ? totalPathLength * offset : 0;
|
||||||
|
var endOffsetPixels = endOffset > 0 ? totalPathLength * endOffset : 0;
|
||||||
|
|
||||||
|
// 2. generate the positions of the pattern as offsets from the path start
|
||||||
|
var positionOffsets = [];
|
||||||
|
var positionOffset = startOffsetPixels;
|
||||||
|
do {
|
||||||
|
positionOffsets.push(positionOffset);
|
||||||
|
positionOffset += repeatIntervalPixels;
|
||||||
|
} while (repeatIntervalPixels > 0 && positionOffset < totalPathLength - endOffsetPixels);
|
||||||
|
|
||||||
|
// 3. projects offsets to segments
|
||||||
|
var segmentIndex = 0;
|
||||||
|
var segment = segments[0];
|
||||||
|
return positionOffsets.map(function (positionOffset) {
|
||||||
|
// find the segment matching the offset,
|
||||||
|
// starting from the previous one as offsets are ordered
|
||||||
|
while (positionOffset > segment.distB && segmentIndex < nbSegments - 1) {
|
||||||
|
segmentIndex++;
|
||||||
|
segment = segments[segmentIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentRatio = (positionOffset - segment.distA) / (segment.distB - segment.distA);
|
||||||
|
return {
|
||||||
|
pt: interpolateBetweenPoints(segment.a, segment.b, segmentRatio),
|
||||||
|
heading: segment.heading
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the point which lies on the segment defined by points A and B,
|
||||||
|
* at the given ratio of the distance from A to B, by linear interpolation.
|
||||||
|
*/
|
||||||
|
function interpolateBetweenPoints(ptA, ptB, ratio) {
|
||||||
|
if (ptB.x !== ptA.x) {
|
||||||
|
return {
|
||||||
|
x: ptA.x + ratio * (ptB.x - ptA.x),
|
||||||
|
y: ptA.y + ratio * (ptB.y - ptA.y)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// special case where points lie on the same vertical axis
|
||||||
|
return {
|
||||||
|
x: ptA.x,
|
||||||
|
y: ptA.y + (ptB.y - ptA.y) * ratio
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
// save these original methods before they are overwritten
|
||||||
|
var proto_initIcon = L.Marker.prototype._initIcon;
|
||||||
|
var proto_setPos = L.Marker.prototype._setPos;
|
||||||
|
|
||||||
|
var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
|
||||||
|
|
||||||
|
L.Marker.addInitHook(function () {
|
||||||
|
var iconOptions = this.options.icon && this.options.icon.options;
|
||||||
|
var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
|
||||||
|
if (iconAnchor) {
|
||||||
|
iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
|
||||||
|
}
|
||||||
|
this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ;
|
||||||
|
this.options.rotationAngle = this.options.rotationAngle || 0;
|
||||||
|
|
||||||
|
// Ensure marker keeps rotated during dragging
|
||||||
|
this.on('drag', function(e) { e.target._applyRotation(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
L.Marker.include({
|
||||||
|
_initIcon: function() {
|
||||||
|
proto_initIcon.call(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPos: function (pos) {
|
||||||
|
proto_setPos.call(this, pos);
|
||||||
|
this._applyRotation();
|
||||||
|
},
|
||||||
|
|
||||||
|
_applyRotation: function () {
|
||||||
|
if(this.options.rotationAngle) {
|
||||||
|
this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin;
|
||||||
|
|
||||||
|
if(oldIE) {
|
||||||
|
// for IE 9, use the 2D rotation
|
||||||
|
this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
|
||||||
|
} else {
|
||||||
|
// for modern browsers, prefer the 3D accelerated version
|
||||||
|
this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setRotationAngle: function(angle) {
|
||||||
|
this.options.rotationAngle = angle;
|
||||||
|
this.update();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
setRotationOrigin: function(origin) {
|
||||||
|
this.options.rotationOrigin = origin;
|
||||||
|
this.update();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
L$1.Symbol = L$1.Symbol || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple dash symbol, drawn as a Polyline.
|
||||||
|
* Can also be used for dots, if 'pixelSize' option is given the 0 value.
|
||||||
|
*/
|
||||||
|
L$1.Symbol.Dash = L$1.Class.extend({
|
||||||
|
options: {
|
||||||
|
pixelSize: 10,
|
||||||
|
pathOptions: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function initialize(options) {
|
||||||
|
L$1.Util.setOptions(this, options);
|
||||||
|
this.options.pathOptions.clickable = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
|
||||||
|
var opts = this.options;
|
||||||
|
var d2r = Math.PI / 180;
|
||||||
|
|
||||||
|
// for a dot, nothing more to compute
|
||||||
|
if (opts.pixelSize <= 1) {
|
||||||
|
return L$1.polyline([dirPoint.latLng, dirPoint.latLng], opts.pathOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
var midPoint = map.project(dirPoint.latLng);
|
||||||
|
var angle = -(dirPoint.heading - 90) * d2r;
|
||||||
|
var a = L$1.point(midPoint.x + opts.pixelSize * Math.cos(angle + Math.PI) / 2, midPoint.y + opts.pixelSize * Math.sin(angle) / 2);
|
||||||
|
// compute second point by central symmetry to avoid unecessary cos/sin
|
||||||
|
var b = midPoint.add(midPoint.subtract(a));
|
||||||
|
return L$1.polyline([map.unproject(a), map.unproject(b)], opts.pathOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L$1.Symbol.dash = function (options) {
|
||||||
|
return new L$1.Symbol.Dash(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
L$1.Symbol.ArrowHead = L$1.Class.extend({
|
||||||
|
options: {
|
||||||
|
polygon: true,
|
||||||
|
pixelSize: 10,
|
||||||
|
headAngle: 60,
|
||||||
|
pathOptions: {
|
||||||
|
stroke: false,
|
||||||
|
weight: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function initialize(options) {
|
||||||
|
L$1.Util.setOptions(this, options);
|
||||||
|
this.options.pathOptions.clickable = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
|
||||||
|
return this.options.polygon ? L$1.polygon(this._buildArrowPath(dirPoint, map), this.options.pathOptions) : L$1.polyline(this._buildArrowPath(dirPoint, map), this.options.pathOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
_buildArrowPath: function _buildArrowPath(dirPoint, map) {
|
||||||
|
var d2r = Math.PI / 180;
|
||||||
|
var tipPoint = map.project(dirPoint.latLng);
|
||||||
|
var direction = -(dirPoint.heading - 90) * d2r;
|
||||||
|
var radianArrowAngle = this.options.headAngle / 2 * d2r;
|
||||||
|
|
||||||
|
var headAngle1 = direction + radianArrowAngle;
|
||||||
|
var headAngle2 = direction - radianArrowAngle;
|
||||||
|
var arrowHead1 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle1), tipPoint.y + this.options.pixelSize * Math.sin(headAngle1));
|
||||||
|
var arrowHead2 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle2), tipPoint.y + this.options.pixelSize * Math.sin(headAngle2));
|
||||||
|
|
||||||
|
return [map.unproject(arrowHead1), dirPoint.latLng, map.unproject(arrowHead2)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L$1.Symbol.arrowHead = function (options) {
|
||||||
|
return new L$1.Symbol.ArrowHead(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
L$1.Symbol.Marker = L$1.Class.extend({
|
||||||
|
options: {
|
||||||
|
markerOptions: {},
|
||||||
|
rotate: false
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function initialize(options) {
|
||||||
|
L$1.Util.setOptions(this, options);
|
||||||
|
this.options.markerOptions.clickable = false;
|
||||||
|
this.options.markerOptions.draggable = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildSymbol: function buildSymbol(directionPoint, latLngs, map, index, total) {
|
||||||
|
if (this.options.rotate) {
|
||||||
|
this.options.markerOptions.rotationAngle = directionPoint.heading + (this.options.angleCorrection || 0);
|
||||||
|
}
|
||||||
|
return L$1.marker(directionPoint.latLng, this.options.markerOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L$1.Symbol.marker = function (options) {
|
||||||
|
return new L$1.Symbol.Marker(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
var isCoord = function isCoord(c) {
|
||||||
|
return c instanceof L$1.LatLng || Array.isArray(c) && c.length === 2 && typeof c[0] === 'number';
|
||||||
|
};
|
||||||
|
|
||||||
|
var isCoordArray = function isCoordArray(ll) {
|
||||||
|
return Array.isArray(ll) && isCoord(ll[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
L$1.PolylineDecorator = L$1.FeatureGroup.extend({
|
||||||
|
options: {
|
||||||
|
patterns: []
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function initialize(paths, options) {
|
||||||
|
L$1.FeatureGroup.prototype.initialize.call(this);
|
||||||
|
L$1.Util.setOptions(this, options);
|
||||||
|
this._map = null;
|
||||||
|
this._paths = this._initPaths(paths);
|
||||||
|
this._bounds = this._initBounds();
|
||||||
|
this._patterns = this._initPatterns(this.options.patterns);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deals with all the different cases. input can be one of these types:
|
||||||
|
* array of LatLng, array of 2-number arrays, Polyline, Polygon,
|
||||||
|
* array of one of the previous.
|
||||||
|
*/
|
||||||
|
_initPaths: function _initPaths(input, isPolygon) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
if (isCoordArray(input)) {
|
||||||
|
// Leaflet Polygons don't need the first point to be repeated, but we do
|
||||||
|
var coords = isPolygon ? input.concat([input[0]]) : input;
|
||||||
|
return [coords];
|
||||||
|
}
|
||||||
|
if (input instanceof L$1.Polyline) {
|
||||||
|
// we need some recursivity to support multi-poly*
|
||||||
|
return this._initPaths(input.getLatLngs(), input instanceof L$1.Polygon);
|
||||||
|
}
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
// flatten everything, we just need coordinate lists to apply patterns
|
||||||
|
return input.reduce(function (flatArray, p) {
|
||||||
|
return flatArray.concat(_this._initPaths(p, isPolygon));
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// parse pattern definitions and precompute some values
|
||||||
|
_initPatterns: function _initPatterns(patternDefs) {
|
||||||
|
return patternDefs.map(this._parsePatternDef);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the patterns used by this decorator
|
||||||
|
* and redraws the new one.
|
||||||
|
*/
|
||||||
|
setPatterns: function setPatterns(patterns) {
|
||||||
|
this.options.patterns = patterns;
|
||||||
|
this._patterns = this._initPatterns(this.options.patterns);
|
||||||
|
this.redraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the patterns used by this decorator
|
||||||
|
* and redraws the new one.
|
||||||
|
*/
|
||||||
|
setPaths: function setPaths(paths) {
|
||||||
|
this._paths = this._initPaths(paths);
|
||||||
|
this._bounds = this._initBounds();
|
||||||
|
this.redraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the pattern definition
|
||||||
|
*/
|
||||||
|
_parsePatternDef: function _parsePatternDef(patternDef, latLngs) {
|
||||||
|
return {
|
||||||
|
symbolFactory: patternDef.symbol,
|
||||||
|
// Parse offset and repeat values, managing the two cases:
|
||||||
|
// absolute (in pixels) or relative (in percentage of the polyline length)
|
||||||
|
offset: parseRelativeOrAbsoluteValue(patternDef.offset),
|
||||||
|
endOffset: parseRelativeOrAbsoluteValue(patternDef.endOffset),
|
||||||
|
repeat: parseRelativeOrAbsoluteValue(patternDef.repeat)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function onAdd(map) {
|
||||||
|
this._map = map;
|
||||||
|
this._draw();
|
||||||
|
this._map.on('moveend', this.redraw, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function onRemove(map) {
|
||||||
|
this._map.off('moveend', this.redraw, this);
|
||||||
|
this._map = null;
|
||||||
|
L$1.FeatureGroup.prototype.onRemove.call(this, map);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As real pattern bounds depends on map zoom and bounds,
|
||||||
|
* we just compute the total bounds of all paths decorated by this instance.
|
||||||
|
*/
|
||||||
|
_initBounds: function _initBounds() {
|
||||||
|
var allPathCoords = this._paths.reduce(function (acc, path) {
|
||||||
|
return acc.concat(path);
|
||||||
|
}, []);
|
||||||
|
return L$1.latLngBounds(allPathCoords);
|
||||||
|
},
|
||||||
|
|
||||||
|
getBounds: function getBounds() {
|
||||||
|
return this._bounds;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of ILayers object
|
||||||
|
*/
|
||||||
|
_buildSymbols: function _buildSymbols(latLngs, symbolFactory, directionPoints) {
|
||||||
|
var _this2 = this;
|
||||||
|
|
||||||
|
return directionPoints.map(function (directionPoint, i) {
|
||||||
|
return symbolFactory.buildSymbol(directionPoint, latLngs, _this2._map, i, directionPoints.length);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute pairs of LatLng and heading angle,
|
||||||
|
* that define positions and directions of the symbols on the path
|
||||||
|
*/
|
||||||
|
_getDirectionPoints: function _getDirectionPoints(latLngs, pattern) {
|
||||||
|
var _this3 = this;
|
||||||
|
|
||||||
|
if (latLngs.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var pathAsPoints = latLngs.map(function (latLng) {
|
||||||
|
return _this3._map.project(latLng);
|
||||||
|
});
|
||||||
|
return projectPatternOnPointPath(pathAsPoints, pattern).map(function (point) {
|
||||||
|
return {
|
||||||
|
latLng: _this3._map.unproject(L$1.point(point.pt)),
|
||||||
|
heading: point.heading
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
redraw: function redraw() {
|
||||||
|
if (!this._map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clearLayers();
|
||||||
|
this._draw();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all symbols for a given pattern as an array of FeatureGroup
|
||||||
|
*/
|
||||||
|
_getPatternLayers: function _getPatternLayers(pattern) {
|
||||||
|
var _this4 = this;
|
||||||
|
|
||||||
|
var mapBounds = this._map.getBounds().pad(0.1);
|
||||||
|
return this._paths.map(function (path) {
|
||||||
|
var directionPoints = _this4._getDirectionPoints(path, pattern)
|
||||||
|
// filter out invisible points
|
||||||
|
.filter(function (point) {
|
||||||
|
return mapBounds.contains(point.latLng);
|
||||||
|
});
|
||||||
|
return L$1.featureGroup(_this4._buildSymbols(path, pattern.symbolFactory, directionPoints));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw all patterns
|
||||||
|
*/
|
||||||
|
_draw: function _draw() {
|
||||||
|
var _this5 = this;
|
||||||
|
|
||||||
|
this._patterns.map(function (pattern) {
|
||||||
|
return _this5._getPatternLayers(pattern);
|
||||||
|
}).forEach(function (layers) {
|
||||||
|
_this5.addLayer(L$1.featureGroup(layers));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
* Allows compact syntax to be used
|
||||||
|
*/
|
||||||
|
L$1.polylineDecorator = function (paths, options) {
|
||||||
|
return new L$1.PolylineDecorator(paths, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
})));
|
||||||
Reference in New Issue
Block a user