diff --git a/src/pages/Well/Telemetry/TelemetryView/archive_methods.js b/src/pages/Well/Telemetry/TelemetryView/archive_methods.js new file mode 100644 index 0000000..ef3dc7c --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/archive_methods.js @@ -0,0 +1,102 @@ +import { range } from 'd3' + +export const DATA_COUNT = 2048 // Колличество точек на подгрузку графика +export const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков +export const LOADING_TRIGGER = 0.5 // Кол-во экранов до подгрузки + +export const getLoadingInterval = (loaded, endDate, interval) => { + // Если данные загружены и дата не заходит за тригер дозагрузка не требуется + if ( + (loaded && (loaded.start ?? null) !== null && (loaded.end ?? null) !== null) && + +endDate - interval * (LOADING_TRIGGER + 1) > loaded.start && + +endDate + interval * LOADING_TRIGGER < loaded.end + ) + return { loadingStartDate: endDate, newLoaded: loaded, loadingInterval: 0 } + + let loadingStartDate = +endDate - interval * (ADDITIVE_PAGES + 1) + let loadingEndDate = +endDate + interval * ADDITIVE_PAGES + + const newLoaded = { + start: loadingStartDate, + end: loadingEndDate + } + + if (loaded && (loaded.start ?? null) !== null && (loaded.end ?? null) !== null) { + if (loadingStartDate >= loaded.start) loadingStartDate = loaded.end + if (loadingEndDate <= loaded.end) loadingEndDate = loaded.start + newLoaded.start = Math.min(loaded.start, loadingStartDate) + newLoaded.end = Math.max(loaded.end, loadingEndDate) + } + + const loadingInterval = Math.trunc((loadingEndDate - loadingStartDate) / 1000) + + return { + loadingStartDate: new Date(loadingStartDate), + newLoaded: { + start: new Date(newLoaded.start), + end: new Date(newLoaded.end), + }, + loadingInterval, + } +} + +const interpolationSearch = (data, begin, end, accessor) => { + const fy = (i) => new Date(data[i]?.[accessor] ?? 0) + const fx = (y, b, e) => Math.round(b + (y - fy(b)) * (e - b) / (fy(e) - fy(b))) + const findIdx = (val, startIdx, c) => { + let x = startIdx + let endIdx = data.length - 1 + if(val < fy(startIdx)) + return startIdx + if(val > fy(endIdx)) + return endIdx + for(let i = 0; i < c; i++){ + x = fx(val, startIdx, endIdx) + if(fy(x) < val) + startIdx = x + else + endIdx = x + if ((startIdx === endIdx)||(fy(startIdx) === fy(endIdx))) + return x + } + return x + } + let x0 = findIdx(begin, 0, 100) + let x1 = findIdx(end, x0, 100) + return { start: x0, end: x1, count: x1 - x0 } +} + +export const cutData = (data, beginDate, endDate) => { + if (data?.length > 0) { + let { start, end } = interpolationSearch(data, beginDate, endDate, 'date') + if (start > 0) start-- + if (end + 1 < end.length) end++ + return data.slice(start, end) + } + return data +} + +export const makeDateTimeDisabled = (dateLimit, chartInterval) => ({ + disabledDate: (date) => { + if (!date) return false + const dt = new Date(date).setHours(0, 0, 0, 0) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }, + disabledTime: (date) => ({ + disabledHours: () => range(24).filter(h => { + if (!date) return false + const dt = +new Date(date).setHours(h) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }), + disabledMinutes: () => range(60).filter(m => { + if (!date) return false + const dt = +new Date(date).setMinutes(m) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }), + disabledSeconds: () => range(60).filter(s => { + if (!date) return false + const dt = +new Date(date).setSeconds(s) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }) + }), +}) diff --git a/src/pages/Well/Telemetry/TelemetryView/index.jsx b/src/pages/Well/Telemetry/TelemetryView/index.jsx index ff43210..4aa0c8a 100644 --- a/src/pages/Well/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/index.jsx @@ -1,15 +1,16 @@ import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { BehaviorSubject, buffer, throttleTime } from 'rxjs' import { useSearchParams } from 'react-router-dom' -import { Button, Select } from 'antd' +import { Alert, Button, Select } from 'antd' import { useWell } from '@asb/context' -import { DatePickerWrapper, makeDateSorter } from '@components/Table' -import { D3MonitoringCharts } from '@components/d3/monitoring' import LoaderPortal from '@components/LoaderPortal' +import { CopyUrlButton } from '@components/CopyUrl' +import { D3MonitoringCharts } from '@components/d3/monitoring' import { invokeWebApiWrapperAsync } from '@components/factory' +import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' -import { formatDate, hasPermission, isRawDate, range, withPermissions } from '@utils' +import { formatDate, hasPermission, isRawDate, withPermissions } from '@utils' import { Subscribe } from '@services/signalr' import { DrillFlowChartService, @@ -19,6 +20,7 @@ import { } from '@api' import { makeChartGroups, yAxis } from './dataset' +import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods' import ActiveMessagesOnline from './ActiveMessagesOnline' import TelemetrySummary from './TelemetrySummary' import WirelineRunOut from './WirelineRunOut' @@ -60,7 +62,7 @@ export const normalizeData = (data) => data?.map(item => ({ })) ?? [] const dateSorter = makeDateSorter('date') -const defaultDate = () => Date.now() - defaultPeriod * 1000 +const defaultDate = () => new Date(Date.now() - defaultPeriod * 1000) const makeSubjectSubsription = (subject$, handler) => { const subscribtion = subject$.pipe( @@ -70,54 +72,33 @@ const makeSubjectSubsription = (subject$, handler) => { return () => subscribtion.unsubscribe() } +const getRowDate = (row) => (row && isRawDate(row.date)) ? +new Date(row.date) : null + const TelemetryView = memo(() => { const [well, updateWell] = useWell() const [searchParams, setSearchParams] = useSearchParams() - const [currentWellId, setCurrentWellId] = useState(null) const [dataSaub, setDataSaub] = useState([]) const [dataSpin, setDataSpin] = useState([]) const [showLoader, setShowLoader] = useState(false) const [flowChartData, setFlowChartData] = useState([]) const [rop, setRop] = useState(null) - const [domain, setDomain] = useState({}) const [chartMethods, setChartMethods] = useState() - const [chartInterval, setChartInterval] = useState(defaultPeriod) - const [startDate, setStartDate] = useState(defaultDate) - const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) + const [loadedDataRange, setLoadedDataRange] = useState({}) + const [chartInterval, setChartInterval] = useState(defaultPeriod * 1000) + const [endDate, setEndDate] = useState(defaultDate) + const [dateLimit, setDateLimit] = useState({ from: 0, to: Date.now() }) const [archiveMode, setArchiveMode] = useState(false) const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) - const isDateDisabled = useCallback((date) => { - if (!date) return false - const dt = new Date(date).setHours(0, 0, 0, 0) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }, [dateLimit, chartInterval]) - - const isDateTimeDisabled = useCallback((date) => ({ - disabledHours: () => range(24).filter(h => { - if (!date) return false - const dt = +new Date(date).setHours(h) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }), - disabledMinutes: () => range(60).filter(m => { - if (!date) return false - const dt = +new Date(date).setMinutes(m) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }), - disabledSeconds: () => range(60).filter(s => { - if (!date) return false - const dt = +new Date(date).setSeconds(s) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }) - }), [dateLimit, chartInterval]) + const dateTimeDisabled = useMemo(() => makeDateTimeDisabled(dateLimit, chartInterval), [dateLimit, chartInterval]) const handleDataSaub = useCallback((data, replace = false) => { setDataSaub((prev) => { - if (!data) + if (!data || !Array.isArray(data)) return replace ? [] : prev const dataSaub = normalizeData(data) const out = replace ? [...dataSaub] : [...prev, ...dataSaub] @@ -126,90 +107,71 @@ const TelemetryView = memo(() => { }) }, []) - const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), []) + const handleDataSpin = useCallback((data, replace) => { + setDataSpin((prev) => { + if (!data || !Array.isArray(data)) + return replace ? [] : prev + return replace ? data : [...prev, ...data] + }) + }, []) - const onWheel = useCallback((value) => { - if (!archiveMode && value.deltaY < 0) { + const onWheel = useCallback((e) => { + if (!archiveMode && e.deltaY < 0) { setArchiveMode(true) - // load data - } else { - // move + setDataSaub([]) + setDataSpin([]) + } else if (archiveMode) { + setEndDate((prevEndDate) => { + const offset = e.deltaY / 100 * chartInterval * 0.15 // сдвиг в 15% интервала + const nextEndDate = +prevEndDate + offset + const firstPossibleDate = Math.max(loadedDataRange?.start || 0, dateLimit.from) + chartInterval + const lastPossibleDate = Math.min(dateLimit.to, (loadedDataRange?.end ?? Date.now())) + const out = new Date(Math.max(firstPossibleDate, Math.min(nextEndDate, lastPossibleDate))) + if (e.deltaY > 0 && +out >= lastPossibleDate) { // Автопереход к актуальным данным при прокручивании в самый низ + setArchiveMode(false) + setLoadedDataRange(null) + } + return out + }) } - }, [archiveMode]) + }, [archiveMode, loadedDataRange, chartInterval, dateLimit]) - const spinLast = useMemo(() => dataSpin.at(-1), [dataSpin]) - const saubLast = useMemo(() => dataSaub.at(-1), [dataSaub]) + const domain = useMemo(() => ({ min: new Date(+endDate - chartInterval), max: endDate }), [endDate, chartInterval]) + + const spinLast = useMemo(() => getLast(dataSpin), [dataSpin]) + const saubLast = useMemo(() => getLast(dataSaub), [dataSaub]) const summaryData = useMemo(() => ({ ...saubLast, ...rop }), [saubLast, rop]) const saubSubject$ = useMemo(() => new BehaviorSubject(), []) const spinSubject$ = useMemo(() => new BehaviorSubject(), []) - const filteredData = useMemo(() => { - let i, j - for (i = 0; i < dataSaub.length; i++) { - const date = +new Date(dataSaub[i]?.date) - if (date >= +domain.min) break - } - - for (j = dataSaub.length - 1; j >= i; j--) { - const date = +new Date(dataSaub[i]?.date) - if (date <= +domain.max) break - } - - if (i >= j) return [] - return dataSaub.slice(i, j) - }, [dataSaub, domain]) - const chartGroups = useMemo(() => makeChartGroups(flowChartData), [flowChartData]) useEffect(() => { - setArchiveMode(isRawDate(searchParams.get('start'))) - const interval = parseInt(searchParams.get('range') || defaultPeriod) - const date = new Date(searchParams.get('start') || (Date.now() - interval)) + if (!searchParams.has('range') || !searchParams.has('end')) return + setArchiveMode(isRawDate(searchParams.get('end'))) + const interval = parseInt(searchParams.get('range') || defaultPeriod) * 1000 + const date = new Date(searchParams.get('end') || Date.now()) setChartInterval(interval) - setStartDate(date) - }, [searchParams]) + setEndDate(date) + }, []) // Получение параметров должно работать только при открытии страницы useEffect(() => { - if (archiveMode) return - const subscribtion = saubSubject$.pipe( - buffer(saubSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSaub(data.flat().filter(Boolean))) - - return () => subscribtion.unsubscribe() - }, [saubSubject$, archiveMode]) - - useEffect(() => { - if (archiveMode) return - const subscribtion = spinSubject$.pipe( - buffer(spinSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSpin(data.flat().filter(Boolean))) - - return () => subscribtion.unsubscribe() - }, [spinSubject$, archiveMode]) + const params = {} + if (archiveMode) { + if (endDate) + params.end = endDate.toISOString() + if (chartInterval) + params.range = parseInt(chartInterval / 1000) + } + setSearchParams(params) + }, [archiveMode, endDate, chartInterval]) useEffect(() => makeSubjectSubsription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub]) useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) useEffect(() => { - if (currentWellId == well.id) return - setCurrentWellId(well.id) - invokeWebApiWrapperAsync( - async () => { - const flowChart = await DrillFlowChartService.getByIdWell(well.id) - const dataSaub = await TelemetryDataSaubService.getData(well.id, null, chartInterval) - const dataSpin = await TelemetryDataSpinService.getData(well.id, null, chartInterval) - setFlowChartData(flowChart ?? []) - handleDataSaub(dataSaub, true) - handleDataSpin(dataSpin) - }, - null, - `Не удалось получить данные`, - { actionName: 'Получение данных по скважине', well } - ) - }, [well, chartInterval, currentWellId, handleDataSaub]) - - useEffect(() => { + if (archiveMode) return const unsubscribe = Subscribe( 'hubs/telemetry', `well_${well.id}`, { methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) }, @@ -217,11 +179,56 @@ const TelemetryView = memo(() => { ) return () => unsubscribe() - }, [well.id, saubSubject$, spinSubject$]) + }, [archiveMode, well.id, saubSubject$, spinSubject$]) + + useEffect(() => { + if (archiveMode) return + invokeWebApiWrapperAsync( + async () => { + const dataSaub = await TelemetryDataSaubService.getData(well.id, null, chartInterval / 1000) + const dataSpin = await TelemetryDataSpinService.getData(well.id, null, chartInterval / 1000) + handleDataSaub(dataSaub, true) + handleDataSpin(dataSpin, true) + }, + setShowLoader, + `Не удалось получить данные`, + { actionName: 'Получение данных по скважине', well } + ) + }, [archiveMode, chartInterval, well]) + + useEffect(() => { + if (!archiveMode || showLoader) return + const { loadingStartDate, loadingInterval, newLoaded } = getLoadingInterval(loadedDataRange, endDate, chartInterval) + if (loadingInterval <= 0) return + invokeWebApiWrapperAsync( + async () => { + const data = await TelemetryDataSaubService.getData(well.id, loadingStartDate.toISOString(), loadingInterval, DATA_COUNT) + + const loadedStartDate = new Date(Math.max(+newLoaded.start, +endDate - chartInterval * (ADDITIVE_PAGES + 1))) + const loadedEndDate = new Date(Math.min(+newLoaded.end, +endDate + chartInterval * ADDITIVE_PAGES)) + setLoadedDataRange({ start: loadedStartDate, end: loadedEndDate }) + + if (data) { + data.forEach(elm => elm.date = new Date(elm.date)) + setDataSaub((prevDataSaub) => { + const newData = [...prevDataSaub, ...normalizeData(data)] + newData.sort(dateSorter) + return cutData(newData, loadedStartDate, loadedEndDate) + }) + } + + }, + setShowLoader, + `Не удалось загрузить данные c ${formatDate(newLoaded.start)} по ${formatDate(newLoaded.end)}`, + { actionName: 'Загрузка телеметрий в диапозоне', well } + ) + }, [archiveMode, showLoader, well, chartInterval, loadedDataRange, endDate, handleDataSaub]) useEffect(() => { invokeWebApiWrapperAsync( async () => { + const flowChart = await DrillFlowChartService.getByIdWell(well.id) + setFlowChartData(flowChart ?? []) const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) setRop(rop) let dates = await TelemetryDataSaubService.getDataDatesRange(well.id) @@ -232,72 +239,81 @@ const TelemetryView = memo(() => { setDateLimit(dates) }, setShowLoader, - `Не удалось загрузить данные`, + e => `Не удалось загрузить данные: ${String(e)}`, { actionName: 'Получение данных по скважине', well } ) }, [well]) useEffect(() => { - if (!saubLast || archiveMode) return - const last = new Date(saubLast.date) - const startDate = new Date(+last - chartInterval * 1000) - setStartDate(startDate) - setDomain({ min: startDate, max: last }) - }, [archiveMode, saubLast, chartInterval]) + if (archiveMode || !saubLast || !isRawDate(saubLast.date)) return + setEndDate(new Date(saubLast.date)) + }, [archiveMode, saubLast]) - return ( - - - - - - Статус: - - Неизвестно - В работе - Завершено - - - - - - - - MSE - - - - - Начальная дата: - setStartDate(new Date(startDate))} - /> - - - Интервал: - - - chartMethods?.setSettingsVisible(true)}>Настроить графики - setArchiveMode((prev) => !prev)}> - {archiveMode ? 'Выйти из архива' : 'Войти в архив'} - - - - - - - ) + return ( + + + + + + + Статус: + + Неизвестно + В работе + Завершено + + + + + + + + MSE + + + {archiveMode && ( + + + + )} + + + + Последняя дата: + setEndDate(new Date(endDate))} + /> + + + Интервал: + setChartInterval(value * 1000)} /> + + chartMethods?.setSettingsVisible(true)}>Настроить графики + setArchiveMode((prev) => !prev)} danger={archiveMode}> + {archiveMode ? 'Выйти из архива' : 'Войти в архив'} + + {archiveMode && } + + + + + + ) }) export default withPermissions(TelemetryView, [ diff --git a/src/styles/telemetry_view.less b/src/styles/telemetry_view.less index cdbb3c6..f166b95 100644 --- a/src/styles/telemetry_view.less +++ b/src/styles/telemetry_view.less @@ -53,6 +53,26 @@ } } +.archive-alert-wrapper { + display: flex; + flex-direction: column; + gap: 10px; + position: relative; + padding: 10px; +} + +.archive-alert { + display: flex; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + background-color: #0001; +} + @media only screen and (max-width: 1280px) { .telemetry-view-page { --page-gap: 7.5px;