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('
'), 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('
'), 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 = ` Нет данных для отображения `; 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 ` ${stateFormatted.text} `; })(); return ` ${primaryValue} SAT-${slot.satelliteId || '-'} ${tn} ${stateContent} ${durationFormatted} `; } // Форматирование даты и времени 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 = ` Выберите заявку и включите покрытие для отображения интервалов `; } // Обновляем сводку 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 = ` ${message || 'Нет данных для отображения'} `; } 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'); } }