1603 lines
55 KiB
JavaScript
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');
|
|
}
|
|
}
|