Добавил форму для отправки данных
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="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 %}
|
||||||
|
|||||||
@@ -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.lineWidth = 1;
|
ctx.strokeStyle = '#adb5bd';
|
||||||
ctx.beginPath();
|
ctx.lineWidth = 1;
|
||||||
ctx.moveTo(leftMargin, uplinkY);
|
ctx.beginPath();
|
||||||
ctx.lineTo(width - rightMargin, uplinkY);
|
ctx.moveTo(leftMargin, uplinkY + rowHeightUL);
|
||||||
ctx.stroke();
|
ctx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
|
||||||
|
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
|
// Draw uplink transponders (now first, on top)
|
||||||
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
|
|
||||||
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 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 delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
const newZoom = Math.max(1, Math.min(20, zoomLevel * delta));
|
|
||||||
|
|
||||||
if (newZoom !== zoomLevel) {
|
if (isUplinkArea) {
|
||||||
zoomLevel = newZoom;
|
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;
|
|
||||||
|
|
||||||
// Limit pan
|
if (dragArea === 'uplink') {
|
||||||
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel);
|
const freqPerPixel = (freqRangeUL / zoomLevelUL) / (rect.width - 80);
|
||||||
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset));
|
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();
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
@@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
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