@@ -120,6 +120,32 @@
. moving-marker {
transition : transform 0.1 s linear ;
}
. marker-size-control {
position : fixed ;
bottom : 10 px ;
right : 10 px ;
z-index : 999 ;
background : white ;
padding : 10 px 12 px ;
border-radius : 4 px ;
box-shadow : 0 1 px 5 px rgba ( 0 , 0 , 0 , 0.3 ) ;
font-size : 11 px ;
}
. marker-size-control label {
display : block ;
margin-bottom : 5 px ;
font-weight : bold ;
}
. marker-size-control input [ type = "range" ] {
width : 120 px ;
cursor : pointer ;
}
. marker-size-control . size-value {
display : inline-block ;
margin-left : 5 px ;
font-weight : bold ;
color : #007bff ;
}
< / style >
{% endblock %}
@@ -133,6 +159,12 @@
< div id = "map" > < / div >
< div class = "marker-size-control" >
< label > Размер маркеров:< / label >
< input type = "range" id = "markerSizeSlider" min = "0.5" max = "2" step = "0.1" value = "1" >
< span class = "size-value" id = "sizeValue" > 1.0x< / span >
< / div >
< div class = "playback-control" id = "playbackControl" style = "display: none;" >
< button id = "playBtn" title = "Воспроизвести" > ▶< / button >
< button id = "pauseBtn" title = "Пауза" disabled > ⏸< / button >
@@ -195,7 +227,7 @@ let currentMarkers = {};
let trailPolylines = { } ;
let staticMarkers = { } ;
// Color mapping
// Color mapping - расширенная палитра
const colorMap = {
'red' : '#dc3545' ,
'blue' : '#007bff' ,
@@ -204,41 +236,85 @@ const colorMap = {
'orange' : '#fd7e14' ,
'cyan' : '#17a2b8' ,
'magenta' : '#e83e8c' ,
'yellow ' : '#ffc107 ' ,
'lime ' : '#32cd32 ' ,
'p ink ' : '#ff69b4'
'pink ' : '#ff69b4 ' ,
'teal ' : '#20c997 ' ,
'indigo ' : '#6610f2' ,
'brown' : '#8b4513' ,
'navy' : '#000080' ,
'maroon' : '#800000' ,
'olive' : '#808000' ,
'coral' : '#ff7f50' ,
'turquoise' : '#40e0d0'
} ;
// Create marker icon using leaflet-markers library for static markers
function createStaticMarkerIcon ( type ) {
let markerColor = 'grey' ;
// Shape mapping - разные формы маркеров
const shapeMap = {
'circle' : ( color , size ) => {
const adjustedSize = Math . round ( size * 0.85 ) ; // Уменьшаем на 15%
return ` <div style="background: ${ color } ; width: ${ adjustedSize } px; height: ${ adjustedSize } px; border-radius: 50%; border: 1px solid black; box-sizing: border-box;"></div> ` ;
} ,
'square' : ( color , size ) => {
const adjustedSize = Math . round ( size * 0.85 ) ; // Уменьшаем на 15%
return ` <div style="background: ${ color } ; width: ${ adjustedSize } px; height: ${ adjustedSize } px; border: 1px solid black; box-sizing: border-box;"></div> ` ;
} ,
'triangle' : ( color , size ) => {
const adjustedSize = Math . round ( size * 0.8 ) ; // Уменьшаем на 20%
return ` <svg width=" ${ adjustedSize } " height=" ${ adjustedSize } " viewBox="0 0 100 100"><polygon points="50,10 90,90 10,90" fill=" ${ color } " stroke="black" stroke-width="2"/></svg> ` ;
} ,
'star' : ( color , size ) => ` <svg width=" ${ size } " height=" ${ size } " viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill=" ${ color } " stroke="black" stroke-width="0.8"/></svg> ` ,
'pentagon' : ( color , size ) => ` <svg width=" ${ size } " height=" ${ size } " viewBox="0 0 24 24"><path d="M12 2l7.5 5.5-2.9 9H7.4l-2.9-9z" fill=" ${ color } " stroke="black" stroke-width="0.8"/></svg> ` ,
'hexagon' : ( color , size ) => ` <svg width=" ${ size } " height=" ${ size } " viewBox="0 0 24 24"><path d="M12 2l6 4v8l-6 4-6-4V6z" fill=" ${ color } " stroke="black" stroke-width="0.8"/></svg> ` ,
'diamond' : ( color , size ) => {
const adjustedSize = Math . round ( size * 0.85 ) ; // Уменьшаем на 15%
return ` <div style="background: ${ color } ; width: ${ adjustedSize } px; height: ${ adjustedSize } px; transform: rotate(45deg); border: 1px solid black; box-sizing: border-box;"></div> ` ;
} ,
'cross' : ( color , size ) => ` <svg width=" ${ size } " height=" ${ size } " viewBox="0 0 24 24"><path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7V2z" fill=" ${ color } " stroke="black" stroke-width="0.8"/></svg> `
} ;
// Available shapes for assignment
const availableShapes = [ 'circle' , 'square' , 'triangle' , 'star' , 'pentagon' , 'hexagon' , 'diamond' , 'cross' ] ;
// Global marker size multiplier
let markerSizeMultiplier = 1.0 ;
// Create static marker icon (start/intermediate/end points)
function createStaticMarkerIcon ( type , color , shape ) {
const hexColor = colorMap [ color ] || color ;
let baseSize = 12 ; // Уменьшенный базовый размер
if ( type === 'start' ) {
markerColor = 'green' ;
baseSize = 14 ; // Начальная точка чуть больше
} else if ( type === 'end' ) {
markerColor = 'red' ;
baseSize = 14 ; // Конечная точка чуть больше
} else if ( type === 'intermediate' ) {
markerColor = 'grey' ;
baseSize = 10 ; // Промежуточные точки меньше
}
return L . icon ( {
iconUrl : '{% static "leaflet-markers/img/marker-icon-" %}' + markerColor + '.png' ,
shadowUrl : '{% static "leaflet-markers/img/marker-shadow.png" %}' ,
iconSize : [ 25 , 41 ] ,
iconAnchor : [ 12 , 41 ] ,
popupAnchor : [ 1 , - 34 ] ,
shadowSize : [ 41 , 41 ]
const size = Math . round ( baseSize * markerSizeMultiplier ) ;
const shapeFunc = shapeMap [ shape ] || shapeMap [ 'circle' ] ;
return L . divIcon ( {
className : 'static-marker' ,
iconSize : [ size , size ] ,
iconAnchor : [ size / 2 , size / 2 ] ,
popupAnchor : [ 0 , - size / 2 ] ,
html : shapeFunc ( hexColor , size )
} ) ;
}
// Create moving marker icon (colored circle )
function createMovingMarkerIcon ( color ) {
// Create moving marker icon (current position - larger and more prominent )
function createMovingMarkerIcon ( color , shape ) {
const hexColor = colorMap [ color ] || color ;
const baseSize = 16 ; // Движущийся маркер немного больше
const size = Math . round ( baseSize * markerSizeMultiplier ) ;
const shapeFunc = shapeMap [ shape ] || shapeMap [ 'circle' ] ;
return L . divIcon ( {
className : 'current-marker' ,
iconSize : [ 18 , 18 ] ,
iconAnchor : [ 9 , 9 ] ,
html : ` <div style="background: ${ hexColor } ; width: 100%; height: 100%; border-radius: 50%; border: 3px solid white; box-shadow: 0 0 6px rgba(0,0,0,0.5);"></div> `
className : 'current-marker moving-marker ' ,
iconSize : [ size , size ] ,
iconAnchor : [ size / 2 , size / 2 ] ,
popupAnchor : [ 0 , - size / 2 ] ,
html : shapeFunc ( hexColor , size )
} ) ;
}
@@ -335,21 +411,34 @@ function updateDisplay(progress) {
sourcesData . forEach ( source => {
const pos = getPositionAtProgress ( source , progress ) ;
const color = source . color ;
const shape = source . shape ;
if ( pos ) {
// Show/update current marker (moving object - colored circle)
if ( ! currentMarkers [ source . source _id ] ) {
// Get first point name for popup
const firstPointName = source . points && source . points . length > 0 ? source . points [ 0 ] . name : '' ;
const popupContent = ` <b>ID ${ source . source _id } :</b> ${ firstPointName } <br><i style="color: #666;">Текущая позиция</i> ` ;
currentMarkers [ source . source _id ] = L . marker ( [ pos . lat , pos . lng ] , {
icon : createMovingMarkerIcon ( color ) ,
zIndexOffset : 1000
} ) . bindPopup ( popupContent ) ;
sourceLayerGroups [ source . source _id ] . addLayer ( currentMarkers [ source . source _id ] ) ;
// Check if we've reached the end
const isAtEnd = ( progress >= 1 || pos . pointIndex === source . points . length - 1 ) && pos . segmentProgress >= 0.99 ;
// Show/update current marker (moving object) - hide when at end
if ( isAtEnd ) {
// Remove moving marker when at end point
if ( currentMarkers [ source . source _id ] ) {
sourceLayerGroups [ source . source _id ] . removeLayer ( currentMarkers [ source . source _id ] ) ;
delete currentMarkers [ source . source _id ] ;
}
} else {
currentMarkers [ source . source _id ] . setLatLng ( [ pos . lat , pos . lng ] ) ;
// Show/update moving marker
if ( ! currentMarkers [ source . source _id ] ) {
// Get first point name for popup
const firstPointName = source . points && source . points . length > 0 ? source . points [ 0 ] . name : '' ;
const popupContent = ` <b>ID ${ source . source _id } :</b> ${ firstPointName } <br><i style="color: #666;">Текущая позиция</i> ` ;
currentMarkers [ source . source _id ] = L . marker ( [ pos . lat , pos . lng ] , {
icon : createMovingMarkerIcon ( color , shape ) ,
zIndexOffset : 1000
} ) . bindPopup ( popupContent ) ;
sourceLayerGroups [ source . source _id ] . addLayer ( currentMarkers [ source . source _id ] ) ;
} else {
currentMarkers [ source . source _id ] . setLatLng ( [ pos . lat , pos . lng ] ) ;
}
}
// Update trail (dashed line showing path)
@@ -386,7 +475,7 @@ function updateDisplay(progress) {
}
const marker = L . marker ( [ point . lat , point . lng ] , {
icon : createStaticMarkerIcon ( markerType ) ,
icon : createStaticMarkerIcon ( markerType , color , shape ),
zIndexOffset : markerType === 'start' ? 500 : ( markerType === 'end' ? 600 : 100 )
} ) . bindPopup ( ` <b> ${ source . source _name } </b><br> ${ popupPrefix } ${ point . name } <br> ${ point . frequency } <br> ${ formatDate ( point . timestamp ) } ` ) ;
@@ -401,7 +490,7 @@ function updateDisplay(progress) {
if ( ! staticMarkers [ endMarkerKey ] && source . points . length > 1 ) {
const lastPoint = source . points [ source . points . length - 1 ] ;
const endMarker = L . marker ( [ lastPoint . lat , lastPoint . lng ] , {
icon : createStaticMarkerIcon ( 'end' ) ,
icon : createStaticMarkerIcon ( 'end' , color , shape ),
zIndexOffset : 600
} ) . bindPopup ( ` <b> ${ source . source _name } </b><br><span style="color: red;">■ Конец</span><br> ${ lastPoint . name } <br> ${ lastPoint . frequency } <br> ${ formatDate ( lastPoint . timestamp ) } ` ) ;
sourceLayerGroups [ source . source _id ] . addLayer ( endMarker ) ;
@@ -422,19 +511,21 @@ function resetPlayback() {
// Recreate trails
sourcesData . forEach ( source => {
const color = colorMap [ source . color ] || source . color ;
const shape = source . shape ;
trailPolylines [ source . source _id ] = L . polyline ( [ ] , {
color : color ,
weight : 3 ,
weight : 2 , // Уменьшенная толщина линии
opacity : 0.7 ,
dashArray : '5, 10'
} ) ;
sourceLayerGroups [ source . source _id ] . addLayer ( trailPolylines [ source . source _id ] ) ;
// Add start marker immediately (green)
// Add start marker immediately
if ( source . points && source . points . length > 0 ) {
const firstPoint = source . points [ 0 ] ;
const startMarker = L . marker ( [ firstPoint . lat , firstPoint . lng ] , {
icon : createStaticMarkerIcon ( 'start' ) ,
icon : createStaticMarkerIcon ( 'start' , color , shape ),
zIndexOffset : 500
} ) . bindPopup ( ` <b> ${ source . source _name } </b><br><span style="color: green;">▶ Начало</span><br> ${ firstPoint . name } <br> ${ firstPoint . frequency } <br> ${ formatDate ( firstPoint . timestamp ) } ` ) ;
sourceLayerGroups [ source . source _id ] . addLayer ( startMarker ) ;
@@ -446,6 +537,16 @@ function resetPlayback() {
updateDisplay ( currentProgress ) ;
}
// Update all marker sizes
function updateMarkerSizes ( ) {
// Clear and recreate all markers with new size
pause ( ) ;
const currentProg = currentProgress ;
resetPlayback ( ) ;
currentProgress = currentProg ;
updateDisplay ( currentProgress ) ;
}
// Play animation
function play ( ) {
if ( playbackInterval ) return ;
@@ -493,6 +594,11 @@ async function loadData() {
// Filter out sources with no points
sourcesData = sourcesData . filter ( s => s . points && s . points . length > 0 ) ;
// Assign shapes to sources (cycle through available shapes)
sourcesData . forEach ( ( source , idx ) => {
source . shape = availableShapes [ idx % availableShapes . length ] ;
} ) ;
if ( ! sourcesData || sourcesData . length === 0 ) {
document . getElementById ( 'loadingOverlay' ) . innerHTML = `
<div class="alert alert-warning">
@@ -567,6 +673,21 @@ async function loadData() {
speedMultiplier = parseFloat ( this . value ) ;
} ) ;
// Marker size control
const markerSizeSlider = document . getElementById ( 'markerSizeSlider' ) ;
const sizeValue = document . getElementById ( 'sizeValue' ) ;
markerSizeSlider . addEventListener ( 'input' , function ( ) {
markerSizeMultiplier = parseFloat ( this . value ) ;
sizeValue . textContent = markerSizeMultiplier . toFixed ( 1 ) + 'x' ;
updateMarkerSizes ( ) ;
} ) ;
// Disable map scroll/zoom when mouse is over marker size control
const markerSizeControl = document . querySelector ( '.marker-size-control' ) ;
L . DomEvent . disableScrollPropagation ( markerSizeControl ) ;
L . DomEvent . disableClickPropagation ( markerSizeControl ) ;
} catch ( error ) {
console . error ( 'Error loading data:' , error ) ;
document . getElementById ( 'loadingOverlay' ) . innerHTML = `
@@ -593,22 +714,12 @@ function addLegend() {
sourcesData . forEach ( source => {
const color = colorMap [ source . color ] || source . color ;
const shape = source . shape ;
const points = source . points ;
// Get marker color for this source
const markerColorMap = {
'red' : 'red' ,
'blue' : 'blue' ,
'green' : 'green' ,
'purple' : 'violet' ,
'orange' : 'orange' ,
'cyan' : 'blue' ,
'magenta' : 'red' ,
'yellow' : 'yellow' ,
'lime' : 'green' ,
'pink' : 'red'
} ;
const markerColor = markerColorMap [ source . color ] || 'blue' ;
// Create shape preview
const shapeFunc = shapeMap [ shape ] || shapeMap [ 'circle' ] ;
const shapePreview = shapeFunc ( color , 16 ) ;
// Get first point name and time info
let firstPointName = '' ;
@@ -627,11 +738,12 @@ function addLegend() {
html += `
<div class="legend-item" style="flex-direction: column; align-items: flex-start; margin-bottom: 8px;">
<div style="display: flex; align-items: center;">
<img src="{% static "leaflet-markers/img/marker-icon-" %} ${ markerColor } .png"
style="width: 18px; height: 30px; margin-right: 6px;">
<div style="width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; margin-right: 6px;">
${ shapePreview }
</div>
<span><strong>ID ${ source . source _id } :</strong> ${ firstPointName } </span>
</div>
<div style="margin-left: 24 px;">
<div style="margin-left: 26 px;">
<small style="color: #666;"> ${ source . points _count } точек</small>
${ timeInfo }
</div>
@@ -639,31 +751,6 @@ function addLegend() {
` ;
} ) ;
html += '<div class="legend-section"><strong>Маркеры:</strong></div>' ;
html += `
<div class="legend-item">
<img src="{% static "leaflet-markers/img/marker-icon-green.png" %}"
style="width: 18px; height: 30px; margin-right: 6px;">
<span>Начальная точка</span>
</div>
<div class="legend-item">
<img src="{% static "leaflet-markers/img/marker-icon-red.png" %}"
style="width: 18px; height: 30px; margin-right: 6px;">
<span>Конечная точка</span>
</div>
<div class="legend-item">
<img src="{% static "leaflet-markers/img/marker-icon-grey.png" %}"
style="width: 18px; height: 30px; margin-right: 6px;">
<span>Промежуточная точка</span>
</div>
<div class="legend-item">
<div style="width: 18px; height: 18px; border-radius: 50%; background: #007bff; border: 3px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.4); margin-right: 6px;"></div>
<span>Движущийся объект (цвет по объекту)</span>
</div>
` ;
html += '<div class="legend-section"><small style="color: #666;">В с е объекты движутся<br>с одинаковой скоростью</small></div>' ;
div . innerHTML = html ;
return div ;
} ;