Подправил маркеры на карте

This commit is contained in:
2025-12-03 11:47:41 +03:00
parent d7d85ac834
commit 51eb5f3732
7 changed files with 358 additions and 243 deletions

View File

@@ -120,6 +120,32 @@
.moving-marker { .moving-marker {
transition: transform 0.1s linear; transition: transform 0.1s linear;
} }
.marker-size-control {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 999;
background: white;
padding: 10px 12px;
border-radius: 4px;
box-shadow: 0 1px 5px rgba(0,0,0,0.3);
font-size: 11px;
}
.marker-size-control label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.marker-size-control input[type="range"] {
width: 120px;
cursor: pointer;
}
.marker-size-control .size-value {
display: inline-block;
margin-left: 5px;
font-weight: bold;
color: #007bff;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -133,6 +159,12 @@
<div id="map"></div> <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;"> <div class="playback-control" id="playbackControl" style="display: none;">
<button id="playBtn" title="Воспроизвести"></button> <button id="playBtn" title="Воспроизвести"></button>
<button id="pauseBtn" title="Пауза" disabled></button> <button id="pauseBtn" title="Пауза" disabled></button>
@@ -195,7 +227,7 @@ let currentMarkers = {};
let trailPolylines = {}; let trailPolylines = {};
let staticMarkers = {}; let staticMarkers = {};
// Color mapping // Color mapping - расширенная палитра
const colorMap = { const colorMap = {
'red': '#dc3545', 'red': '#dc3545',
'blue': '#007bff', 'blue': '#007bff',
@@ -204,41 +236,85 @@ const colorMap = {
'orange': '#fd7e14', 'orange': '#fd7e14',
'cyan': '#17a2b8', 'cyan': '#17a2b8',
'magenta': '#e83e8c', 'magenta': '#e83e8c',
'yellow': '#ffc107', 'pink': '#ff69b4',
'lime': '#32cd32', 'teal': '#20c997',
'pink': '#ff69b4' 'indigo': '#6610f2',
'brown': '#8b4513',
'navy': '#000080',
'maroon': '#800000',
'olive': '#808000',
'coral': '#ff7f50',
'turquoise': '#40e0d0'
}; };
// Create marker icon using leaflet-markers library for static markers // Shape mapping - разные формы маркеров
function createStaticMarkerIcon(type) { const shapeMap = {
let markerColor = 'grey'; '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') { if (type === 'start') {
markerColor = 'green'; baseSize = 14; // Начальная точка чуть больше
} else if (type === 'end') { } else if (type === 'end') {
markerColor = 'red'; baseSize = 14; // Конечная точка чуть больше
} else if (type === 'intermediate') { } else if (type === 'intermediate') {
markerColor = 'grey'; baseSize = 10; // Промежуточные точки меньше
} }
return L.icon({ const size = Math.round(baseSize * markerSizeMultiplier);
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + markerColor + '.png', const shapeFunc = shapeMap[shape] || shapeMap['circle'];
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41], return L.divIcon({
iconAnchor: [12, 41], className: 'static-marker',
popupAnchor: [1, -34], iconSize: [size, size],
shadowSize: [41, 41] iconAnchor: [size/2, size/2],
popupAnchor: [0, -size/2],
html: shapeFunc(hexColor, size)
}); });
} }
// Create moving marker icon (colored circle) // Create moving marker icon (current position - larger and more prominent)
function createMovingMarkerIcon(color) { function createMovingMarkerIcon(color, shape) {
const hexColor = colorMap[color] || color; const hexColor = colorMap[color] || color;
const baseSize = 16; // Движущийся маркер немного больше
const size = Math.round(baseSize * markerSizeMultiplier);
const shapeFunc = shapeMap[shape] || shapeMap['circle'];
return L.divIcon({ return L.divIcon({
className: 'current-marker', className: 'current-marker moving-marker',
iconSize: [18, 18], iconSize: [size, size],
iconAnchor: [9, 9], iconAnchor: [size/2, size/2],
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>` popupAnchor: [0, -size/2],
html: shapeFunc(hexColor, size)
}); });
} }
@@ -335,21 +411,34 @@ function updateDisplay(progress) {
sourcesData.forEach(source => { sourcesData.forEach(source => {
const pos = getPositionAtProgress(source, progress); const pos = getPositionAtProgress(source, progress);
const color = source.color; const color = source.color;
const shape = source.shape;
if (pos) { if (pos) {
// Show/update current marker (moving object - colored circle) // Check if we've reached the end
if (!currentMarkers[source.source_id]) { const isAtEnd = (progress >= 1 || pos.pointIndex === source.points.length - 1) && pos.segmentProgress >= 0.99;
// Get first point name for popup
const firstPointName = source.points && source.points.length > 0 ? source.points[0].name : ''; // Show/update current marker (moving object) - hide when at end
const popupContent = `<b>ID ${source.source_id}:</b> ${firstPointName}<br><i style="color: #666;">Текущая позиция</i>`; if (isAtEnd) {
// Remove moving marker when at end point
currentMarkers[source.source_id] = L.marker([pos.lat, pos.lng], { if (currentMarkers[source.source_id]) {
icon: createMovingMarkerIcon(color), sourceLayerGroups[source.source_id].removeLayer(currentMarkers[source.source_id]);
zIndexOffset: 1000 delete currentMarkers[source.source_id];
}).bindPopup(popupContent); }
sourceLayerGroups[source.source_id].addLayer(currentMarkers[source.source_id]);
} else { } 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) // Update trail (dashed line showing path)
@@ -386,7 +475,7 @@ function updateDisplay(progress) {
} }
const marker = L.marker([point.lat, point.lng], { const marker = L.marker([point.lat, point.lng], {
icon: createStaticMarkerIcon(markerType), icon: createStaticMarkerIcon(markerType, color, shape),
zIndexOffset: markerType === 'start' ? 500 : (markerType === 'end' ? 600 : 100) 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)}`); }).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) { if (!staticMarkers[endMarkerKey] && source.points.length > 1) {
const lastPoint = source.points[source.points.length - 1]; const lastPoint = source.points[source.points.length - 1];
const endMarker = L.marker([lastPoint.lat, lastPoint.lng], { const endMarker = L.marker([lastPoint.lat, lastPoint.lng], {
icon: createStaticMarkerIcon('end'), icon: createStaticMarkerIcon('end', color, shape),
zIndexOffset: 600 zIndexOffset: 600
}).bindPopup(`<b>${source.source_name}</b><br><span style="color: red;">■ Конец</span><br>${lastPoint.name}<br>${lastPoint.frequency}<br>${formatDate(lastPoint.timestamp)}`); }).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); sourceLayerGroups[source.source_id].addLayer(endMarker);
@@ -422,19 +511,21 @@ function resetPlayback() {
// Recreate trails // Recreate trails
sourcesData.forEach(source => { sourcesData.forEach(source => {
const color = colorMap[source.color] || source.color; const color = colorMap[source.color] || source.color;
const shape = source.shape;
trailPolylines[source.source_id] = L.polyline([], { trailPolylines[source.source_id] = L.polyline([], {
color: color, color: color,
weight: 3, weight: 2, // Уменьшенная толщина линии
opacity: 0.7, opacity: 0.7,
dashArray: '5, 10' dashArray: '5, 10'
}); });
sourceLayerGroups[source.source_id].addLayer(trailPolylines[source.source_id]); 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) { if (source.points && source.points.length > 0) {
const firstPoint = source.points[0]; const firstPoint = source.points[0];
const startMarker = L.marker([firstPoint.lat, firstPoint.lng], { const startMarker = L.marker([firstPoint.lat, firstPoint.lng], {
icon: createStaticMarkerIcon('start'), icon: createStaticMarkerIcon('start', color, shape),
zIndexOffset: 500 zIndexOffset: 500
}).bindPopup(`<b>${source.source_name}</b><br><span style="color: green;">▶ Начало</span><br>${firstPoint.name}<br>${firstPoint.frequency}<br>${formatDate(firstPoint.timestamp)}`); }).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); sourceLayerGroups[source.source_id].addLayer(startMarker);
@@ -446,6 +537,16 @@ function resetPlayback() {
updateDisplay(currentProgress); 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 // Play animation
function play() { function play() {
if (playbackInterval) return; if (playbackInterval) return;
@@ -493,6 +594,11 @@ async function loadData() {
// Filter out sources with no points // Filter out sources with no points
sourcesData = sourcesData.filter(s => s.points && s.points.length > 0); 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) { if (!sourcesData || sourcesData.length === 0) {
document.getElementById('loadingOverlay').innerHTML = ` document.getElementById('loadingOverlay').innerHTML = `
<div class="alert alert-warning"> <div class="alert alert-warning">
@@ -567,6 +673,21 @@ async function loadData() {
speedMultiplier = parseFloat(this.value); 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) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
document.getElementById('loadingOverlay').innerHTML = ` document.getElementById('loadingOverlay').innerHTML = `
@@ -593,22 +714,12 @@ function addLegend() {
sourcesData.forEach(source => { sourcesData.forEach(source => {
const color = colorMap[source.color] || source.color; const color = colorMap[source.color] || source.color;
const shape = source.shape;
const points = source.points; const points = source.points;
// Get marker color for this source // Create shape preview
const markerColorMap = { const shapeFunc = shapeMap[shape] || shapeMap['circle'];
'red': 'red', const shapePreview = shapeFunc(color, 16);
'blue': 'blue',
'green': 'green',
'purple': 'violet',
'orange': 'orange',
'cyan': 'blue',
'magenta': 'red',
'yellow': 'yellow',
'lime': 'green',
'pink': 'red'
};
const markerColor = markerColorMap[source.color] || 'blue';
// Get first point name and time info // Get first point name and time info
let firstPointName = ''; let firstPointName = '';
@@ -627,11 +738,12 @@ function addLegend() {
html += ` html += `
<div class="legend-item" style="flex-direction: column; align-items: flex-start; margin-bottom: 8px;"> <div class="legend-item" style="flex-direction: column; align-items: flex-start; margin-bottom: 8px;">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="{% static "leaflet-markers/img/marker-icon-" %}${markerColor}.png" <div style="width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; margin-right: 6px;">
style="width: 18px; height: 30px; margin-right: 6px;"> ${shapePreview}
</div>
<span><strong>ID ${source.source_id}:</strong> ${firstPointName}</span> <span><strong>ID ${source.source_id}:</strong> ${firstPointName}</span>
</div> </div>
<div style="margin-left: 24px;"> <div style="margin-left: 26px;">
<small style="color: #666;">${source.points_count} точек</small> <small style="color: #666;">${source.points_count} точек</small>
${timeInfo} ${timeInfo}
</div> </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; div.innerHTML = html;
return div; return div;
}; };

View File

@@ -124,7 +124,7 @@
</div> </div>
<div class="averaging-container"> <div class="averaging-container">
<h2>Усреднение точек по источникам</h2> <h2>Усреднение точек по объектам</h2>
<div class="form-section"> <div class="form-section">
<div class="row"> <div class="row">
@@ -156,7 +156,7 @@
<div class="table-section"> <div class="table-section">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<h5>Источники <span id="source-count" class="badge bg-primary">0</span></h5> <h5>Объекты <span id="source-count" class="badge bg-primary">0</span></h5>
</div> </div>
<div class="btn-group-custom"> <div class="btn-group-custom">
<button id="export-xlsx" class="btn btn-success" disabled> <button id="export-xlsx" class="btn btn-success" disabled>
@@ -180,7 +180,7 @@
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника</h5> <h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body" id="modal-body-content"> <div class="modal-body" id="modal-body-content">
@@ -279,7 +279,7 @@ document.addEventListener('DOMContentLoaded', function() {
{column: "frequency", dir: "asc"} {column: "frequency", dir: "asc"}
], ],
columns: [ columns: [
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2}, {title: "Объект", field: "source_name", minWidth: 180, widthGrow: 2},
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"}, {title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"}, {title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"}, {title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
@@ -327,7 +327,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Delete source // Delete source
function deleteSource(sourceIdx) { function deleteSource(sourceIdx) {
//if (!confirm('Удалить этот источник со всеми группами?')) return; //if (!confirm('Удалить этот объект со всеми группами?')) return;
allSourcesData.splice(sourceIdx, 1); allSourcesData.splice(sourceIdx, 1);
updateSourcesTable(); updateSourcesTable();
} }
@@ -338,7 +338,7 @@ document.addEventListener('DOMContentLoaded', function() {
const source = allSourcesData[sourceIdx]; const source = allSourcesData[sourceIdx];
if (!source) return; if (!source) return;
document.getElementById('sourceDetailsModalLabel').textContent = `Источник: ${source.source_name}`; document.getElementById('sourceDetailsModalLabel').textContent = `Объект: ${source.source_name}`;
renderModalContent(); renderModalContent();
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal')); const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
@@ -619,7 +619,7 @@ document.addEventListener('DOMContentLoaded', function() {
allSourcesData.forEach(source => { allSourcesData.forEach(source => {
source.groups.forEach(group => { source.groups.forEach(group => {
summaryData.push({ summaryData.push({
'Источник': source.source_name, 'Объект': source.source_name,
'Частота, МГц': group.frequency, 'Частота, МГц': group.frequency,
'Полоса, МГц': group.freq_range, 'Полоса, МГц': group.freq_range,
'Символьная скорость, БОД': group.bod_velocity, 'Символьная скорость, БОД': group.bod_velocity,
@@ -646,7 +646,7 @@ document.addEventListener('DOMContentLoaded', function() {
source.groups.forEach(group => { source.groups.forEach(group => {
group.points.forEach(point => { group.points.forEach(point => {
allPointsData.push({ allPointsData.push({
'Источник': source.source_name, 'Объект': source.source_name,
'ID точки': point.id, 'ID точки': point.id,
'Имя точки': point.name, 'Имя точки': point.name,
'Частота, МГц': point.frequency, 'Частота, МГц': point.frequency,

View File

@@ -216,7 +216,7 @@
filterPolygon.addTo(map); filterPolygon.addTo(map);
// Добавляем popup с информацией // Добавляем popup с информацией
filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только источники с точками в этой области'); filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только объекты с точками в этой области');
// Если нет других точек, центрируем карту на полигоне // Если нет других точек, центрируем карту на полигоне
{% if not groups %} {% if not groups %}

View File

@@ -154,7 +154,7 @@ class ShowSourcesMapView(LoginRequiredMixin, View):
points.append( points.append(
{ {
"point": (coords.x, coords.y), # (lon, lat) "point": (coords.x, coords.y), # (lon, lat)
"source_id": f"Источник #{source.id}", "source_id": f"Объект #{source.id}",
} }
) )

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-12-03 07:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0017_add_satellite_alternative_name'),
('mapsapp', '0002_alter_transponders_snr'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
]

View File

@@ -1,149 +1,150 @@
# Django imports # Django imports
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import ExpressionWrapper, F from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs from django.db.models.functions import Abs
# Local imports # Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser
class Transponders(models.Model): class Transponders(models.Model):
""" """
Модель транспондера спутника. Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации. Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
""" """
# Основные поля # Основные поля
name = models.CharField( name = models.CharField(
max_length=30, max_length=30,
null=True, null=True,
blank=True, blank=True,
verbose_name="Название транспондера", verbose_name="Название транспондера",
db_index=True, db_index=True,
help_text="Название транспондера", help_text="Название транспондера",
) )
downlink = models.FloatField( downlink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Downlink", verbose_name="Downlink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота downlink в МГц (0-50000)" # help_text="Частота downlink в МГц (0-50000)"
) )
frequency_range = models.FloatField( frequency_range = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
# help_text="Полоса частот в МГц (0-1000)" # help_text="Полоса частот в МГц (0-1000)"
) )
uplink = models.FloatField( uplink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Uplink", verbose_name="Uplink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота uplink в МГц (0-50000)" # help_text="Частота uplink в МГц (0-50000)"
) )
zone_name = models.CharField( zone_name = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
verbose_name="Название зоны", verbose_name="Название зоны",
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера", help_text="Название зоны покрытия транспондера",
) )
snr = models.FloatField( snr = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="ОСШ, дБ", verbose_name="ОСШ, дБ",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Отношение сигнал/шум в децибелах", help_text="Отношение сигнал/шум в децибелах",
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name="Дата создания", verbose_name="Дата создания",
help_text="Дата и время создания записи", help_text="Дата и время создания записи",
) )
created_by = models.ForeignKey( created_by = models.ForeignKey(
CustomUser, CustomUser,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="transponder_created", related_name="transponder_created",
null=True, null=True,
blank=True, blank=True,
verbose_name="Создан пользователем", verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись", help_text="Пользователь, создавший запись",
) )
updated_at = models.DateTimeField( updated_at = models.DateTimeField(
auto_now=True, auto_now=True,
verbose_name="Дата последнего изменения", verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения", help_text="Дата и время последнего изменения",
) )
updated_by = models.ForeignKey( updated_by = models.ForeignKey(
CustomUser, CustomUser,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="transponder_updated", related_name="transponder_updated",
null=True, null=True,
blank=True, blank=True,
verbose_name="Изменен пользователем", verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись", help_text="Пользователь, последним изменивший запись",
) )
# Связи # Связи
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, Polarization,
default=get_default_polarization, default=get_default_polarization,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
related_name="tran_polarizations", related_name="tran_polarizations",
null=True, null=True,
blank=True, blank=True,
verbose_name="Поляризация", verbose_name="Поляризация",
help_text="Поляризация сигнала", help_text="Поляризация сигнала",
) )
sat_id = models.ForeignKey( sat_id = models.ForeignKey(
Satellite, Satellite,
on_delete=models.PROTECT, on_delete=models.SET_NULL,
related_name="tran_satellite", null=True,
verbose_name="Спутник", related_name="tran_satellite",
db_index=True, verbose_name="Спутник",
help_text="Спутник, которому принадлежит транспондер", db_index=True,
) help_text="Спутник, которому принадлежит транспондер",
)
# Вычисляемые поля
transfer = models.GeneratedField( # Вычисляемые поля
expression=ExpressionWrapper( transfer = models.GeneratedField(
Abs(F("downlink") - F("uplink")), output_field=models.FloatField() expression=ExpressionWrapper(
), Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
output_field=models.FloatField(), ),
db_persist=True, output_field=models.FloatField(),
null=True, db_persist=True,
blank=True, null=True,
verbose_name="Перенос", blank=True,
) verbose_name="Перенос",
)
# def clean(self):
# """Валидация на уровне модели""" # def clean(self):
# super().clean() # """Валидация на уровне модели"""
# super().clean()
# # Проверка что downlink и uplink заданы
# if self.downlink and self.uplink: # # Проверка что downlink и uplink заданы
# # Обычно uplink выше downlink для спутниковой связи # if self.downlink and self.uplink:
# if self.uplink < self.downlink: # # Обычно uplink выше downlink для спутниковой связи
# raise ValidationError({ # if self.uplink < self.downlink:
# 'uplink': 'Частота uplink обычно выше частоты downlink' # raise ValidationError({
# }) # 'uplink': 'Частота uplink обычно выше частоты downlink'
# })
def __str__(self):
if self.name: def __str__(self):
return self.name if self.name:
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}" return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер" class Meta:
verbose_name_plural = "Транспондеры" verbose_name = "Транспондер"
ordering = ["sat_id", "downlink"] verbose_name_plural = "Транспондеры"
indexes = [ ordering = ["sat_id", "downlink"]
models.Index(fields=["sat_id", "downlink"]), indexes = [
models.Index(fields=["sat_id", "zone_name"]), models.Index(fields=["sat_id", "downlink"]),
] models.Index(fields=["sat_id", "zone_name"]),
]

View File

@@ -64,5 +64,12 @@ def test_celery_connection():
print("=" * 60) print("=" * 60)
return True return True
if __name__ == "__main__": # if __name__ == "__main__":
test_celery_connection() # test_celery_connection()
import requests
url = f"https://www.lyngsat.com/europe.html"
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
response = requests.post("http://localhost:8191/v1", json=payload)
print(response.content)