Добавил форму для отправки данных

This commit is contained in:
2025-11-26 17:35:59 +03:00
parent 27694a3a7d
commit cfaaae9360
7 changed files with 671 additions and 152 deletions

View File

@@ -0,0 +1,290 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Ввод данных{% endblock %}
{% block extra_css %}
<link href="https://unpkg.com/tabulator-tables@6.2.5/dist/css/tabulator_bootstrap5.min.css" rel="stylesheet">
<style>
.data-entry-container {
padding: 20px;
}
.form-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.table-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#data-table {
margin-top: 20px;
font-size: 12px;
}
#data-table .tabulator-header {
font-size: 12px;
white-space: normal;
word-wrap: break-word;
}
#data-table .tabulator-header .tabulator-col {
white-space: normal;
word-wrap: break-word;
height: auto;
min-height: 40px;
}
#data-table .tabulator-header .tabulator-col-content {
white-space: normal;
word-wrap: break-word;
padding: 6px 4px;
}
#data-table .tabulator-cell {
font-size: 12px;
padding: 6px 4px;
}
.btn-group-custom {
margin-top: 15px;
}
.input-field {
font-family: monospace;
}
</style>
{% endblock %}
{% block content %}
<div class="data-entry-container">
<h2>Ввод данных точек спутников</h2>
<div class="form-section">
<div class="row">
<div class="col-md-4 mb-3">
<label for="satellite-select" class="form-label">Спутник</label>
<select id="satellite-select" class="form-select">
<option value="">Выберите спутник</option>
{% for satellite in satellites %}
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-8 mb-3">
<label for="data-input" class="form-label">Данные</label>
<input type="text" id="data-input" class="form-control input-field"
placeholder="Вставьте строку данных и нажмите Enter">
</div>
</div>
</div>
<div class="table-section">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5>Таблица данных <span id="row-count" class="badge bg-primary">0</span></h5>
</div>
<div class="btn-group-custom">
<button id="export-xlsx" class="btn btn-success">
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
</button>
<button id="clear-table" class="btn btn-danger ms-2">
<i class="bi bi-trash"></i> Очистить таблицу
</button>
</div>
</div>
<div id="data-table"></div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/tabulator-tables@6.2.5/dist/js/tabulator.min.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Tabulator
const table = new Tabulator("#data-table", {
layout: "fitDataStretch",
height: "500px",
placeholder: "Нет данных. Введите данные в поле выше и нажмите Enter.",
headerWordWrap: true,
columns: [
{title: "Объект наблюдения", field: "object_name", minWidth: 180, widthGrow: 2, editor: "input"},
{title: "Частота, МГц", field: "frequency", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 120, widthGrow: 1.5, editor: "input"},
{title: "Модуляция", field: "modulation", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "ОСШ", field: "snr", minWidth: 70, widthGrow: 0.8, editor: "input"},
{title: "Дата", field: "date", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Время", field: "time", minWidth: 90, widthGrow: 1, editor: "input"},
{title: "Зеркала", field: "mirrors", minWidth: 130, widthGrow: 1.5, editor: "input"},
{title: "Местоопределение", field: "location", minWidth: 130, widthGrow: 1.5, editor: "input"},
{title: "Координаты", field: "coordinates", minWidth: 150, widthGrow: 2, editor: "input"},
],
data: [],
});
// Update row count
function updateRowCount() {
const count = table.getDataCount();
document.getElementById('row-count').textContent = count;
}
// Listen to table events
table.on("rowAdded", updateRowCount);
table.on("dataChanged", updateRowCount);
// Parse input string
function parseInputString(inputStr) {
const parts = inputStr.split(';');
if (parts.length < 5) {
return null;
}
// Parse date and time (first part)
const dateTimePart = parts[0].trim();
const dateTimeMatch = dateTimePart.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}:\d{2})/);
if (!dateTimeMatch) {
return null;
}
const date = dateTimeMatch[1];
const time = dateTimeMatch[2];
// Parse object name (second part)
const objectName = parts[1].trim();
// Parse location (fourth part - "Позиция")
// const location = parts[3].trim() || '-';
const location = '-';
// Parse coordinates (fifth part)
const coordsPart = parts[4].trim();
const coordsMatch = coordsPart.match(/([-\d,]+)\s+([-\d,]+)/);
let coordinates = '-';
if (coordsMatch) {
const lat = coordsMatch[1].replace(',', '.');
const lon = coordsMatch[2].replace(',', '.');
coordinates = `${lat}, ${lon}`;
}
return {
date: date,
time: time,
object_name: objectName,
location: location,
coordinates: coordinates,
};
}
// Search for ObjItem data
async function searchObjItemData(objectName, satelliteId) {
try {
const params = new URLSearchParams({
name: objectName,
});
if (satelliteId) {
params.append('satellite_id', satelliteId);
}
const response = await fetch(`/api/search-objitem/?${params.toString()}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Error searching ObjItem:', error);
return { found: false };
}
}
// Handle input
const dataInput = document.getElementById('data-input');
const satelliteSelect = document.getElementById('satellite-select');
dataInput.addEventListener('keypress', async function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const inputValue = this.value.trim();
if (!inputValue) {
alert('Введите данные');
return;
}
// Disable input while processing
this.disabled = true;
try {
// Parse input
const parsedData = parseInputString(inputValue);
if (!parsedData) {
alert('Неверный формат данных. Проверьте формат строки.');
return;
}
// Search for ObjItem data
const satelliteId = satelliteSelect.value;
const objItemData = await searchObjItemData(parsedData.object_name, satelliteId);
// Show warning if object not found
if (!objItemData.found) {
console.warn('Объект не найден в базе данных:', parsedData.object_name);
}
// Prepare row data
const rowData = {
object_name: parsedData.object_name || '-',
date: parsedData.date || '-',
time: parsedData.time || '-',
location: parsedData.location || '-',
coordinates: parsedData.coordinates || '-',
frequency: objItemData.found && objItemData.frequency !== null ? objItemData.frequency : '-',
freq_range: objItemData.found && objItemData.freq_range !== null ? objItemData.freq_range : '-',
bod_velocity: objItemData.found && objItemData.bod_velocity !== null ? objItemData.bod_velocity : '-',
modulation: objItemData.found && objItemData.modulation !== null ? objItemData.modulation : '-',
snr: objItemData.found && objItemData.snr !== null ? objItemData.snr : '-',
mirrors: objItemData.found && objItemData.mirrors !== null ? objItemData.mirrors : '-',
};
// Add row to table
table.addRow(rowData);
// Clear input
this.value = '';
} catch (error) {
console.error('Ошибка при обработке данных:', error);
alert('Произошла ошибка при обработке данных. Проверьте консоль для деталей.');
} finally {
// Re-enable input
this.disabled = false;
this.focus();
}
}
});
// Export to Excel
document.getElementById('export-xlsx').addEventListener('click', function() {
table.download("xlsx", "data_export.xlsx", {sheetName: "Данные"});
});
// Clear table
document.getElementById('clear-table').addEventListener('click', function() {
if (confirm('Вы уверены, что хотите очистить таблицу?')) {
table.clearData();
updateRowCount();
}
});
// Initialize row count
updateRowCount();
});
</script>
{% endblock %}

View File

@@ -307,7 +307,7 @@
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Сим. скор." field="bod_velocity" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}

View File

@@ -274,17 +274,26 @@ const transpondersData = {{ transponders|safe }};
// Chart state // Chart state
let canvas, ctx, container; let canvas, ctx, container;
let zoomLevel = 1; let zoomLevelUL = 1;
let panOffset = 0; let zoomLevelDL = 1;
let panOffsetUL = 0;
let panOffsetDL = 0;
let isDragging = false; let isDragging = false;
let dragStartX = 0; let dragStartX = 0;
let dragStartOffset = 0; let dragStartOffsetUL = 0;
let dragStartOffsetDL = 0;
let dragArea = null; // 'uplink' or 'downlink'
let hoveredTransponder = null; let hoveredTransponder = null;
let transponderRects = []; let transponderRects = [];
// Frequency range // Frequency ranges for uplink and downlink
let minFreq, maxFreq, freqRange; let minFreqUL, maxFreqUL, freqRangeUL;
let originalMinFreq, originalMaxFreq, originalFreqRange; let minFreqDL, maxFreqDL, freqRangeDL;
let originalMinFreqUL, originalMaxFreqUL, originalFreqRangeUL;
let originalMinFreqDL, originalMaxFreqDL, originalFreqRangeDL;
// Layout variables (need to be global for event handlers)
let uplinkStartY, uplinkHeight, downlinkStartY, downlinkHeight;
function initializeFrequencyChart() { function initializeFrequencyChart() {
if (!transpondersData || transpondersData.length === 0) { if (!transpondersData || transpondersData.length === 0) {
@@ -297,36 +306,50 @@ function initializeFrequencyChart() {
container = canvas.parentElement; container = canvas.parentElement;
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Calculate frequency range (including both downlink and uplink) // Calculate frequency ranges separately for uplink and downlink
minFreq = Infinity; minFreqUL = Infinity;
maxFreq = -Infinity; maxFreqUL = -Infinity;
minFreqDL = Infinity;
maxFreqDL = -Infinity;
transpondersData.forEach(t => { transpondersData.forEach(t => {
// Downlink // Downlink
const dlStartFreq = t.downlink - (t.frequency_range / 2); const dlStartFreq = t.downlink - (t.frequency_range / 2);
const dlEndFreq = t.downlink + (t.frequency_range / 2); const dlEndFreq = t.downlink + (t.frequency_range / 2);
minFreq = Math.min(minFreq, dlStartFreq); minFreqDL = Math.min(minFreqDL, dlStartFreq);
maxFreq = Math.max(maxFreq, dlEndFreq); maxFreqDL = Math.max(maxFreqDL, dlEndFreq);
// Uplink (if exists) // Uplink (if exists)
if (t.uplink) { if (t.uplink) {
const ulStartFreq = t.uplink - (t.frequency_range / 2); const ulStartFreq = t.uplink - (t.frequency_range / 2);
const ulEndFreq = t.uplink + (t.frequency_range / 2); const ulEndFreq = t.uplink + (t.frequency_range / 2);
minFreq = Math.min(minFreq, ulStartFreq); minFreqUL = Math.min(minFreqUL, ulStartFreq);
maxFreq = Math.max(maxFreq, ulEndFreq); maxFreqUL = Math.max(maxFreqUL, ulEndFreq);
} }
}); });
// Add 2% padding // Add 2% padding for downlink
const padding = (maxFreq - minFreq) * 0.04; const paddingDL = (maxFreqDL - minFreqDL) * 0.04;
minFreq -= padding; minFreqDL -= paddingDL;
maxFreq += padding; maxFreqDL += paddingDL;
// Add 2% padding for uplink (if exists)
if (maxFreqUL !== -Infinity) {
const paddingUL = (maxFreqUL - minFreqUL) * 0.04;
minFreqUL -= paddingUL;
maxFreqUL += paddingUL;
}
// Store original values // Store original values
originalMinFreq = minFreq; originalMinFreqDL = minFreqDL;
originalMaxFreq = maxFreq; originalMaxFreqDL = maxFreqDL;
originalFreqRange = maxFreq - minFreq; originalFreqRangeDL = maxFreqDL - minFreqDL;
freqRange = originalFreqRange; freqRangeDL = originalFreqRangeDL;
originalMinFreqUL = minFreqUL;
originalMaxFreqUL = maxFreqUL;
originalFreqRangeUL = maxFreqUL - minFreqUL;
freqRangeUL = originalFreqRangeUL;
// Setup event listeners // Setup event listeners
canvas.addEventListener('wheel', handleWheel, { passive: false }); canvas.addEventListener('wheel', handleWheel, { passive: false });
@@ -359,10 +382,15 @@ function renderChart() {
// Layout constants // Layout constants
const leftMargin = 60; const leftMargin = 60;
const rightMargin = 20; const rightMargin = 20;
const topMargin = 40; const topMargin = 60;
const middleMargin = 60; // Space between UL and DL sections
const bottomMargin = 40; const bottomMargin = 40;
const chartWidth = width - leftMargin - rightMargin; const chartWidth = width - leftMargin - rightMargin;
const chartHeight = height - topMargin - bottomMargin; const availableHeight = height - topMargin - middleMargin - bottomMargin;
// Split available height between UL and DL
uplinkHeight = availableHeight * 0.48;
downlinkHeight = availableHeight * 0.48;
// Group transponders by polarization (use first letter only) // Group transponders by polarization (use first letter only)
const polarizationGroups = {}; const polarizationGroups = {};
@@ -377,56 +405,106 @@ function renderChart() {
}); });
const polarizations = Object.keys(polarizationGroups); const polarizations = Object.keys(polarizationGroups);
// Each polarization gets 2 rows (downlink + uplink) const rowHeightUL = uplinkHeight / polarizations.length;
const rowHeight = chartHeight / (polarizations.length * 2); const rowHeightDL = downlinkHeight / polarizations.length;
// Calculate visible frequency range with zoom and pan // Calculate visible frequency ranges with zoom and pan for UL
const visibleFreqRange = freqRange / zoomLevel; const visibleFreqRangeUL = freqRangeUL / zoomLevelUL;
const centerFreq = (minFreq + maxFreq) / 2; const centerFreqUL = (minFreqUL + maxFreqUL) / 2;
const visibleMinFreq = centerFreq - visibleFreqRange / 2 + panOffset; const visibleMinFreqUL = centerFreqUL - visibleFreqRangeUL / 2 + panOffsetUL;
const visibleMaxFreq = centerFreq + visibleFreqRange / 2 + panOffset; const visibleMaxFreqUL = centerFreqUL + visibleFreqRangeUL / 2 + panOffsetUL;
// Draw frequency axis // Calculate visible frequency ranges with zoom and pan for DL
const visibleFreqRangeDL = freqRangeDL / zoomLevelDL;
const centerFreqDL = (minFreqDL + maxFreqDL) / 2;
const visibleMinFreqDL = centerFreqDL - visibleFreqRangeDL / 2 + panOffsetDL;
const visibleMaxFreqDL = centerFreqDL + visibleFreqRangeDL / 2 + panOffsetDL;
uplinkStartY = topMargin;
downlinkStartY = topMargin + uplinkHeight + middleMargin;
// Draw UPLINK frequency axis
ctx.strokeStyle = '#dee2e6'; ctx.strokeStyle = '#dee2e6';
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(leftMargin, topMargin); ctx.moveTo(leftMargin, uplinkStartY);
ctx.lineTo(width - rightMargin, topMargin); ctx.lineTo(width - rightMargin, uplinkStartY);
ctx.stroke(); ctx.stroke();
// Draw frequency labels and grid // Draw UPLINK frequency labels and grid
ctx.fillStyle = '#6c757d'; ctx.fillStyle = '#6c757d';
ctx.font = '11px sans-serif'; ctx.font = '11px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
const numTicks = 10; const numTicks = 10;
for (let i = 0; i <= numTicks; i++) { for (let i = 0; i <= numTicks; i++) {
const freq = visibleMinFreq + (visibleMaxFreq - visibleMinFreq) * i / numTicks; const freq = visibleMinFreqUL + (visibleMaxFreqUL - visibleMinFreqUL) * i / numTicks;
const x = leftMargin + chartWidth * i / numTicks; const x = leftMargin + chartWidth * i / numTicks;
// Draw tick // Draw tick
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, topMargin); ctx.moveTo(x, uplinkStartY);
ctx.lineTo(x, topMargin - 5); ctx.lineTo(x, uplinkStartY - 5);
ctx.stroke(); ctx.stroke();
// Draw grid line // Draw grid line
ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)'; ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, topMargin); ctx.moveTo(x, uplinkStartY);
ctx.lineTo(x, height - bottomMargin); ctx.lineTo(x, uplinkStartY + uplinkHeight);
ctx.stroke(); ctx.stroke();
ctx.strokeStyle = '#dee2e6'; ctx.strokeStyle = '#dee2e6';
// Draw label // Draw label
ctx.fillText(freq.toFixed(1), x, topMargin - 10); ctx.fillText(freq.toFixed(1), x, uplinkStartY - 10);
} }
// Draw axis title // Draw UPLINK axis title
ctx.fillStyle = '#000'; ctx.fillStyle = '#000';
ctx.font = 'bold 12px sans-serif'; ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText('Частота (МГц)', width / 2, topMargin - 25); ctx.fillText('Uplink Частота (МГц)', width / 2, uplinkStartY - 25);
// Draw DOWNLINK frequency axis
ctx.strokeStyle = '#dee2e6';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(leftMargin, downlinkStartY);
ctx.lineTo(width - rightMargin, downlinkStartY);
ctx.stroke();
// Draw DOWNLINK frequency labels and grid
ctx.fillStyle = '#6c757d';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
for (let i = 0; i <= numTicks; i++) {
const freq = visibleMinFreqDL + (visibleMaxFreqDL - visibleMinFreqDL) * i / numTicks;
const x = leftMargin + chartWidth * i / numTicks;
// Draw tick
ctx.beginPath();
ctx.moveTo(x, downlinkStartY);
ctx.lineTo(x, downlinkStartY - 5);
ctx.stroke();
// Draw grid line
ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
ctx.beginPath();
ctx.moveTo(x, downlinkStartY);
ctx.lineTo(x, downlinkStartY + downlinkHeight);
ctx.stroke();
ctx.strokeStyle = '#dee2e6';
// Draw label
ctx.fillText(freq.toFixed(1), x, downlinkStartY - 10);
}
// Draw DOWNLINK axis title
ctx.fillStyle = '#000';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Downlink Частота (МГц)', width / 2, downlinkStartY - 25);
// Draw polarization label // Draw polarization label
ctx.save(); ctx.save();
@@ -442,102 +520,58 @@ function renderChart() {
// Draw transponders // Draw transponders
polarizations.forEach((pol, index) => { polarizations.forEach((pol, index) => {
const group = polarizationGroups[pol]; const group = polarizationGroups[pol];
const downlinkColor = '#0000ff'; //getColor(pol); const downlinkColor = '#0000ff';
const uplinkColor = '#fd7e14'; const uplinkColor = '#fd7e14';
// Downlink row // Uplink row (now on top)
const downlinkY = topMargin + (index * 2) * rowHeight; const uplinkY = uplinkStartY + index * rowHeightUL;
const downlinkBarHeight = rowHeight * 0.8; const uplinkBarHeight = rowHeightUL * 0.8;
const downlinkBarY = downlinkY + (rowHeight - downlinkBarHeight) / 2; const uplinkBarY = uplinkY + (rowHeightUL - uplinkBarHeight) / 2;
// Uplink row // Downlink row (now on bottom)
const uplinkY = topMargin + (index * 2 + 1) * rowHeight; const downlinkY = downlinkStartY + index * rowHeightDL;
const uplinkBarHeight = rowHeight * 0.8; const downlinkBarHeight = rowHeightDL * 0.8;
const uplinkBarY = uplinkY + (rowHeight - uplinkBarHeight) / 2; const downlinkBarY = downlinkY + (rowHeightDL - downlinkBarHeight) / 2;
// Draw polarization label (centered between downlink and uplink) // Draw polarization label for UL section
ctx.fillStyle = '#000'; ctx.fillStyle = '#000';
ctx.font = 'bold 14px sans-serif'; ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
const labelY = downlinkY + rowHeight; ctx.fillText(pol, leftMargin - 25, uplinkBarY + uplinkBarHeight / 2 + 4);
ctx.fillText(pol, leftMargin - 25, labelY);
// Draw "DL" and "UL" labels // Draw polarization label for DL section
ctx.font = '10px sans-serif'; ctx.fillText(pol, leftMargin - 25, downlinkBarY + downlinkBarHeight / 2 + 4);
ctx.fillStyle = '#666';
ctx.fillText('DL', leftMargin - 5, downlinkBarY + downlinkBarHeight / 2 + 3);
ctx.fillText('UL', leftMargin - 5, uplinkBarY + uplinkBarHeight / 2 + 3);
// Draw separator line between DL and UL // Draw separator lines between polarization groups
ctx.strokeStyle = '#dee2e6'; if (index < polarizations.length - 1) {
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(leftMargin, uplinkY); ctx.moveTo(leftMargin, uplinkY + rowHeightUL);
ctx.lineTo(width - rightMargin, uplinkY); ctx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
ctx.stroke(); ctx.stroke();
// Draw downlink transponders ctx.beginPath();
group.forEach(t => { ctx.moveTo(leftMargin, downlinkY + rowHeightDL);
const startFreq = t.downlink - (t.frequency_range / 2); ctx.lineTo(width - rightMargin, downlinkY + rowHeightDL);
const endFreq = t.downlink + (t.frequency_range / 2); ctx.stroke();
// Check if transponder is visible
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
return;
} }
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; // Draw uplink transponders (now first, on top)
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
const barWidth = x2 - x1;
if (barWidth < 1) return;
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
// Draw downlink bar
ctx.fillStyle = downlinkColor;
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw border (thicker if hovered)
ctx.strokeStyle = isHovered ? '#000' : '#fff';
ctx.lineWidth = isHovered ? 3 : 1;
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw name if there's space
if (barWidth > 40) {
ctx.fillStyle = '#fff';
ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
}
// Store for hover detection
transponderRects.push({
x: x1,
y: downlinkBarY,
width: barWidth,
height: downlinkBarHeight,
transponder: t,
type: 'downlink',
centerX: x1 + barWidth / 2
});
});
// Draw uplink transponders
group.forEach(t => { group.forEach(t => {
if (!t.uplink) return; // Skip if no uplink data if (!t.uplink) return; // Skip if no uplink data
const startFreq = t.uplink - (t.frequency_range / 2); const startFreq = t.uplink - (t.frequency_range / 2);
const endFreq = t.uplink + (t.frequency_range / 2); const endFreq = t.uplink + (t.frequency_range / 2);
// Check if transponder is visible // Check if transponder is visible in UL range
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) { if (endFreq < visibleMinFreqUL || startFreq > visibleMaxFreqUL) {
return; return;
} }
// Calculate position // Calculate position using UL axis
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; const x1 = leftMargin + ((startFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; const x2 = leftMargin + ((endFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
const barWidth = x2 - x1; const barWidth = x2 - x1;
// Skip if too small // Skip if too small
@@ -574,16 +608,53 @@ function renderChart() {
}); });
}); });
// Draw separator line after each polarization group (except last) // Draw downlink transponders (now second, on bottom)
if (index < polarizations.length - 1) { group.forEach(t => {
const separatorY = topMargin + (index * 2 + 2) * rowHeight; const startFreq = t.downlink - (t.frequency_range / 2);
ctx.strokeStyle = '#adb5bd'; const endFreq = t.downlink + (t.frequency_range / 2);
ctx.lineWidth = 2;
ctx.beginPath(); // Check if transponder is visible in DL range
ctx.moveTo(leftMargin, separatorY); if (endFreq < visibleMinFreqDL || startFreq > visibleMaxFreqDL) {
ctx.lineTo(width - rightMargin, separatorY); return;
ctx.stroke();
} }
// Calculate position using DL axis
const x1 = leftMargin + ((startFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
const barWidth = x2 - x1;
if (barWidth < 1) return;
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
// Draw downlink bar
ctx.fillStyle = downlinkColor;
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw border (thicker if hovered)
ctx.strokeStyle = isHovered ? '#000' : '#fff';
ctx.lineWidth = isHovered ? 3 : 1;
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw name if there's space
if (barWidth > 40) {
ctx.fillStyle = '#fff';
ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
}
// Store for hover detection
transponderRects.push({
x: x1,
y: downlinkBarY,
width: barWidth,
height: downlinkBarHeight,
transponder: t,
type: 'downlink',
centerX: x1 + barWidth / 2
});
});
}); });
// Draw connection line between downlink and uplink when hovering // Draw connection line between downlink and uplink when hovering
@@ -694,24 +765,50 @@ function drawTooltip(rectInfo) {
function handleWheel(e) { function handleWheel(e) {
e.preventDefault(); e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1; const rect = canvas.getBoundingClientRect();
const newZoom = Math.max(1, Math.min(20, zoomLevel * delta)); const mouseY = e.clientY - rect.top;
if (newZoom !== zoomLevel) { // Determine which area we're zooming
zoomLevel = newZoom; const isUplinkArea = mouseY < (uplinkStartY + uplinkHeight);
const delta = e.deltaY > 0 ? 0.9 : 1.1;
if (isUplinkArea) {
const newZoom = Math.max(1, Math.min(20, zoomLevelUL * delta));
if (newZoom !== zoomLevelUL) {
zoomLevelUL = newZoom;
// Adjust pan to keep center // Adjust pan to keep center
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel); const maxPan = (originalFreqRangeUL * (zoomLevelUL - 1)) / (2 * zoomLevelUL);
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset)); panOffsetUL = Math.max(-maxPan, Math.min(maxPan, panOffsetUL));
renderChart(); renderChart();
} }
} else {
const newZoom = Math.max(1, Math.min(20, zoomLevelDL * delta));
if (newZoom !== zoomLevelDL) {
zoomLevelDL = newZoom;
// Adjust pan to keep center
const maxPan = (originalFreqRangeDL * (zoomLevelDL - 1)) / (2 * zoomLevelDL);
panOffsetDL = Math.max(-maxPan, Math.min(maxPan, panOffsetDL));
renderChart();
}
}
} }
function handleMouseDown(e) { function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const mouseY = e.clientY - rect.top;
// Determine which area we're dragging
dragArea = mouseY < (uplinkStartY + uplinkHeight) ? 'uplink' : 'downlink';
isDragging = true; isDragging = true;
dragStartX = e.clientX; dragStartX = e.clientX;
dragStartOffset = panOffset; dragStartOffsetUL = panOffsetUL;
dragStartOffsetDL = panOffsetDL;
canvas.style.cursor = 'grabbing'; canvas.style.cursor = 'grabbing';
} }
@@ -722,12 +819,22 @@ function handleMouseMove(e) {
if (isDragging) { if (isDragging) {
const dx = e.clientX - dragStartX; const dx = e.clientX - dragStartX;
const freqPerPixel = (freqRange / zoomLevel) / (rect.width - 80);
panOffset = dragStartOffset - dx * freqPerPixel; if (dragArea === 'uplink') {
const freqPerPixel = (freqRangeUL / zoomLevelUL) / (rect.width - 80);
panOffsetUL = dragStartOffsetUL - dx * freqPerPixel;
// Limit pan // Limit pan
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel); const maxPan = (originalFreqRangeUL * (zoomLevelUL - 1)) / (2 * zoomLevelUL);
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset)); panOffsetUL = Math.max(-maxPan, Math.min(maxPan, panOffsetUL));
} else {
const freqPerPixel = (freqRangeDL / zoomLevelDL) / (rect.width - 80);
panOffsetDL = dragStartOffsetDL - dx * freqPerPixel;
// Limit pan
const maxPan = (originalFreqRangeDL * (zoomLevelDL - 1)) / (2 * zoomLevelDL);
panOffsetDL = Math.max(-maxPan, Math.min(maxPan, panOffsetDL));
}
renderChart(); renderChart();
} else { } else {
@@ -767,20 +874,27 @@ function handleMouseLeave() {
} }
function resetZoom() { function resetZoom() {
zoomLevel = 1; zoomLevelUL = 1;
panOffset = 0; zoomLevelDL = 1;
panOffsetUL = 0;
panOffsetDL = 0;
renderChart(); renderChart();
} }
function zoomIn() { function zoomIn() {
zoomLevel = Math.min(20, zoomLevel * 1.2); zoomLevelUL = Math.min(20, zoomLevelUL * 1.2);
zoomLevelDL = Math.min(20, zoomLevelDL * 1.2);
renderChart(); renderChart();
} }
function zoomOut() { function zoomOut() {
zoomLevel = Math.max(1, zoomLevel / 1.2); zoomLevelUL = Math.max(1, zoomLevelUL / 1.2);
if (zoomLevel === 1) { zoomLevelDL = Math.max(1, zoomLevelDL / 1.2);
panOffset = 0; if (zoomLevelUL === 1) {
panOffsetUL = 0;
}
if (zoomLevelDL === 1) {
panOffsetDL = 0;
} }
renderChart(); renderChart();
} }

View File

@@ -79,6 +79,9 @@
<i class="bi bi-plus-circle"></i> Создать <i class="bi bi-plus-circle"></i> Создать
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
<i class="bi bi-keyboard"></i> Ввод данных
</a>
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel"> <a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
<i class="bi bi-file-earmark-excel"></i> Excel <i class="bi bi-file-earmark-excel"></i> Excel
</a> </a>

View File

@@ -8,6 +8,7 @@ from .views import (
AddTranspondersView, AddTranspondersView,
# ClusterTestView, # ClusterTestView,
ClearLyngsatCacheView, ClearLyngsatCacheView,
DataEntryView,
DeleteSelectedObjectsView, DeleteSelectedObjectsView,
DeleteSelectedSourcesView, DeleteSelectedSourcesView,
DeleteSelectedTranspondersView, DeleteSelectedTranspondersView,
@@ -36,6 +37,7 @@ from .views import (
SatelliteListView, SatelliteListView,
SatelliteCreateView, SatelliteCreateView,
SatelliteUpdateView, SatelliteUpdateView,
SearchObjItemAPIView,
ShowMapView, ShowMapView,
ShowSelectedObjectsMapView, ShowSelectedObjectsMapView,
ShowSourcesMapView, ShowSourcesMapView,
@@ -118,5 +120,7 @@ urlpatterns = [
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'), path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
path('kubsat/', KubsatView.as_view(), name='kubsat'), path('kubsat/', KubsatView.as_view(), name='kubsat'),
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'), path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
path('logout/', custom_logout, name='logout'), path('logout/', custom_logout, name='logout'),
] ]

View File

@@ -59,6 +59,10 @@ from .kubsat import (
KubsatView, KubsatView,
KubsatExportView, KubsatExportView,
) )
from .data_entry import (
DataEntryView,
SearchObjItemAPIView,
)
__all__ = [ __all__ = [
# Base # Base
@@ -122,4 +126,7 @@ __all__ = [
# Kubsat # Kubsat
'KubsatView', 'KubsatView',
'KubsatExportView', 'KubsatExportView',
# Data Entry
'DataEntryView',
'SearchObjItemAPIView',
] ]

View File

@@ -0,0 +1,101 @@
"""
Data entry view for satellite points.
"""
import json
import re
from datetime import datetime
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render
from django.views import View
from ..models import ObjItem, Satellite
class DataEntryView(LoginRequiredMixin, View):
"""
View for data entry form with Tabulator table.
"""
def get(self, request):
# Get satellites that have points
satellites = Satellite.objects.filter(
parameters__objitem__isnull=False
).distinct().order_by('name')
context = {
'satellites': satellites,
"full_width_page": True
}
return render(request, 'mainapp/data_entry.html', context)
class SearchObjItemAPIView(LoginRequiredMixin, View):
"""
API endpoint for searching ObjItem by name.
Returns first matching ObjItem with all required data.
"""
def get(self, request):
name = request.GET.get('name', '').strip()
satellite_id = request.GET.get('satellite_id', '').strip()
if not name:
return JsonResponse({'error': 'Name parameter is required'}, status=400)
# Build query
query = Q(name__iexact=name)
# Add satellite filter if provided
if satellite_id:
try:
sat_id = int(satellite_id)
query &= Q(parameter_obj__id_satellite_id=sat_id)
except (ValueError, TypeError):
pass
# Search for ObjItem
objitem = ObjItem.objects.filter(query).select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj'
).prefetch_related(
'geo_obj__mirrors'
).first()
if not objitem:
return JsonResponse({'found': False})
# Prepare response data
data = {
'found': True,
'frequency': None,
'freq_range': None,
'bod_velocity': None,
'modulation': None,
'snr': None,
'mirrors': None,
}
# Get parameter data
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
param = objitem.parameter_obj
data['frequency'] = param.frequency if param.frequency else None
data['freq_range'] = param.freq_range if param.freq_range else None
data['bod_velocity'] = param.bod_velocity if param.bod_velocity else None
data['modulation'] = param.modulation.name if param.modulation else None
data['snr'] = param.snr if param.snr else None
# Get mirrors data
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
mirrors = objitem.geo_obj.mirrors.all()
if mirrors:
data['mirrors'] = ', '.join([m.name for m in mirrors])
return JsonResponse(data)