Добавил форму для отправки данных
This commit is contained in:
290
dbapp/mainapp/templates/mainapp/data_entry.html
Normal file
290
dbapp/mainapp/templates/mainapp/data_entry.html
Normal 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 %}
|
||||
@@ -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="freq_range" 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="snr" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
|
||||
|
||||
@@ -274,17 +274,26 @@ const transpondersData = {{ transponders|safe }};
|
||||
|
||||
// Chart state
|
||||
let canvas, ctx, container;
|
||||
let zoomLevel = 1;
|
||||
let panOffset = 0;
|
||||
let zoomLevelUL = 1;
|
||||
let zoomLevelDL = 1;
|
||||
let panOffsetUL = 0;
|
||||
let panOffsetDL = 0;
|
||||
let isDragging = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartOffset = 0;
|
||||
let dragStartOffsetUL = 0;
|
||||
let dragStartOffsetDL = 0;
|
||||
let dragArea = null; // 'uplink' or 'downlink'
|
||||
let hoveredTransponder = null;
|
||||
let transponderRects = [];
|
||||
|
||||
// Frequency range
|
||||
let minFreq, maxFreq, freqRange;
|
||||
let originalMinFreq, originalMaxFreq, originalFreqRange;
|
||||
// Frequency ranges for uplink and downlink
|
||||
let minFreqUL, maxFreqUL, freqRangeUL;
|
||||
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() {
|
||||
if (!transpondersData || transpondersData.length === 0) {
|
||||
@@ -297,36 +306,50 @@ function initializeFrequencyChart() {
|
||||
container = canvas.parentElement;
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// Calculate frequency range (including both downlink and uplink)
|
||||
minFreq = Infinity;
|
||||
maxFreq = -Infinity;
|
||||
// Calculate frequency ranges separately for uplink and downlink
|
||||
minFreqUL = Infinity;
|
||||
maxFreqUL = -Infinity;
|
||||
minFreqDL = Infinity;
|
||||
maxFreqDL = -Infinity;
|
||||
|
||||
transpondersData.forEach(t => {
|
||||
// Downlink
|
||||
const dlStartFreq = t.downlink - (t.frequency_range / 2);
|
||||
const dlEndFreq = t.downlink + (t.frequency_range / 2);
|
||||
minFreq = Math.min(minFreq, dlStartFreq);
|
||||
maxFreq = Math.max(maxFreq, dlEndFreq);
|
||||
minFreqDL = Math.min(minFreqDL, dlStartFreq);
|
||||
maxFreqDL = Math.max(maxFreqDL, dlEndFreq);
|
||||
|
||||
// Uplink (if exists)
|
||||
if (t.uplink) {
|
||||
const ulStartFreq = t.uplink - (t.frequency_range / 2);
|
||||
const ulEndFreq = t.uplink + (t.frequency_range / 2);
|
||||
minFreq = Math.min(minFreq, ulStartFreq);
|
||||
maxFreq = Math.max(maxFreq, ulEndFreq);
|
||||
minFreqUL = Math.min(minFreqUL, ulStartFreq);
|
||||
maxFreqUL = Math.max(maxFreqUL, ulEndFreq);
|
||||
}
|
||||
});
|
||||
|
||||
// Add 2% padding
|
||||
const padding = (maxFreq - minFreq) * 0.04;
|
||||
minFreq -= padding;
|
||||
maxFreq += padding;
|
||||
// Add 2% padding for downlink
|
||||
const paddingDL = (maxFreqDL - minFreqDL) * 0.04;
|
||||
minFreqDL -= paddingDL;
|
||||
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
|
||||
originalMinFreq = minFreq;
|
||||
originalMaxFreq = maxFreq;
|
||||
originalFreqRange = maxFreq - minFreq;
|
||||
freqRange = originalFreqRange;
|
||||
originalMinFreqDL = minFreqDL;
|
||||
originalMaxFreqDL = maxFreqDL;
|
||||
originalFreqRangeDL = maxFreqDL - minFreqDL;
|
||||
freqRangeDL = originalFreqRangeDL;
|
||||
|
||||
originalMinFreqUL = minFreqUL;
|
||||
originalMaxFreqUL = maxFreqUL;
|
||||
originalFreqRangeUL = maxFreqUL - minFreqUL;
|
||||
freqRangeUL = originalFreqRangeUL;
|
||||
|
||||
// Setup event listeners
|
||||
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
||||
@@ -359,10 +382,15 @@ function renderChart() {
|
||||
// Layout constants
|
||||
const leftMargin = 60;
|
||||
const rightMargin = 20;
|
||||
const topMargin = 40;
|
||||
const topMargin = 60;
|
||||
const middleMargin = 60; // Space between UL and DL sections
|
||||
const bottomMargin = 40;
|
||||
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)
|
||||
const polarizationGroups = {};
|
||||
@@ -377,56 +405,106 @@ function renderChart() {
|
||||
});
|
||||
|
||||
const polarizations = Object.keys(polarizationGroups);
|
||||
// Each polarization gets 2 rows (downlink + uplink)
|
||||
const rowHeight = chartHeight / (polarizations.length * 2);
|
||||
const rowHeightUL = uplinkHeight / polarizations.length;
|
||||
const rowHeightDL = downlinkHeight / polarizations.length;
|
||||
|
||||
// Calculate visible frequency range with zoom and pan
|
||||
const visibleFreqRange = freqRange / zoomLevel;
|
||||
const centerFreq = (minFreq + maxFreq) / 2;
|
||||
const visibleMinFreq = centerFreq - visibleFreqRange / 2 + panOffset;
|
||||
const visibleMaxFreq = centerFreq + visibleFreqRange / 2 + panOffset;
|
||||
// Calculate visible frequency ranges with zoom and pan for UL
|
||||
const visibleFreqRangeUL = freqRangeUL / zoomLevelUL;
|
||||
const centerFreqUL = (minFreqUL + maxFreqUL) / 2;
|
||||
const visibleMinFreqUL = centerFreqUL - visibleFreqRangeUL / 2 + panOffsetUL;
|
||||
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.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftMargin, topMargin);
|
||||
ctx.lineTo(width - rightMargin, topMargin);
|
||||
ctx.moveTo(leftMargin, uplinkStartY);
|
||||
ctx.lineTo(width - rightMargin, uplinkStartY);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw frequency labels and grid
|
||||
// Draw UPLINK frequency labels and grid
|
||||
ctx.fillStyle = '#6c757d';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const numTicks = 10;
|
||||
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;
|
||||
|
||||
// Draw tick
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, topMargin);
|
||||
ctx.lineTo(x, topMargin - 5);
|
||||
ctx.moveTo(x, uplinkStartY);
|
||||
ctx.lineTo(x, uplinkStartY - 5);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw grid line
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, topMargin);
|
||||
ctx.lineTo(x, height - bottomMargin);
|
||||
ctx.moveTo(x, uplinkStartY);
|
||||
ctx.lineTo(x, uplinkStartY + uplinkHeight);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = '#dee2e6';
|
||||
|
||||
// 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.font = 'bold 12px sans-serif';
|
||||
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
|
||||
ctx.save();
|
||||
@@ -442,102 +520,58 @@ function renderChart() {
|
||||
// Draw transponders
|
||||
polarizations.forEach((pol, index) => {
|
||||
const group = polarizationGroups[pol];
|
||||
const downlinkColor = '#0000ff'; //getColor(pol);
|
||||
const downlinkColor = '#0000ff';
|
||||
const uplinkColor = '#fd7e14';
|
||||
|
||||
// Downlink row
|
||||
const downlinkY = topMargin + (index * 2) * rowHeight;
|
||||
const downlinkBarHeight = rowHeight * 0.8;
|
||||
const downlinkBarY = downlinkY + (rowHeight - downlinkBarHeight) / 2;
|
||||
// Uplink row (now on top)
|
||||
const uplinkY = uplinkStartY + index * rowHeightUL;
|
||||
const uplinkBarHeight = rowHeightUL * 0.8;
|
||||
const uplinkBarY = uplinkY + (rowHeightUL - uplinkBarHeight) / 2;
|
||||
|
||||
// Uplink row
|
||||
const uplinkY = topMargin + (index * 2 + 1) * rowHeight;
|
||||
const uplinkBarHeight = rowHeight * 0.8;
|
||||
const uplinkBarY = uplinkY + (rowHeight - uplinkBarHeight) / 2;
|
||||
// Downlink row (now on bottom)
|
||||
const downlinkY = downlinkStartY + index * rowHeightDL;
|
||||
const downlinkBarHeight = rowHeightDL * 0.8;
|
||||
const downlinkBarY = downlinkY + (rowHeightDL - downlinkBarHeight) / 2;
|
||||
|
||||
// Draw polarization label (centered between downlink and uplink)
|
||||
// Draw polarization label for UL section
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
const labelY = downlinkY + rowHeight;
|
||||
ctx.fillText(pol, leftMargin - 25, labelY);
|
||||
ctx.fillText(pol, leftMargin - 25, uplinkBarY + uplinkBarHeight / 2 + 4);
|
||||
|
||||
// Draw "DL" and "UL" labels
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText('DL', leftMargin - 5, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||
ctx.fillText('UL', leftMargin - 5, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||
// Draw polarization label for DL section
|
||||
ctx.fillText(pol, leftMargin - 25, downlinkBarY + downlinkBarHeight / 2 + 4);
|
||||
|
||||
// Draw separator line between DL and UL
|
||||
ctx.strokeStyle = '#dee2e6';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftMargin, uplinkY);
|
||||
ctx.lineTo(width - rightMargin, uplinkY);
|
||||
ctx.stroke();
|
||||
// Draw separator lines between polarization groups
|
||||
if (index < polarizations.length - 1) {
|
||||
ctx.strokeStyle = '#adb5bd';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftMargin, uplinkY + rowHeightUL);
|
||||
ctx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw downlink transponders
|
||||
group.forEach(t => {
|
||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftMargin, downlinkY + rowHeightDL);
|
||||
ctx.lineTo(width - rightMargin, downlinkY + rowHeightDL);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Check if transponder is visible
|
||||
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||
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
|
||||
// Draw uplink transponders (now first, on top)
|
||||
group.forEach(t => {
|
||||
if (!t.uplink) return; // Skip if no uplink data
|
||||
|
||||
const startFreq = t.uplink - (t.frequency_range / 2);
|
||||
const endFreq = t.uplink + (t.frequency_range / 2);
|
||||
|
||||
// Check if transponder is visible
|
||||
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
|
||||
// Check if transponder is visible in UL range
|
||||
if (endFreq < visibleMinFreqUL || startFreq > visibleMaxFreqUL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||
// Calculate position using UL axis
|
||||
const x1 = leftMargin + ((startFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||
const x2 = leftMargin + ((endFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||
const barWidth = x2 - x1;
|
||||
|
||||
// Skip if too small
|
||||
@@ -574,16 +608,53 @@ function renderChart() {
|
||||
});
|
||||
});
|
||||
|
||||
// Draw separator line after each polarization group (except last)
|
||||
if (index < polarizations.length - 1) {
|
||||
const separatorY = topMargin + (index * 2 + 2) * rowHeight;
|
||||
ctx.strokeStyle = '#adb5bd';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftMargin, separatorY);
|
||||
ctx.lineTo(width - rightMargin, separatorY);
|
||||
ctx.stroke();
|
||||
}
|
||||
// Draw downlink transponders (now second, on bottom)
|
||||
group.forEach(t => {
|
||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||
|
||||
// Check if transponder is visible in DL range
|
||||
if (endFreq < visibleMinFreqDL || startFreq > visibleMaxFreqDL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -694,24 +765,50 @@ function drawTooltip(rectInfo) {
|
||||
function handleWheel(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// Determine which area we're zooming
|
||||
const isUplinkArea = mouseY < (uplinkStartY + uplinkHeight);
|
||||
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newZoom = Math.max(1, Math.min(20, zoomLevel * delta));
|
||||
|
||||
if (newZoom !== zoomLevel) {
|
||||
zoomLevel = newZoom;
|
||||
if (isUplinkArea) {
|
||||
const newZoom = Math.max(1, Math.min(20, zoomLevelUL * delta));
|
||||
if (newZoom !== zoomLevelUL) {
|
||||
zoomLevelUL = newZoom;
|
||||
|
||||
// Adjust pan to keep center
|
||||
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel);
|
||||
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset));
|
||||
// Adjust pan to keep center
|
||||
const maxPan = (originalFreqRangeUL * (zoomLevelUL - 1)) / (2 * zoomLevelUL);
|
||||
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) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// Determine which area we're dragging
|
||||
dragArea = mouseY < (uplinkStartY + uplinkHeight) ? 'uplink' : 'downlink';
|
||||
|
||||
isDragging = true;
|
||||
dragStartX = e.clientX;
|
||||
dragStartOffset = panOffset;
|
||||
dragStartOffsetUL = panOffsetUL;
|
||||
dragStartOffsetDL = panOffsetDL;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
@@ -722,12 +819,22 @@ function handleMouseMove(e) {
|
||||
|
||||
if (isDragging) {
|
||||
const dx = e.clientX - dragStartX;
|
||||
const freqPerPixel = (freqRange / zoomLevel) / (rect.width - 80);
|
||||
panOffset = dragStartOffset - dx * freqPerPixel;
|
||||
|
||||
// Limit pan
|
||||
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel);
|
||||
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset));
|
||||
if (dragArea === 'uplink') {
|
||||
const freqPerPixel = (freqRangeUL / zoomLevelUL) / (rect.width - 80);
|
||||
panOffsetUL = dragStartOffsetUL - dx * freqPerPixel;
|
||||
|
||||
// Limit pan
|
||||
const maxPan = (originalFreqRangeUL * (zoomLevelUL - 1)) / (2 * zoomLevelUL);
|
||||
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();
|
||||
} else {
|
||||
@@ -767,20 +874,27 @@ function handleMouseLeave() {
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
zoomLevel = 1;
|
||||
panOffset = 0;
|
||||
zoomLevelUL = 1;
|
||||
zoomLevelDL = 1;
|
||||
panOffsetUL = 0;
|
||||
panOffsetDL = 0;
|
||||
renderChart();
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
zoomLevel = Math.min(20, zoomLevel * 1.2);
|
||||
zoomLevelUL = Math.min(20, zoomLevelUL * 1.2);
|
||||
zoomLevelDL = Math.min(20, zoomLevelDL * 1.2);
|
||||
renderChart();
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
zoomLevel = Math.max(1, zoomLevel / 1.2);
|
||||
if (zoomLevel === 1) {
|
||||
panOffset = 0;
|
||||
zoomLevelUL = Math.max(1, zoomLevelUL / 1.2);
|
||||
zoomLevelDL = Math.max(1, zoomLevelDL / 1.2);
|
||||
if (zoomLevelUL === 1) {
|
||||
panOffsetUL = 0;
|
||||
}
|
||||
if (zoomLevelDL === 1) {
|
||||
panOffsetDL = 0;
|
||||
}
|
||||
renderChart();
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% 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">
|
||||
<i class="bi bi-file-earmark-excel"></i> Excel
|
||||
</a>
|
||||
|
||||
@@ -8,6 +8,7 @@ from .views import (
|
||||
AddTranspondersView,
|
||||
# ClusterTestView,
|
||||
ClearLyngsatCacheView,
|
||||
DataEntryView,
|
||||
DeleteSelectedObjectsView,
|
||||
DeleteSelectedSourcesView,
|
||||
DeleteSelectedTranspondersView,
|
||||
@@ -36,6 +37,7 @@ from .views import (
|
||||
SatelliteListView,
|
||||
SatelliteCreateView,
|
||||
SatelliteUpdateView,
|
||||
SearchObjItemAPIView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
@@ -118,5 +120,7 @@ urlpatterns = [
|
||||
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
|
||||
path('kubsat/', KubsatView.as_view(), name='kubsat'),
|
||||
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'),
|
||||
]
|
||||
@@ -59,6 +59,10 @@ from .kubsat import (
|
||||
KubsatView,
|
||||
KubsatExportView,
|
||||
)
|
||||
from .data_entry import (
|
||||
DataEntryView,
|
||||
SearchObjItemAPIView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
@@ -122,4 +126,7 @@ __all__ = [
|
||||
# Kubsat
|
||||
'KubsatView',
|
||||
'KubsatExportView',
|
||||
# Data Entry
|
||||
'DataEntryView',
|
||||
'SearchObjItemAPIView',
|
||||
]
|
||||
|
||||
101
dbapp/mainapp/views/data_entry.py
Normal file
101
dbapp/mainapp/views/data_entry.py
Normal 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)
|
||||
Reference in New Issue
Block a user