diff --git a/src/pages/Well/Telemetry/Archive/index.jsx b/src/pages/Well/Telemetry/Archive/index.jsx index b15afdf..ce3d94a 100644 --- a/src/pages/Well/Telemetry/Archive/index.jsx +++ b/src/pages/Well/Telemetry/Archive/index.jsx @@ -14,8 +14,9 @@ import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { formatDate, range, withPermissions } from '@utils' import { TelemetryDataSaubService } from '@api' -import { makeChartGroups, normalizeData, yAxis } from '../TelemetryView' +import { normalizeData } from '../TelemetryView' import cursorRender from '../TelemetryView/cursorRender' +import { makeChartGroups, yAxis } from '../TelemetryView/dataset' const DATA_COUNT = 2048 // Колличество точек на подгрузку графика const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков @@ -108,10 +109,10 @@ const Archive = memo(() => { const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) const [showLoader, setShowLoader] = useState(false) const [loaded, setLoaded] = useState(null) - + const [well] = useWell() const [search, setSearchParams] = useSearchParams() - + const getInitialRange = useCallback(() => parseInt(search.get('range') ?? defaultPeriod) * 1000, [search]) const [scrollPercent, setScrollPercent] = useState(0.15) @@ -181,7 +182,7 @@ const Archive = memo(() => { }, [well]) useEffect(() => { - setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval)))) + setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval)))) }, [chartInterval, dateLimit]) useEffect(() => { @@ -191,11 +192,11 @@ const Archive = memo(() => { invokeWebApiWrapperAsync( async () => { const data = await TelemetryDataSaubService.getData(well.id, loadingStartDate.toISOString(), loadingInterval, DATA_COUNT) - + const loadedStartDate = new Date(Math.max(+newLoaded.start, +startDate - chartInterval * ADDITIVE_PAGES)) const loadedEndDate = new Date(Math.min(+newLoaded.end, +startDate + chartInterval * (ADDITIVE_PAGES + 1))) setLoaded({ start: loadedStartDate, end: loadedEndDate }) - + if (data) { data.forEach(elm => elm.date = new Date(elm.date)) setDataSaub((prevDataSaub) => { @@ -204,7 +205,7 @@ const Archive = memo(() => { return cutData(newData, loadedStartDate, loadedEndDate) }) } - + }, setShowLoader, `Не удалось загрузить данные c ${formatDate(startDate)} по ${formatDate(+startDate + chartInterval)}`, diff --git a/src/pages/Well/Telemetry/TelemetryView/dataset.js b/src/pages/Well/Telemetry/TelemetryView/dataset.js new file mode 100644 index 0000000..3f96262 --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/dataset.js @@ -0,0 +1,77 @@ +import { formatDate } from "@utils" + +export const yAxis = { + type: 'time', + accessor: (d) => new Date(d.date), + format: (d) => formatDate(d, undefined, 'DD.MM.YYYY HH:mm:ss'), +} + +const dash = [7, 3] + +const makeDataset = (label, shortLabel, color, key, unit, other) => ({ + key, + label, + shortLabel, + color, + xAxis: { + type: 'linear', + accessor: key, + unit, + }, + type: 'line', + ...other, +}) + +export const makeChartGroups = (flowChart) => { + const makeAreaOptions = (accessor) => ({ + type: 'rect_area', + data: flowChart, + hideLabel: true, + yAxis: { + type: 'linear', + accessor: 'depth', + }, + minXAccessor: 'depthStart', + maxXAccessor: 'depthEnd', + minYAccessor: accessor + 'Min', + maxYAccessor: accessor + 'Max', + linkedTo: accessor, + }) + + return [ + [ + makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'), + makeDataset('Глубина скважины', 'Глубина скв','#7789A1', 'wellDepth', 'м', { dash }), + makeDataset('Расход', 'Расход','#007070', 'flow', 'л/с'), + makeDataset('Положение долота', 'Долото','#B39D59', 'bitPosition', 'м'), + makeDataset('Расход', 'Расход','#007070', 'flowMM', 'л/с', makeAreaOptions('flow')), + ], [ + makeDataset('Скорость блока', 'Скорость ТБ','#59B359', 'blockSpeed', 'м/ч'), + makeDataset('Скорость заданная', 'Скор зад-я','#95B359', 'blockSpeedSp', 'м/ч', { dash }), + ], [ + makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'), + makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'), + makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }), + makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }), + makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')), + ], [ + makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'), + makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }), + makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }), + makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')), + ], [ + makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'), + makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }), + makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }), + makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }), + makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'), + makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')), + ], [ + makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'), + makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }), + makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }), + makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }), + makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')), + ] + ] +} diff --git a/src/pages/Well/Telemetry/TelemetryView/index.jsx b/src/pages/Well/Telemetry/TelemetryView/index.jsx index 7a2ca50..1c5c4f9 100644 --- a/src/pages/Well/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/index.jsx @@ -12,12 +12,13 @@ import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { formatDate, hasPermission, withPermissions } from '@utils' import { Subscribe } from '@services/signalr' import { - DrillFlowChartService, - OperationStatService, - TelemetryDataSaubService, - TelemetryDataSpinService + DrillFlowChartService, + OperationStatService, + TelemetryDataSaubService, + TelemetryDataSpinService } from '@api' +import { makeChartGroups, yAxis } from './dataset' import ActiveMessagesOnline from './ActiveMessagesOnline' import WirelineRunOut from './WirelineRunOut' import { CustomColumn } from './CustomColumn' @@ -36,286 +37,196 @@ import '@styles/message.less' const { Option } = Select -export const yAxis = { - type: 'time', - accessor: (d) => new Date(d.date), - format: (d) => formatDate(d, undefined, 'DD.MM.YYYY HH:mm:ss'), -} - -const dash = [7, 3] - -const makeDataset = (label, shortLabel, color, key, unit, other) => ({ - key, - label, - shortLabel, - color, - xAxis: { - type: 'linear', - accessor: key, - unit, - }, - type: 'line', - ...other, -}) - -export const makeChartGroups = (flowChart) => { - const makeAreaOptions = (accessor) => ({ - type: 'rect_area', - data: flowChart, - hideLabel: true, - yAxis: { - type: 'linear', - accessor: 'depth', - }, - minXAccessor: 'depthStart', - maxXAccessor: 'depthEnd', - minYAccessor: accessor + 'Min', - maxYAccessor: accessor + 'Max', - linkedTo: accessor, - }) - - return [ - [ - makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'), - makeDataset('Глубина скважины', 'Глубина скв','#7789A1', 'wellDepth', 'м', { dash }), - makeDataset('Расход', 'Расход','#007070', 'flow', 'л/с'), - makeDataset('Положение долота', 'Долото','#B39D59', 'bitPosition', 'м'), - makeDataset('Расход', 'Расход','#007070', 'flowMM', 'л/с', makeAreaOptions('flow')), - ], [ - makeDataset('Скорость блока', 'Скорость ТБ','#59B359', 'blockSpeed', 'м/ч'), - makeDataset('Скорость заданная', 'Скор зад-я','#95B359', 'blockSpeedSp', 'м/ч', { dash }), - ], [ - makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'), - makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'), - makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }), - makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }), - makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')), - ], [ - makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'), - makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }), - makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }), - makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')), - ], [ - makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'), - makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }), - makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }), - makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }), - makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'), - makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')), - ], [ - makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'), - makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }), - makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }), - makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }), - makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')), - ] - ] -} - -const getLast = (data) => - Array.isArray(data) ? data.at(-1) : data - -const isMseEnabled = (dataSaub) => { - const lastData = getLast(dataSaub) - return (lastData?.mseState && 2) > 0 -} - -const isTorqueStabEnabled = (dataSpin) => { - const lastData = getLast(dataSpin) - return lastData?.state === 7 -} +const getLast = (data) => Array.isArray(data) ? data.at(-1) : data +const isMseEnabled = (dataSaub) => (getLast(dataSaub)?.mseState && 2) > 0 +const isTorqueStabEnabled = (dataSpin) => getLast(dataSpin)?.state === 7 const isSpinEnabled = (dataSpin) => { - const lastData = getLast(dataSpin) - return lastData?.state > 0 && lastData?.state !== 6 + const lastData = getLast(dataSpin) + return lastData?.state > 0 && lastData?.state !== 6 } export const normalizeData = (data) => data?.map(item => ({ - ...item, - rotorSpeed: item.rotorSpeed < 1 ? 0 : item.rotorSpeed, - rotorTorque: item.rotorTorque < 1 ? 0 : item.rotorTorque, - blockSpeed: Math.abs(item.blockSpeed) + ...item, + rotorSpeed: item.rotorSpeed < 1 ? 0 : item.rotorSpeed, + rotorTorque: item.rotorTorque < 1 ? 0 : item.rotorTorque, + blockSpeed: Math.abs(item.blockSpeed) })) ?? [] const dateSorter = makeDateSorter('date') +const makeSubjectSubsription = (subject$, handler) => { + const subscribtion = subject$.pipe( + buffer(subject$.pipe(throttleTime(700))) + ).subscribe((data) => handler(data.flat().filter(Boolean))) + + return () => subscribtion.unsubscribe() +} + const TelemetryView = memo(() => { - const [dataSaub, setDataSaub] = useState([]) - const [dataSpin, setDataSpin] = useState([]) - const [chartInterval, setChartInterval] = useState(defaultPeriod) - const [showLoader, setShowLoader] = useState(false) - const [flowChartData, setFlowChartData] = useState([]) - const [rop, setRop] = useState(null) - const [domain, setDomain] = useState({}) - const [chartMethods, setChartMethods] = useState() + const [dataSaub, setDataSaub] = useState([]) + const [dataSpin, setDataSpin] = useState([]) + const [chartInterval, setChartInterval] = useState(defaultPeriod) + const [showLoader, setShowLoader] = useState(false) + const [flowChartData, setFlowChartData] = useState([]) + const [rop, setRop] = useState(null) + const [domain, setDomain] = useState({}) + const [chartMethods, setChartMethods] = useState() - const [well, updateWell] = useWell() + const [well, updateWell] = useWell() - const saubSubject$ = useMemo(() => new BehaviorSubject(), []) - const spinSubject$ = useMemo(() => new BehaviorSubject(), []) + const saubSubject$ = useMemo(() => new BehaviorSubject(), []) + const spinSubject$ = useMemo(() => new BehaviorSubject(), []) - const handleDataSaub = useCallback((data) => { - if (data) { - const dataSaub = normalizeData(data) - setDataSaub((prev) => { - const out = [...prev, ...dataSaub] - out.sort(dateSorter) - return out - }) - } - }, []) + const handleDataSaub = useCallback((data) => { + if (!data) return - const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), []) + const dataSaub = normalizeData(data) + setDataSaub((prev) => { + const out = [...prev, ...dataSaub] + out.sort(dateSorter) + return out + }) + }, []) - useEffect(() => { - const subscribtion = saubSubject$.pipe( - buffer(saubSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSaub(data.flat().filter(Boolean))) + const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), []) - return () => subscribtion.unsubscribe() - }, [saubSubject$]) + const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) - useEffect(() => { - const subscribtion = spinSubject$.pipe( - buffer(spinSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSpin(data.flat().filter(Boolean))) + 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 + } - return () => subscribtion.unsubscribe() - }, [spinSubject$]) + for (j = dataSaub.length - 1; j >= i; j--) { + const date = +new Date(dataSaub[i]?.date) + if (date <= +domain.max) break + } - useEffect(() => { - 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) - handleDataSpin(dataSpin) - }, - null, - `Не удалось получить данные`, - { actionName: 'Получение данных по скважине', well } + if (i >= j) return [] + return dataSaub.slice(i, j) + }, [dataSaub, domain]) + + const chartGroups = useMemo(() => makeChartGroups(flowChartData), [flowChartData]) + + useEffect(() => makeSubjectSubsription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub]) + useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) + + useEffect(() => { + 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) + handleDataSpin(dataSpin) + }, + null, + `Не удалось получить данные`, + { actionName: 'Получение данных по скважине', well } + ) + }, [well, chartInterval, handleDataSpin, handleDataSaub]) + + useEffect(() => { + const unsubscribe = Subscribe( + 'hubs/telemetry', `well_${well.id}`, + { methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) }, + { methodName: 'ReceiveDataSpin', handler: (data) => spinSubject$.next(data) } + ) + + return () => unsubscribe() + }, [well.id, saubSubject$, spinSubject$]) + + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) + setRop(rop) + }, + setShowLoader, + `Не удалось загрузить данные`, + { actionName: 'Получение данных по скважине', well } + ) + }, [well]) + + useEffect(() => { + if (dataSaub.length <= 0) return + const last = new Date(dataSaub.at(-1).date) + setDomain({ + min: new Date(+last - chartInterval * 1000), + max: last + }) + }, [dataSaub, chartInterval]) + + return ( + + + + + + + Интервал: + + + chartMethods?.setSettingsVisible(true)}>Настроить графики + + Статус: + + Неизвестно + В работе + Завершено + + + + + + + + + MSE + + + + + + + + + formatDate(d, 'YYYY-MM-DD') + }} + plugins={{ + menu: { enabled: false }, + cursor: { + render: cursorRender, + }, + }} + height={'70vh'} + /> + + + + + + ) - }, [well, chartInterval, handleDataSpin, handleDataSaub]) - - useEffect(() => { - const unsubscribe = Subscribe( - 'hubs/telemetry', `well_${well.id}`, - { methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) }, - { methodName: 'ReceiveDataSpin', handler: (data) => spinSubject$.next(data) } - ) - - return () => unsubscribe() - }, [well.id, saubSubject$, spinSubject$]) - - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) - setRop(rop) - }, - setShowLoader, - `Не удалось загрузить данные`, - { actionName: 'Получение данных по скважине', well } - ) - }, [well]) - - const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) - - useEffect(() => { - if (dataSaub.length <= 0) return - const last = new Date(dataSaub.at(-1).date) - setDomain({ - min: new Date(+last - chartInterval * 1000), - max: last - }) - }, [dataSaub, chartInterval]) - - 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]) - - return ( - - - - - - - Интервал: - - - chartMethods?.setSettingsVisible(true)}>Настроить графики - - Статус: - - Неизвестно - В работе - Завершено - - - - - - - - - MSE - - - - - - - - - formatDate(d, 'YYYY-MM-DD') - }} - plugins={{ - menu: { enabled: false }, - cursor: { - render: cursorRender, - }, - }} - height={'70vh'} - /> - - - - - - - ) }) export default withPermissions(TelemetryView, [ - 'DrillFlowChart.get', - 'OperationStat.get', - 'TelemetryDataSaub.get', - 'TelemetryDataSpin.get', - 'Well.get', + 'DrillFlowChart.get', + 'OperationStat.get', + 'TelemetryDataSaub.get', + 'TelemetryDataSpin.get', + 'Well.get', ])