Files
dc-observatio/services/pcp-ui-service/src/main/resources/static/requests_scripts.js
T
2026-05-28 16:47:43 +03:00

1603 lines
55 KiB
JavaScript

function drawReq(show, req){
if (req){
if (!show){
var name = 'req_'+req.requestId
while (viewer.dataSources.remove(viewer.dataSources.getByName(name)[0])) {}
} else{
var u = '/requests/req-with-cells/'+req.requestId
fetch(u, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then((response)=>response.json())
.then((json) =>{
viewer.dataSources.add(Cesium.CzmlDataSource.load(json));
})
}
}
}
function drawOrbit(show, req, startInput, endInput){
if (req){
if (!show){
var name = 'sat_'+req.noradId
while (viewer.dataSources.remove(viewer.dataSources.getByName(name)[0])) {}
checkModels();
} else{
const inputOrb = document.getElementById('toggleOrbit');
let valueOrb = inputOrb.checked;
const inputFl = document.getElementById('toggleFL');
let valueFl = inputFl.checked;
const inputSw = document.getElementById('toggleSwath');
let valueSw = inputSw.checked;
var sceneMode = (viewer.scene.mode == Cesium.SceneMode.SCENE2D);
var u = '/requests/sat/'+req.noradId+
'?view='+sceneMode+'&orbit='+valueOrb+
'&sw='+valueSw+
'&fl='+valueFl
if (startInput && endInput) {
u += '&tn=' + encodeURIComponent(startInput) + '&tk=' + encodeURIComponent(endInput)
}
console.log(u)
fetch(u, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then((response)=>response.json())
.then((json) =>{
console.log(json)
viewer.dataSources.add(Cesium.CzmlDataSource.load(json));
checkModels();
})
}
}
}
function drawSt(show, req){
if (req){
if (!show){
var name = 'station_'+req.id
while (viewer.dataSources.remove(viewer.dataSources.getByName(name)[0])) {}
checkModels();
} else{
var sceneMode = (viewer.scene.mode == Cesium.SceneMode.SCENE2D);
var u = '/requests/station/'+req.id+'?view='+sceneMode+
'&orbitHeightKm='+encodeURIComponent(selectedStationOrbitHeightKm())
fetch(u, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(async (response) => {
const text = await response.text();
let payload;
try {
payload = text ? JSON.parse(text) : [];
} catch (error) {
payload = { message: text || `HTTP error! status: ${response.status}` };
}
if (!response.ok) {
throw new Error(payload.error || payload.message || `HTTP error! status: ${response.status}`);
}
return payload;
})
.then((json) =>{
if (!Array.isArray(json) || !json[0] || json[0].id !== 'document') {
throw new Error('Некорректный CZML для плана спутника');
}
viewer.dataSources.add(Cesium.CzmlDataSource.load(json));
checkModels();
})
.catch(error => {
console.error('Ошибка при загрузке плана спутника:', error);
alert(error.message);
})
}
}
}
function selectedStationOrbitHeightKm() {
const input = document.getElementById('stationOrbitHeightKm');
const value = Number(input?.value || 500);
if (!Number.isFinite(value) || value <= 0) {
if (input) input.value = '500';
return 500;
}
return value;
}
function drawSat(show, req, startInput, endInput, runId){
if (req){
if (!show){
var name = 'plan_'+req.noradId
while (viewer.dataSources.remove(viewer.dataSources.getByName(name)[0])) {}
checkModels();
} else{
// const url = `/requests/req-with-covs/${requestId}?tn=${startInput}&tk=${endInput}&cov=`+cover+sign;
let u = '/requests/plan/'+req.noradId+'?tn='+startInput+'&tk='+endInput
if (runId) {
u += '&runId=' + encodeURIComponent(runId)
}
console.log(u)
fetch(u, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then((response)=>response.json())
.then((json) =>{
viewer.dataSources.add(Cesium.CzmlDataSource.load(json));
checkModels();
})
}
}
}
// выбрать все НИП/ППИ
function selectAllRequests(){
$('input:checkbox[name="request"]:visible:not(:checked)').each(function(){$(this).trigger('click')});
}
// скрыть все НИП/ППИ
function deselectAllRequests(){
$('input:checkbox[name="request"]:checked').each(function(){$(this).trigger('click')});
}
// Функция для отображения тепловой карты
function showHeatmap(importance, countLat, countLong) {
if (!window.viewer) return;
hideHeatmap();
var params = new URLSearchParams({ importance: importance });
if (countLat) {
params.append('countLat', countLat);
}
if (countLong) {
params.append('countLong', countLong);
}
var u = '/requests?' + params.toString()
fetch(u, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then((response)=>response.json())
.then((json) =>{
viewer.dataSources.add(Cesium.CzmlDataSource.load(json));
})
}
// Функция для скрытия тепловой карты
function hideHeatmap() {
if (!window.viewer) return;
// Удаляем слой тепловой карты, если он существует
while (viewer.dataSources.remove(viewer.dataSources.getByName('heatMap')[0])) {}
}
function useSceneMode(sceneMode){
if (sceneMode == Cesium.SceneMode.SCENE2D){
console.log('2D')
} else
console.log('3D')
}
function checkModels(){
var sceneMode = (viewer.scene.mode == Cesium.SceneMode.SCENE2D);
viewer.dataSources._dataSources.forEach(dataSource => {
dataSource._entityCollection._entities._array.forEach( entity => {
if (entity.id.includes('station_')){
entity.model.show=!sceneMode
entity.billboard.show=sceneMode
} else if (entity.id.includes('orbit_')){
entity.model.show=!sceneMode
entity.point.show=sceneMode
}
})
});
}
function handleOrbits(show){
viewer.dataSources._dataSources.forEach(dataSource => {
dataSource._entityCollection._entities._array.forEach( entity => {
if (entity.id.includes('orbit_')){
entity.path.show=show
}
})
});
}
function handleSw(show){
viewer.dataSources._dataSources.forEach(dataSource => {
dataSource._entityCollection._entities._array.forEach( entity => {
if (entity.id.includes('swath_')){
entity.path.show=show
}
})
});
}
function handleFl(show){
viewer.dataSources._dataSources.forEach(dataSource => {
dataSource._entityCollection._entities._array.forEach( entity => {
if (entity.id.includes('flight_line_')){
entity.path.show=show
entity.point.show=show
}
})
});
}
// Глобальные переменные для хранения данных слотов
let currentSlotsData = [];
let currentRequestId = null;
let currentSlotsContext = null;
let slotsSortConfig = {
column: 'tn',
direction: 'asc'
};
function removeDataSourceByName(dataSourceName) {
let dataSource = viewer.dataSources.getByName(dataSourceName)[0];
while (dataSource && viewer.dataSources.remove(dataSource)) {
dataSource = viewer.dataSources.getByName(dataSourceName)[0];
}
}
function getCurrentSlotsDataSource() {
if (!currentSlotsContext || !currentSlotsContext.dataSourceName) {
return null;
}
return viewer.dataSources.getByName(currentSlotsContext.dataSourceName)[0] || null;
}
function getCoverageSchemeDataSourceName(requestId) {
return `req_cov_scheme_${requestId}`;
}
function setSlotsContext(context) {
currentSlotsContext = context;
updateSlotsActionButtons();
}
function updateSlotsActionButtons() {
const isRequestContext = currentSlotsContext && currentSlotsContext.type === 'request';
const bookButton = document.getElementById('bookSlotsBtn');
const cancelButton = document.getElementById('cancelBookingBtn');
if (bookButton) {
bookButton.style.display = isRequestContext ? '' : 'none';
}
if (cancelButton) {
cancelButton.style.display = isRequestContext ? '' : 'none';
}
}
function getSatelliteBookedSlotsDataSourceName(satelliteId) {
return `sat_booked_${satelliteId}`;
}
function getSatelliteSlotsDataSourceName(satelliteId) {
return `sat_slots_${satelliteId}`;
}
function buildSlotEntityId(slot, index) {
if (slot.entityId) {
return slot.entityId;
}
const satelliteId = slot.satelliteId ?? 'unknown';
const cycle = slot.cycle ?? slot.revolution ?? 0;
const slotNumber = slot.slotNumber
?? (Array.isArray(slot.slotIds) && slot.slotIds.length > 0 ? slot.slotIds[0] : index);
return `slot_${satelliteId}_${cycle}_${slotNumber}`;
}
function normalizeRequestSlots(slots) {
return (slots || []).map((slot, index) => ({
...slot,
index: index,
entityId: buildSlotEntityId(slot, index)
}));
}
function normalizeSatelliteBookedSlots(slots) {
return (slots || []).map((slot, index) => ({
...slot,
cycle: slot.revolution ?? 0,
slotNumber: Array.isArray(slot.slotIds) && slot.slotIds.length > 0 ? slot.slotIds[0] : index + 1,
state: 'BOOKED',
index: index,
entityId: buildSlotEntityId({
...slot,
cycle: slot.revolution ?? 0,
slotNumber: Array.isArray(slot.slotIds) && slot.slotIds.length > 0 ? slot.slotIds[0] : index + 1
}, index)
}));
}
function normalizeSatelliteSlots(slots) {
return (slots || []).map((slot, index) => ({
...slot,
index: index,
entityId: buildSlotEntityId(slot, index)
}));
}
function normalizeCoverageSchemeModes(modes) {
return (modes || []).map((mode, index) => ({
...mode,
index: index,
sequence: mode.sequence ?? (index + 1),
cycle: mode.cycle ?? mode.sequence ?? (index + 1),
tn: mode.tn ?? mode.timeStart,
tk: mode.tk ?? mode.timeEnd,
roll: mode.roll ?? mode.rollDegrees,
contour: mode.contour ?? mode.contourWkt,
entityId: mode.entityId ?? `coverage_mode_${mode.satelliteId ?? 'unknown'}_${mode.sequence ?? index + 1}`
}));
}
function parsePolygonWktToDegreesArray(contourWkt) {
if (typeof contourWkt !== 'string') {
return null;
}
const trimmed = contourWkt.trim();
if (!trimmed.toUpperCase().startsWith('POLYGON')) {
return null;
}
const body = trimmed
.replace(/^POLYGON\s*\(\(/i, '')
.replace(/\)\)\s*$/i, '');
const outerRingText = body.split('),(')[0];
const coordinates = outerRingText
.split(',')
.map(pair => pair.trim().split(/\s+/))
.map(parts => ({
longitude: Number(parts[0]),
latitude: Number(parts[1])
}))
.filter(point => Number.isFinite(point.longitude) && Number.isFinite(point.latitude));
if (coordinates.length < 3) {
return null;
}
const degrees = [];
coordinates.forEach(point => {
degrees.push(point.longitude, point.latitude, 0.0);
});
return degrees;
}
function closeDegreesArrayHeights(positions) {
if (!Array.isArray(positions) || positions.length < 9) {
return positions;
}
const first = positions.slice(0, 3);
const last = positions.slice(-3);
const isClosed = first.length === last.length && first.every((value, index) => value === last[index]);
return isClosed ? positions : positions.concat(first);
}
function getSatelliteCesiumColor(satellite) {
const red = Number(satellite && satellite.red);
const green = Number(satellite && satellite.green);
const blue = Number(satellite && satellite.blue);
return Cesium.Color.fromBytes(
Number.isFinite(red) ? red : 255,
Number.isFinite(green) ? green : 140,
Number.isFinite(blue) ? blue : 0,
255
);
}
function createSatelliteBookedSlotsDataSource(dataSourceName, satellite, slots) {
const dataSource = new Cesium.CustomDataSource(dataSourceName);
const color = getSatelliteCesiumColor(satellite);
normalizeSatelliteBookedSlots(slots).forEach(slot => {
const positions = parsePolygonWktToDegreesArray(slot.contour);
if (!positions) {
console.warn('Не удалось разобрать контур забронированного слота', slot);
return;
}
dataSource.entities.add({
id: slot.entityId,
name: `Забронированный слот КА ${slot.satelliteId}`,
description: [
`Спутник: ${slot.satelliteId ?? '-'}`,
`Начало: ${formatDateTime(slot.tn)}`,
`Окончание: ${formatDateTime(slot.tk)}`,
`Длительность: ${formatDuration(calculateDuration(slot.tn, slot.tk))}`,
`Виток: ${slot.revolution ?? '-'}`,
`Слоты: ${(slot.slotIds || []).join(', ') || '-'}`
].join('<br>'),
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(positions),
material: color.withAlpha(0.28),
outline: true,
outlineColor: color,
outlineWidth: 2.0
},
polyline: {
positions: Cesium.Cartesian3.fromDegreesArrayHeights(closeDegreesArrayHeights(positions)),
width: 2.0,
material: color
}
});
});
return dataSource;
}
function createSatelliteSlotsDataSource(dataSourceName, satellite, slots) {
const dataSource = new Cesium.CustomDataSource(dataSourceName);
const color = getSatelliteCesiumColor(satellite);
normalizeSatelliteSlots(slots).forEach(slot => {
const positions = parsePolygonWktToDegreesArray(slot.contour);
if (!positions) {
console.warn('Не удалось разобрать контур слота', slot);
return;
}
dataSource.entities.add({
id: slot.entityId,
name: `Слот КА ${slot.satelliteId}`,
description: [
`Спутник: ${slot.satelliteId ?? '-'}`,
`Цикл: ${slot.cycle ?? '-'}`,
`Начало: ${formatDateTime(slot.tn)}`,
`Окончание: ${formatDateTime(slot.tk)}`,
`Длительность: ${formatDuration(calculateDuration(slot.tn, slot.tk))}`,
`Виток: ${slot.revolution ?? '-'}`,
`Номер слота: ${slot.slotNumber ?? '-'}`
].join('<br>'),
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(positions),
material: color.withAlpha(0.18),
outline: true,
outlineColor: color,
outlineWidth: 2.0
},
polyline: {
positions: Cesium.Cartesian3.fromDegreesArrayHeights(closeDegreesArrayHeights(positions)),
width: 2.0,
material: color
}
});
});
return dataSource;
}
function processAndDisplaySatelliteBookedSlots(slots, satellite) {
currentSlotsData = normalizeSatelliteBookedSlots(slots);
currentRequestId = null;
setSlotsContext({
type: 'satellite-booked',
key: String(satellite.noradId),
dataSourceName: getSatelliteBookedSlotsDataSourceName(satellite.noradId)
});
document.getElementById('slotsTitle').textContent =
`Забронированные слоты спутника "${satellite.name}" (${currentSlotsData.length} интервалов)`;
document.getElementById('slotsContainer').style.display = 'block';
if (currentSlotsData.length === 0) {
showEmptySlotsMessage('Для выбранного временного диапазона забронированные слоты не найдены');
updateSlotsSummary();
return;
}
renderSlotsTable();
updateSlotsSummary();
}
function processAndDisplaySatelliteSlots(slots, satellite) {
currentSlotsData = normalizeSatelliteSlots(slots);
currentRequestId = null;
setSlotsContext({
type: 'satellite-slots',
key: String(satellite.noradId),
dataSourceName: getSatelliteSlotsDataSourceName(satellite.noradId)
});
document.getElementById('slotsTitle').textContent =
`Слоты спутника "${satellite.name}" (${currentSlotsData.length} интервалов)`;
document.getElementById('slotsContainer').style.display = 'block';
if (currentSlotsData.length === 0) {
showEmptySlotsMessage('Для выбранного временного диапазона слоты не найдены');
updateSlotsSummary();
return;
}
updateSlotsTableHeaders();
renderSlotsTable();
updateSlotsSummary();
}
function drawSatelliteBookedSlots(show, satellite, startInput, endInput) {
if (!satellite) {
return;
}
const dataSourceName = getSatelliteBookedSlotsDataSourceName(satellite.noradId);
if (!show) {
removeDataSourceByName(dataSourceName);
if (currentSlotsContext
&& currentSlotsContext.type === 'satellite-booked'
&& currentSlotsContext.key === String(satellite.noradId)) {
clearSlotsTable();
}
return;
}
const params = new URLSearchParams({
timeStart: startInput,
timeStop: endInput
});
fetch(`/api/satellites/${satellite.noradId}/booked-slots?${params.toString()}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(slots => {
removeDataSourceByName(dataSourceName);
viewer.dataSources.add(createSatelliteBookedSlotsDataSource(dataSourceName, satellite, slots));
processAndDisplaySatelliteBookedSlots(slots, satellite);
})
.catch(error => {
console.error('Ошибка при загрузке забронированных слотов спутника:', error);
alert(error.message);
});
}
function drawSatelliteSlots(show, satellite, startInput, endInput) {
if (!satellite) {
return;
}
const dataSourceName = getSatelliteSlotsDataSourceName(satellite.noradId);
if (!show) {
removeDataSourceByName(dataSourceName);
if (currentSlotsContext
&& currentSlotsContext.type === 'satellite-slots'
&& currentSlotsContext.key === String(satellite.noradId)) {
clearSlotsTable();
}
return;
}
const params = new URLSearchParams({
timeStart: startInput,
timeStop: endInput
});
fetch(`/api/satellites/${satellite.noradId}/slots?${params.toString()}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(slots => {
removeDataSourceByName(dataSourceName);
viewer.dataSources.add(createSatelliteSlotsDataSource(dataSourceName, satellite, slots));
processAndDisplaySatelliteSlots(slots, satellite);
})
.catch(error => {
console.error('Ошибка при загрузке слотов спутника:', error);
alert(error.message);
});
}
function drawReqCov(show, req, startInput, endInput, node, cover, satellites, sun) {
if (req) {
const requestId = req.requestId;
const dataSourceName = 'req_cov_' + requestId;
if (!show) {
// Удаляем данные покрытия с карты
removeDataSourceByName(dataSourceName);
// Если это текущая отображаемая заявка, очищаем таблицу слотов
if (currentRequestId === requestId) {
clearSlotsTable();
}
} else {
const params = new URLSearchParams({
tn: startInput,
tk: endInput,
cov: cover,
sun: sun
});
if (node == 1) {
params.append('sign', 'ASC');
} else if (node == 2) {
params.append('sign', 'DESC');
}
if (Array.isArray(satellites)) {
satellites.forEach(satelliteId => params.append('satellites', satelliteId));
}
params.append('sun', sun)
const url = `/requests/req-with-covs/${requestId}?${params.toString()}`;
console.log('Запрос покрытия:', url);
fetch(url, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(json => {
// Отображаем покрытие на карте
if (json.cov && json.cov.length > 0) {
viewer.dataSources.add(Cesium.CzmlDataSource.load(json.cov));
}
// Обрабатываем и отображаем слоты в таблице
if (json.slots && json.slots.length > 0) {
processAndDisplaySlots(json.slots, req);
} else {
showEmptySlotsMessage('Нет интервалов покрытия для выбранного временного диапазона');
}
})
.catch(error => {
console.error('Ошибка при загрузке данных покрытия:', error);
alert(error.message);
});
}
}
}
function drawReqCoverageScheme(show, req, startInput, endInput, satellites, options) {
if (!req) {
return;
}
const requestId = req.requestId;
const dataSourceName = getCoverageSchemeDataSourceName(requestId);
if (!show) {
removeDataSourceByName(dataSourceName);
if (currentSlotsContext
&& currentSlotsContext.type === 'coverage-scheme'
&& currentSlotsContext.key === requestId) {
clearSlotsTable();
}
return;
}
if (!Array.isArray(satellites) || satellites.length === 0) {
alert('Для расчета схемы покрытия выберите хотя бы один спутник');
return;
}
const params = new URLSearchParams({
tn: startInput,
tk: endInput
});
satellites.forEach(satelliteId => params.append('satellites', satelliteId));
if (options) {
if (options.rollStepDegrees !== null && options.rollStepDegrees !== undefined) {
params.append('rollStepDegrees', options.rollStepDegrees);
}
if (options.minimumTechnologyOverlap !== null && options.minimumTechnologyOverlap !== undefined) {
params.append('minimumTechnologyOverlap', options.minimumTechnologyOverlap);
}
if (options.strictContinuousCoverage !== null && options.strictContinuousCoverage !== undefined) {
params.append('strictContinuousCoverage', options.strictContinuousCoverage);
}
if (options.allowPartialCoverage !== null && options.allowPartialCoverage !== undefined) {
params.append('allowPartialCoverage', options.allowPartialCoverage);
}
if (options.orientationToleranceDegrees !== null && options.orientationToleranceDegrees !== undefined) {
params.append('orientationToleranceDegrees', options.orientationToleranceDegrees);
}
if (options.includeDebugInfo !== null && options.includeDebugInfo !== undefined) {
params.append('includeDebugInfo', options.includeDebugInfo);
}
}
fetch(`/requests/req-with-coverage-scheme/${requestId}?${params.toString()}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(json => {
removeDataSourceByName(dataSourceName);
if (json.cov && json.cov.length > 0) {
viewer.dataSources.add(Cesium.CzmlDataSource.load(json.cov));
}
if (json.slots && json.slots.length > 0) {
processAndDisplayCoverageSchemeModes(json.slots, req);
} else {
document.getElementById('slotsTitle').textContent =
`Схема покрытия заявки "${req.name}"`;
setSlotsContext({
type: 'coverage-scheme',
key: requestId,
dataSourceName: dataSourceName
});
updateSlotsTableHeaders();
showEmptySlotsMessage('Для выбранного временного диапазона режимы схемы покрытия не найдены');
}
})
.catch(error => {
console.error('Ошибка при загрузке схемы покрытия:', error);
alert(error.message);
});
}
function getCoverageSchemeRequestOptions() {
const parseOptionalNumber = (elementId) => {
const input = document.getElementById(elementId);
if (!input) {
return null;
}
const rawValue = String(input.value ?? '').trim();
if (rawValue === '') {
return null;
}
const parsedValue = Number.parseFloat(rawValue);
return Number.isFinite(parsedValue) ? parsedValue : null;
};
return {
rollStepDegrees: parseOptionalNumber('coverageSchemeRollStep'),
minimumTechnologyOverlap: parseOptionalNumber('coverageSchemeMinOverlap'),
orientationToleranceDegrees: parseOptionalNumber('coverageSchemeOrientationTolerance'),
strictContinuousCoverage: document.getElementById('coverageSchemeStrictContinuous')?.checked ?? true,
allowPartialCoverage: document.getElementById('coverageSchemeAllowPartial')?.checked ?? false,
includeDebugInfo: document.getElementById('coverageSchemeIncludeDebug')?.checked ?? true
};
}
// Обработка и отображение слотов
function processAndDisplaySlots(slots, request) {
currentSlotsData = normalizeRequestSlots(slots);
currentRequestId = request.requestId;
setSlotsContext({
type: 'request',
key: request.requestId,
dataSourceName: 'req_cov_' + request.requestId
});
// Обновляем заголовок
document.getElementById('slotsTitle').textContent =
`Интервалы покрытия заявки "${request.name}" (${currentSlotsData.length} интервалов)`;
updateSlotsTableHeaders();
// Показываем контейнер слотов
document.getElementById('slotsContainer').style.display = 'block';
// Рендерим таблицу
renderSlotsTable();
// Обновляем сводную информацию
updateSlotsSummary();
}
function processAndDisplayCoverageSchemeModes(modes, request) {
currentSlotsData = normalizeCoverageSchemeModes(modes);
currentRequestId = null;
setSlotsContext({
type: 'coverage-scheme',
key: request.requestId,
dataSourceName: getCoverageSchemeDataSourceName(request.requestId)
});
document.getElementById('slotsTitle').textContent =
`Схема покрытия заявки "${request.name}" (${currentSlotsData.length} режимов)`;
document.getElementById('slotsContainer').style.display = 'block';
updateSlotsTableHeaders();
renderSlotsTable();
updateSlotsSummary();
}
function updateSlotsTableHeaders() {
const primaryHeader = document.getElementById('slotsPrimaryHeader');
const stateHeader = document.getElementById('slotsStateHeader');
const isCoverageScheme = currentSlotsContext && currentSlotsContext.type === 'coverage-scheme';
if (primaryHeader) {
primaryHeader.childNodes[0].textContent = isCoverageScheme ? '№ режима' : '№ слота';
}
if (stateHeader) {
stateHeader.childNodes[0].textContent = isCoverageScheme ? '' : 'Статус';
}
}
// Рендеринг таблицы слотов
function renderSlotsTable() {
const tbody = document.getElementById('slotsTableBody');
if (!tbody) return;
// Очищаем таблицу
tbody.innerHTML = '';
if (currentSlotsData.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted py-4">
Нет данных для отображения
</td>
</tr>
`;
return;
}
// Сортируем данные
const sortedSlots = [...currentSlotsData].sort((a, b) => {
const column = slotsSortConfig.column;
const direction = slotsSortConfig.direction === 'asc' ? 1 : -1;
let aValue, bValue;
switch(column) {
case 'cycle':
aValue = a.cycle || a.sequence || 0;
bValue = b.cycle || b.sequence || 0;
return (aValue - bValue) * direction;
case 'satellite':
aValue = a.satelliteId || 0;
bValue = b.satelliteId || 0;
return (aValue - bValue) * direction;
case 'tn':
aValue = new Date(a.tn).getTime();
bValue = new Date(b.tn).getTime();
return (aValue - bValue) * direction;
case 'tk':
aValue = new Date(a.tk).getTime();
bValue = new Date(b.tk).getTime();
return (aValue - bValue) * direction;
case 'duration':
aValue = calculateDuration(a.tn, a.tk);
bValue = calculateDuration(b.tn, b.tk);
return (aValue - bValue) * direction;
case 'roll':
aValue = a.roll || 0;
bValue = b.roll || 0;
return (aValue - bValue) * direction;
case 'revolution':
aValue = a.revolution || 0;
bValue = b.revolution || 0;
return (aValue - bValue) * direction;
case 'sign':
aValue = a.revolutionSign || '';
bValue = b.revolutionSign || '';
return aValue.localeCompare(bValue) * direction;
case 'slotNumber':
aValue = a.slotNumber || 0;
bValue = b.slotNumber || 0;
return (aValue - bValue) * direction;
case 'state':
aValue = getStateSortOrder(a.state);
bValue = getStateSortOrder(b.state);
return (aValue - bValue) * direction;
default:
return 0;
}
});
// Рендерим строки
sortedSlots.forEach((slot, index) => {
const row = document.createElement('tr');
row.innerHTML = createSlotRowHTML(slot, slot.index);
tbody.appendChild(row);
});
// Обновляем иконки сортировки
updateSlotsSortIcons();
}
// Создание HTML для строки слота
function createSlotRowHTML(slot, index) {
const tn = formatDateTime(slot.tn);
const duration = calculateDuration(slot.tn, slot.tk);
const durationFormatted = formatDuration(duration);
const isCoverageScheme = currentSlotsContext && currentSlotsContext.type === 'coverage-scheme';
const primaryValue = isCoverageScheme
? (slot.sequence || '-')
: (slot.slotNumber || '-');
const stateContent = slot.state === undefined || slot.state === null || slot.state === ''
? ''
: (() => {
const stateFormatted = formatState(slot.state);
const stateClass = getStateClass(slot.state);
return `
<span class="badge ${stateClass}" title="${stateFormatted.text}">
${stateFormatted.text}
</span>
`;
})();
return `
<td>${primaryValue}</td>
<td class="text-nowrap">SAT-${slot.satelliteId || '-'}</td>
<td class="text-nowrap">${tn}</td>
<td class="text-nowrap text-center">${stateContent}</td>
<td class="text-nowrap">${durationFormatted}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary"
onclick="flyToSlot(${index})"
title="Найти на карте">
<i class="bi bi-geo-alt"></i>
</button>
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary "
data-action="toggle-slot-visibility"
data-slot-index="${index}"
onclick="viewSlot(${index}, this)"
title="Скрыть на карте">
<i class="bi bi-eye"></i>
</button>
</td>
`;
}
// Форматирование даты и времени
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '-';
try {
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('Ошибка форматирования даты:', e);
return dateTimeString;
}
}
// Расчет длительности в минутах
function calculateDuration(tn, tk) {
if (!tn || !tk) return 0;
const startDate = new Date(tn);
const endDate = new Date(tk);
return (endDate - startDate) / (1000 * 60); // в минутах
}
// Форматирование длительности
function formatDuration(minutes) {
if (minutes < 1) {
const seconds = Math.round(minutes * 60);
return `${seconds} сек`;
}
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
if (hours > 0) {
return `${hours}ч ${mins}м`;
}
return `${mins} мин`;
}
// Форматирование крена
function formatRoll(roll) {
if (roll === undefined || roll === null) return '-';
return roll.toFixed(2) + '°';
}
// Форматирование направления витка
function formatRevolutionSign(sign) {
const signs = {
'ASC': { text: '↑', class: 'bg-info', title: 'Восходящий виток' },
'DESC': { text: '↓', class: 'bg-warning', title: 'Нисходящий виток' },
'NONE': { text: '-', class: 'bg-secondary', title: 'Не определено' }
};
return signs[sign] || signs['NONE'];
}
// Форматирование статуса
function formatState(state) {
const stateKey = typeof state === 'string' ? state.trim().toUpperCase() : state;
const states = {
BOOKED: { text: 'Бронь', class: 'bg-success text-white' },
AVAILABLE: { text: 'Свободен', class: 'bg-white text-dark border border-secondary' },
BUSY: { text: 'Блок', class: 'bg-secondary text-white' },
1: { text: 'Бронь', class: 'bg-success text-white' },
0: { text: 'Свободен', class: 'bg-white text-dark border border-secondary' },
2: { text: 'Блок', class: 'bg-secondary text-white' },
3: { text: 'Блок', class: 'bg-secondary text-white' },
4: { text: 'Блок', class: 'bg-secondary text-white' }
};
return states[stateKey] || { text: String(state ?? '-'), class: 'bg-secondary text-white' };
}
function getStateClass(state) {
const stateInfo = formatState(state);
return stateInfo.class;
}
function getStateSortOrder(state) {
const stateKey = typeof state === 'string' ? state.trim().toUpperCase() : state;
switch (stateKey) {
case 'AVAILABLE':
case 0:
return 0;
case 'BOOKED':
case 1:
return 1;
case 'BUSY':
case 2:
case 3:
case 4:
return 2;
default:
return 99;
}
}
// Обновление сводной информации
function updateSlotsSummary() {
if (currentSlotsData.length === 0) {
document.getElementById('totalSlotsCount').textContent = '0';
document.getElementById('totalDuration').textContent = '0';
document.getElementById('satellitesCount').textContent = '0';
document.getElementById('coveragePeriod').textContent = '-';
return;
}
// Общее количество
const totalSlots = currentSlotsData.length;
// Общая длительность
const totalDuration = currentSlotsData.reduce((sum, slot) => {
return sum + calculateDuration(slot.tn, slot.tk);
}, 0);
// Количество уникальных спутников
const uniqueSatellites = new Set(
currentSlotsData.map(slot => slot.satelliteId)
).size;
// Период покрытия
const startTimes = currentSlotsData.map(slot => new Date(slot.tn));
const endTimes = currentSlotsData.map(slot => new Date(slot.tk));
const earliest = new Date(Math.min(...startTimes));
const latest = new Date(Math.max(...endTimes));
const period = `${earliest.toLocaleDateString('ru-RU')} - ${latest.toLocaleDateString('ru-RU')}`;
// Обновляем элементы
document.getElementById('totalSlotsCount').textContent = totalSlots;
document.getElementById('totalDuration').textContent = totalDuration.toFixed(1);
document.getElementById('satellitesCount').textContent = uniqueSatellites;
document.getElementById('coveragePeriod').textContent = period;
}
// Сортировка таблицы слотов
function sortSlotsTable(column) {
// Переключаем направление сортировки
if (slotsSortConfig.column === column) {
slotsSortConfig.direction = slotsSortConfig.direction === 'asc' ? 'desc' : 'asc';
} else {
slotsSortConfig.column = column;
slotsSortConfig.direction = 'asc';
}
// Перерисовываем таблицу
renderSlotsTable();
}
// Обновление иконок сортировки слотов
function updateSlotsSortIcons() {
// Сбрасываем все иконки
document.querySelectorAll('.sortable-slot .sort-icon i').forEach(icon => {
icon.className = 'bi bi-arrow-down-up';
});
// Устанавливаем активную иконку
const activeIcon = document.getElementById(`sort-icon-${slotsSortConfig.column}`);
if (activeIcon) {
const icon = activeIcon.querySelector('i');
if (icon) {
icon.className = slotsSortConfig.direction === 'asc'
? 'bi bi-sort-down'
: 'bi bi-sort-up';
}
}
}
// Очистка таблицы слотов
function clearSlotsTable() {
currentSlotsData = [];
currentRequestId = null;
setSlotsContext(null);
updateSlotsTableHeaders();
const tbody = document.getElementById('slotsTableBody');
if (tbody) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted py-4">
Выберите заявку и включите покрытие для отображения интервалов
</td>
</tr>
`;
}
// Обновляем сводку
document.getElementById('totalSlotsCount').textContent = '0';
document.getElementById('totalDuration').textContent = '0';
document.getElementById('satellitesCount').textContent = '0';
document.getElementById('coveragePeriod').textContent = '-';
const toggleAllCheckbox = document.getElementById('toggleAllSlotsVisibility');
if (toggleAllCheckbox) {
toggleAllCheckbox.checked = true;
}
// Скрываем контейнер
document.getElementById('slotsContainer').style.display = 'none';
}
// Показ сообщения об отсутствии данных
function showEmptySlotsMessage(message) {
const tbody = document.getElementById('slotsTableBody');
if (tbody) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted py-4">
${message || 'Нет данных для отображения'}
</td>
</tr>
`;
}
document.getElementById('slotsContainer').style.display = 'block';
updateSlotsActionButtons();
}
// Экспорт в CSV
function exportSlotsToCSV() {
if (currentSlotsData.length === 0) {
alert('Нет данных для экспорта');
return;
}
// Заголовки CSV
const headers = [
'Цикл', 'Спутник', 'Начало', 'Окончание', 'Длительность (мин)',
'Крен (°)', 'Виток', 'Направление', '№ слота', 'Статус'
];
// Данные
const rows = currentSlotsData.map(slot => [
slot.cycle || slot.sequence || '',
slot.satelliteId || '',
formatDateTime(slot.tn),
formatDateTime(slot.tk),
calculateDuration(slot.tn, slot.tk).toFixed(2),
slot.roll === undefined || slot.roll === null ? '' : Number(slot.roll).toFixed(2),
slot.revolution || '',
slot.revolutionSign || '',
slot.slotNumber || '',
slot.state || ''
]);
// Формируем CSV
let csvContent = 'data:text/csv;charset=utf-8,\uFEFF';
csvContent += headers.join(';') + '\n';
csvContent += rows.map(row => row.join(';')).join('\n');
// Создаем ссылку для скачивания
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
const contextKey = currentSlotsContext ? currentSlotsContext.key : 'export';
link.setAttribute('download', `slots_${contextKey}_${new Date().toISOString().slice(0,10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function collectVisibleSlotsForBooking() {
const visibleButtons = document.querySelectorAll(
'#slotsTableBody button[title="Скрыть на карте"]'
);
const uniqueSlots = new Map();
visibleButtons.forEach(button => {
const dataSlotIndex = button.getAttribute('data-slot-index');
let slotIndex = null;
if (dataSlotIndex !== null && dataSlotIndex !== '') {
const parsed = Number(dataSlotIndex);
if (Number.isFinite(parsed)) {
slotIndex = parsed;
}
}
if (!Number.isFinite(slotIndex)) {
const onClickValue = button.getAttribute('onclick') || '';
const matches = onClickValue.match(/viewSlot\((\d+)/);
if (matches) {
const parsed = Number(matches[1]);
if (Number.isFinite(parsed)) {
slotIndex = parsed;
}
}
}
if (!Number.isFinite(slotIndex) || !currentSlotsData[slotIndex]) {
return;
}
const slot = currentSlotsData[slotIndex];
const satelliteId = Number(slot.satelliteId);
const cycle = Number(slot.cycle);
const rawSlotNum = slot.slotNumber ?? slot.slotNum;
const slotNum = Number(rawSlotNum);
if (!Number.isFinite(satelliteId) || !Number.isFinite(slotNum) || !Number.isFinite(cycle)) {
return;
}
const slotKey = `${satelliteId}:${slotNum}:${cycle}`;
if (!uniqueSlots.has(slotKey)) {
uniqueSlots.set(slotKey, {
satelliteId: satelliteId,
slotNum: slotNum,
cycle: cycle
});
}
});
return Array.from(uniqueSlots.values());
}
function bookVisibleSlots(button) {
if (!currentRequestId) {
alert('Нет выбранной заявки для бронирования');
return;
}
const slots = collectVisibleSlotsForBooking();
if (slots.length === 0) {
alert('Нет слотов с активной кнопкой "Скрыть на карте"');
return;
}
const requestBody = {
requestId: String(currentRequestId),
slots: slots
};
if (button) {
button.disabled = true;
}
fetch('/api/slots/booking/request', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(json => {
const bookedCount = Array.isArray(json) ? json.length : 0;
alert(`Забронировано слотов: ${bookedCount}`);
})
.catch(error => {
console.error('Ошибка при бронировании слотов:', error);
alert('Ошибка при бронировании слотов: ' + error.message);
})
.finally(() => {
if (button) {
button.disabled = false;
}
});
}
function cancelRequestBooking(button) {
if (!currentRequestId) {
alert('Нет выбранной заявки для отмены бронирования');
return;
}
if (button) {
button.disabled = true;
}
fetch(`/api/slots/booking/request?requestId=${encodeURIComponent(String(currentRequestId))}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json'
}
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(result => {
const cancelledCount = Number(result) || 0;
alert(`Отменено бронирований: ${cancelledCount}`);
})
.catch(error => {
console.error('Ошибка при отмене бронирования:', error);
alert('Ошибка при отмене бронирования: ' + error.message);
})
.finally(() => {
if (button) {
button.disabled = false;
}
});
}
let prevPoly = null
let materialColor = Cesium.Color.YELLOW
let outlineColor = Cesium.Color.RED
// Полет к слоту на карте
function flyToSlot(slotIndex) {
if (!currentSlotsData[slotIndex]) return;
const slot = currentSlotsData[slotIndex];
const dataSource = getCurrentSlotsDataSource();
if (!dataSource) {
console.warn('DataSource не найден для слота');
return;
}
const entityName = slot.entityId || buildSlotEntityId(slot, slotIndex);
const entity = dataSource.entities.getById(entityName);
if (entity) {
if (prevPoly != null){
prevPoly.polygon.material = materialColor;
prevPoly.polygon.outlineColor = outlineColor;
}
prevPoly = entity
materialColor = entity.polygon.material
outlineColor = entity.polygon.outlineColor
entity.polygon.material = Cesium.Color.YELLOW.withAlpha(0.4);
entity.polygon.outline = true;
entity.polygon.outlineColor = Cesium.Color.BLUE;
entity.polygon.outlineWidth = 3.0;
viewer.selectedEntity = entity;
viewer.flyTo(entity, {
duration: 2
});
} else {
console.warn('Entity не найден для слота:', entityName);
}
}
function viewSlot(slotIndex, button) {
if (!currentSlotsData[slotIndex]) return;
const slot = currentSlotsData[slotIndex];
const dataSource = getCurrentSlotsDataSource();
if (!dataSource) {
console.warn('DataSource не найден для слота');
return;
}
const entityName = slot.entityId || buildSlotEntityId(slot, slotIndex);
const entity = dataSource.entities.getById(entityName);
if (entity) {
//console.log(entityName)
let visibility = entity.polygon.show
//console.log(visibility)
if (visibility == false){
entity.polygon.show = true
updateVisibilityButton(button, true);
}
else {
entity.polygon.show = false
updateVisibilityButton(button, false);
}
} else {
console.warn('Entity не найден для слота:', entityName);
}
}
// Обновление состояния кнопки
function updateVisibilityButton(button, isVisible) {
const icon = button.querySelector('i');
if (!icon) return;
if (isVisible) {
// Полигон видим - показываем иконку "скрыть"
icon.className = 'bi bi-eye';
button.title = 'Скрыть на карте';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-outline-primary');
} else {
// Полигон скрыт - показываем иконку "показать"
icon.className = 'bi bi-eye-slash';
button.title = 'Показать на карте';
button.classList.remove('btn-outline-primary');
button.classList.add('btn-outline-secondary');
}
}