diff --git a/src/components/ArchiveColumn.jsx b/src/components/ArchiveColumn.jsx deleted file mode 100644 index 60c5858..0000000 --- a/src/components/ArchiveColumn.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button, Select, Tag, Popover, Row, Tooltip } from 'antd'; -import { ChartTimeArchive } from './charts/ChartTimeArchive'; -import { DeleteOutlined } from '@ant-design/icons'; - -const { Option } = Select; - -const linesCollection = [ - { label: "Глубина забоя", xAccessorName: "wellDepth", color: '#f00' }, - { label: "Положение инструмента", xAccessorName: "bitDepth", color: '#ff0' }, - { label: "Положение талевого блока", xAccessorName: "blockPosition", color: '#f0f' }, - { label: "Талевый блок. Мин положение", xAccessorName: "blockPositionMin", color: '#0ff' }, - { label: "Талевый блок. Макс положение", xAccessorName: "blockPositionMax", color: '#0f0' }, - { label: "Скорость талевого блока", xAccessorName: "blockSpeed", color: '#00f' }, - - { label: "Скорости талевого блока. Задание", xAccessorName: "blockSpeedSp", color: '#c00' }, - { label: "Талевый блок. Задание скорости для роторного бурения", xAccessorName: "blockSpeedSpRotor", color: '#cc0' }, - { label: "Талевый блок. Задание скорости для режима слайда", xAccessorName: "blockSpeedSpSlide", color: '#c0c' }, - { label: "Талевый блок. Задание скорости для проработки", xAccessorName: "blockSpeedSpDevelop", color: '#0cc' }, - { label: "Давление", xAccessorName: "pressure", color: '#0c0' }, - { label: "Давление. Холостой ход", xAccessorName: "pressureIdle", color: '#00c' }, - - { label: "Давление. Задание", xAccessorName: "pressureSp", color: '#900' }, - { label: "Давление. Задание для роторного бурения", xAccessorName: "pressureSpRotor", color: '#990' }, - { label: "Давление. Задание для режима слайда", xAccessorName: "pressureSpSlide", color: '#909' }, - { label: "Давление. Задание для проработки", xAccessorName: "pressureSpDevelop", color: '#099' }, - { label: "Давление дифф. Аварийное макс.", xAccessorName: "pressureDeltaLimitMax", color: '#090' }, - { label: "Осевая нагрузка", xAccessorName: "axialLoad", color: '#009' }, - - { label: "Осевая нагрузка. Задание", xAccessorName: "axialLoadSp", color: '#600' }, - { label: "Осевая нагрузка. Аварийная макс.", xAccessorName: "axialLoadLimitMax", color: '#660' }, - { label: "Вес на крюке", xAccessorName: "hookWeight", color: '#606' }, - { label: "Вес на крюке. Холостой ход", xAccessorName: "hookWeightIdle", color: '#066' }, - { label: "Вес на крюке. Посадка", xAccessorName: "hookWeightLimitMin", color: '#060' }, - { label: "Вес на крюке. Затяжка", xAccessorName: "hookWeightLimitMax", color: '#006' }, - - { label: "Момент на роторе", xAccessorName: "rotorTorque", color: '#300' }, - { label: "Момент на роторе. Холостой ход", xAccessorName: "rotorTorqueIdle", color: '#330' }, - { label: "Момент на роторе. Задание", xAccessorName: "rotorTorqueSp", color: '#303' }, - { label: "Момент на роторе. Аварийный макс.", xAccessorName: "rotorTorqueLimitMax", color: '#033' }, - { label: "Обороты ротора", xAccessorName: "rotorSpeed", color: '#030' }, - { label: "Расход", xAccessorName: "flow", color: '#003' }, - - { label: "Расход. Холостой ход", xAccessorName: "flowIdle", color: '#666' }, - { label: "Расход. Аварийный макс.", xAccessorName: "flowDeltaLimitMax", color: '#ccc' }, -] - -const tagRender = ({ label, value, closable, onClose }) =>{ - const onPreventMouseDown = event => { - event.preventDefault(); - event.stopPropagation(); - }; - - let color = linesCollection.find(l=>l.xAccessorName === value)?.color - return ( - -       -  {label} - - ); -} - -export function ArchiveColumn({ data, config, rangeDate, chartRatio, onRemoveChart, onSaveConfig }) { - const [lines, setLines] = useState([]); - - useEffect(() => { - setLines(config.lines); - },[config]); - - const handleLinesSetChange = (linesKeys) => { - let newLines = linesCollection.filter(line => linesKeys.includes(line.xAccessorName)); - config.lines = newLines; - if(onSaveConfig) - onSaveConfig() - setLines(newLines); - }; - - let selectedValues = lines?.map(line=>line.xAccessorName)??[] - - const select = ; - - const popBar = - {select} - - - - - - return ( - <> - -
- -
-
- - ); -} diff --git a/src/components/PeriodPicker.tsx b/src/components/PeriodPicker.tsx new file mode 100644 index 0000000..615caa8 --- /dev/null +++ b/src/components/PeriodPicker.tsx @@ -0,0 +1,31 @@ +import { Select } from 'antd' + +export const defaultPeriod = '600' + +const timePeriodCollection = [ + { value: '60', label: '1 минута' }, + { value: '300', label: '5 минут' }, + { value: '600', label: '10 минут' }, + { value: '1800', label: '30 минут' }, + { value: '3600', label: '1 час' }, + { value: '21600', label: '6 часов' }, + { value: '43200', label: '12 часов' }, + { value: '86400', label: '24 часа' } +] + +interface PeriodPickerProps { + defaultValue?: string + onChange?: (value: string, option: any) => void +} + +export const PeriodPicker = ({ defaultValue = defaultPeriod, onChange }: PeriodPickerProps) => ( + +) + +export default PeriodPicker diff --git a/src/components/charts/ChartDepth.jsx b/src/components/charts/ChartDepth.jsx deleted file mode 100644 index 0af149e..0000000 --- a/src/components/charts/ChartDepth.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import ChartDataLabels from 'chartjs-plugin-datalabels' - -const defaultOptionsDepthLabels = { - borderWidth: 1, - borderColor: black, - borderRadius: 4, - clamp: true, - display: true, - data: { - datasets: [{ - datalabels: { - color: '#0f4000' - } - }] - } -} \ No newline at end of file diff --git a/src/components/charts/ChartTimeArchive.jsx b/src/components/charts/ChartTimeArchive.jsx deleted file mode 100644 index e2bffa7..0000000 --- a/src/components/charts/ChartTimeArchive.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import moment from 'moment'; -import { useEffect, useState} from 'react'; -import {ChartTimeBase} from './ChartTimeBase' - -export const GetRandomColor = () => "#" + Math.floor(Math.random()*16777215).toString(16) - -export const CreateDataset = (lineConfig) => { - let color = lineConfig.borderColor - ?? lineConfig.backgroundColor - ?? lineConfig.color - ?? GetRandomColor() - - let datasets = { - label: lineConfig.label, - data: [], - backgroundColor: lineConfig.backgroundColor ?? color, - borderColor: lineConfig.borderColor ?? color, - borderWidth: lineConfig.borderWidth ?? 1, - borderDash: lineConfig.dash ?? [], - } - return datasets -} - -const ChartOptions = { - // responsive: true, - // plugins:{ - // legend:{ - // display: false, - // maxHeight: 64, - // fullSize: true, - // position: 'chartArea', - // align: 'start', - // } - // } -} - -export const ChartTimeArchive = ({lines, data, yDisplay, rangeDate, chartRatio}) => { - const [dataParams, setDataParams] = useState({data:{datasets: []}}) - - useEffect(() => { - if ((!lines) - || (!data)) - return - - let newDatasets = lines.map(lineCfg => { - let dataset = CreateDataset(lineCfg) - dataset.data = data.map(dataItem => { - return { - x: dataItem[lineCfg.xAccessorName], - y: new Date(dataItem[lineCfg.yAccessorName??'date']) - } - }) - return dataset - }) - - let interval = rangeDate ? (rangeDate[1] - rangeDate[0]) / 1000 : null - let startDate = rangeDate ? rangeDate[0] : moment() - let newParams = { - yInterval: interval, - yStart: startDate, - displayLabels: yDisplay??false, - data: { - datasets: newDatasets - } - } - setDataParams(newParams) - }, [data, lines, yDisplay, rangeDate, chartRatio]) - - const opt = ChartOptions - opt.aspectRatio = chartRatio - - return () -} \ No newline at end of file diff --git a/src/components/charts/ChartTimeBase.tsx b/src/components/charts/ChartTimeBase.tsx index b3a8621..7bd701e 100644 --- a/src/components/charts/ChartTimeBase.tsx +++ b/src/components/charts/ChartTimeBase.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef, useState} from 'react' -import { - Chart, - TimeScale, - LinearScale, - Legend, - LineController, - PointElement, - LineElement, - ChartData, - ChartTypeRegistry, +import { + Chart, + TimeScale, + LinearScale, + Legend, + LineController, + PointElement, + LineElement, + ChartData, + ChartTypeRegistry, ChartOptions} from 'chart.js' import 'chartjs-adapter-moment' import ChartDataLabels from 'chartjs-plugin-datalabels' @@ -65,12 +65,13 @@ const defaultOptions = { color:'#000', } }, - + x:{ type:'linear', position:'top' } }, + parsing: false, elements:{ point:{ radius:0, @@ -134,16 +135,16 @@ export const timeUnitByInterval = (intervalSec: number): String => { if(intervalSec <= 32*60*60) return 'minute' - + if(intervalSec <= 32*12*60*60) return 'hour' - + if(intervalSec <= 32*24*60*60) return 'day' if(intervalSec <= 32*7*24*60*60) return 'week' - + if(intervalSec <= 32*30.4375*24*60*60) return 'month' @@ -186,41 +187,38 @@ export const timeParamsByInterval = (intervalSec: number): TimeParams => { stepSize /= 365.25*24*60*60 break; } - + stepSize = Math.round(stepSize/linesPerInterval) stepSize = stepSize > 0 ? stepSize : 1 return {unit, stepSize} } -export const ChartTimeBase: React.FC = ({options, dataParams}) => { +export const ChartTimeBase: React.FC = ({options, dataParams}) => { const chartRef = useRef(null) const [chart, setChart] = useState() - useEffect(()=>{ - if((chartRef.current)&&(!chart)){ - let thisOptions = {} - Object.assign(thisOptions, defaultOptions, options) + useEffect(() => { + let thisOptions = {} + Object.assign(thisOptions, defaultOptions, options) - let newChart = new Chart(chartRef.current, { - type: 'line', - plugins: [ChartDataLabels], - options: thisOptions, - data: dataParams.data - }) - setChart(newChart) - - return () => chart?.destroy() - } - - if(!chart) return + let newChart = new Chart(chartRef.current, { + type: 'line', + plugins: [ChartDataLabels], + options: thisOptions, + data: { datasets: [] } + }) + setChart(newChart) + return () => newChart?.destroy() + }, [options]) + useEffect(() => { + if (!chart) return; chart.data = dataParams.data - chart.options.aspectRatio = options?.aspectRatio if(dataParams.yStart){ - const interval = Number(dataParams.yInterval ?? 600) const start = new Date(dataParams.yStart) const end = new Date(dataParams.yStart) - end.setSeconds(end.getSeconds() + interval) + const interval = Number(dataParams.yInterval ?? 600) + end.setSeconds(end.getSeconds() + interval) const { unit, stepSize } = timeParamsByInterval(interval) if(chart.options.scales?.y){ @@ -233,7 +231,7 @@ export const ChartTimeBase: React.FC = ({options, dataParams } chart.update() - }, [chart, dataParams, options]) + }, [chart, dataParams]) return() -} \ No newline at end of file +} diff --git a/src/components/charts/Column.jsx b/src/components/charts/Column.jsx new file mode 100644 index 0000000..cc48d04 --- /dev/null +++ b/src/components/charts/Column.jsx @@ -0,0 +1,85 @@ +import React, { useState, useEffect } from 'react' +import { ChartTimeBase } from './ChartTimeBase' + +const chartPluginsOptions = { + plugins: { + datalabels: { + backgroundColor: 'transparent', + borderRadius: 4, + color: '#000B', + display: context => (context.dataset.label === 'wellDepth') && 'auto', + formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`, + padding: 6, + align: 'left', + anchor: 'center', + clip: true + }, + legend: { display: false }, + tooltip: { enable: true } + } +} + +const GetRandomColor = () => '#' + Math.floor(Math.random()*(16**6-1)).toString(16) + +export const GetOrCreateDatasetByLineConfig = (data, lineConfig) => { + let dataset = data?.datasets.find(d => d.label === lineConfig.label) + if(!dataset) { + let color = lineConfig.borderColor + ?? lineConfig.backgroundColor + ?? lineConfig.color + ?? GetRandomColor() + + dataset = { + label: lineConfig.label, + data: [], + backgroundColor: lineConfig.backgroundColor ?? color, + borderColor: lineConfig.borderColor ?? color, + borderWidth: lineConfig.borderWidth ?? 1, + borderDash: lineConfig.dash ?? [], + showLine: lineConfig.showLine ?? !lineConfig.isShape, + fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none') + } + + data.datasets.push(dataset) + } + return dataset +} + +export const Column = ({ lineGroup, data, postParsing, additionalPointData, interval, yDisplay, yStart, savePreviousData }) => { + const [dataParams, setDataParams] = useState({data: {datasets:[]}, yStart, }) + + useEffect(()=>{ + if((lineGroup.length === 0) || (data.length === 0)) return + + setDataParams((preDataParams) => { + lineGroup.forEach(lineCfg => { + const dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, lineCfg) + let points = data.map(dataItem => ({ + x: lineCfg.xConstValue ?? dataItem[lineCfg.xAccessorName], + label: dataItem[lineCfg.xAccessorName], + y: new Date(dataItem[lineCfg.yAccessorName]), + depth: dataItem.wellDepth, + ...additionalPointData?.(dataItem, lineCfg) + })).filter(point => (point.x ?? null) !== null && (point.y ?? null) !== null) + + if (savePreviousData) + points = [...dataset.data, ...points] + + if(points?.length > 2) + points.sort((a,b) => a.y > b.y ? 1 : -1) + + dataset.data = points + }) + + preDataParams.yStart = yStart + preDataParams.yInterval = interval + preDataParams.displayLabels = yDisplay + + postParsing?.(preDataParams) + return {...preDataParams} + }) + + }, [data, lineGroup, interval, yDisplay, yStart, postParsing, savePreviousData, additionalPointData]) + + return +} diff --git a/src/pages/Archive.jsx b/src/pages/Archive.jsx deleted file mode 100644 index cb75ad5..0000000 --- a/src/pages/Archive.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useRef, useLayoutEffect, useState, useEffect } from 'react' -import { - Button, - DatePicker, - Row, - Col, - Tooltip} from 'antd' -import { TelemetryDataSaubService } from '../services/api' -import {generateUUID} from '../services/UidGenerator' -import { ArchiveColumn } from '../components/ArchiveColumn' -import moment from 'moment' -import { notify } from "../components/factory" -import LoaderPortal from '../components/LoaderPortal' - -const { RangePicker } = DatePicker - -const SaveObject = (key, obj) => { - let json = JSON.stringify(obj) - localStorage.setItem(key, json) -} - -const LoadObject = (key) => { - let json = localStorage.getItem(key) - return json ? JSON.parse(json) : null -} - -export default function Archive({idWell}) { - const [saubData, setSaubData] = useState([]) - const [chartsCfgs, setChartsCfgs] = useState([]) - const [rangeDate, setRangeDate] = useState([moment().subtract(3,'hours'), moment()]) - const [geometry, setGeometry] = useState({ratioRest:1, ratio1st:1, wRest:.5, w1st:.5}) - const [loader, setLoader] = useState(false) - const chartsCfgsKey = 'chartsCfgs' - const chartsContainerRef = useRef(); - - const handleReceiveDataSaub = (data) => { - if (data) - setSaubData(data) - } - - const onAddChart = () => { - let newChartCfgs = [...chartsCfgs, {id: generateUUID(), yDisplay: false, aspectRatio:1}] - setChartsCfgs(newChartCfgs) - } - - const onRemoveChart = (id) => { - let newChartCfgs = chartsCfgs.filter(cfg => cfg.id !== id ) - setChartsCfgs(newChartCfgs) - } - - const onSaveConfig = ()=>{ - SaveObject(chartsCfgsKey, chartsCfgs) - } - - const onChangeRange = (range) => { - setRangeDate(range) - } - - useLayoutEffect(()=>{ - if(chartsContainerRef.current && chartsCfgs?.length){ - let w = chartsContainerRef.current.offsetWidth //1792 - w = w > 0 ? w : 1792 - let ot = chartsContainerRef.current.offsetTop - let ph = chartsContainerRef.current.offsetParent.offsetHeight - let h = ph - ot - 32 //761 - h = h > 0 ? h : 761 - - let chartsCount = chartsCfgs.length - - let labelLenght = 8 - let borderWidth = 8 - let wRest = Math.floor((w - labelLenght)/chartsCount) - borderWidth - let w1st = wRest + labelLenght - - let ratio1st = w1st/h - let ratioRest = wRest/h - - setGeometry({ratio1st, ratioRest, w1st, wRest}) - } - },[chartsContainerRef, chartsCfgs]) - - useEffect(() => { - let cfgs = LoadObject(chartsCfgsKey) - if(cfgs) - setChartsCfgs(cfgs) - },[]) - - useEffect(()=>{ - SaveObject(chartsCfgsKey, chartsCfgs) - },[chartsCfgs]) - - useEffect(() => { - let interval = (rangeDate[1] - rangeDate[0]) / 1000 - let startDate = rangeDate[0].toISOString() - - setLoader(true) - TelemetryDataSaubService.getData(idWell, startDate, interval, 2048) - .then(handleReceiveDataSaub) - .catch(error => { - notify(`Не удалось загрузить данные по скважине (${idWell}) c ${rangeDate[0]} по ${rangeDate[1]}`, 'error') - console.error(error) - }) - .finally(()=>setLoader(false)) - }, [idWell, rangeDate]); - - let charts = null - if(chartsCfgs.length > 0){ - chartsCfgs[0].yDisplay = true - - charts = chartsCfgs.map((cfg, i) => - - - ) - } - - return (<> - - - - - - - {charts} - - - ) -} \ No newline at end of file diff --git a/src/pages/Archive/ArchiveColumn.jsx b/src/pages/Archive/ArchiveColumn.jsx new file mode 100644 index 0000000..0118086 --- /dev/null +++ b/src/pages/Archive/ArchiveColumn.jsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react' +import { Grid, GridItem } from '../../components/Grid' +import { Column } from '../../components/charts/Column' + +export const ArchiveColumn = ({ lineGroup, data, interval, style, headerHeight, yStart }) => { + const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([]) + const [pv, setPV] = useState([]) + + useEffect(() => { + const lgws = lineGroup.filter(cfg => !cfg.isShape) + setLineGroupWithoutShapes(lgws) + setPV(lgws.filter(line => line.showLabels).map(line => ({ + color: line.color, + label: line.label + }))) + }, [lineGroup]) + + + return ( +
+ + {pv?.map((v, idx) => ( + {v.label} + ))} + + +
+ ) +} diff --git a/src/pages/Archive/ArchiveDisplay.jsx b/src/pages/Archive/ArchiveDisplay.jsx new file mode 100644 index 0000000..7035c82 --- /dev/null +++ b/src/pages/Archive/ArchiveDisplay.jsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' +import { Grid, GridItem } from '../../components/Grid' +import { ArchiveColumn } from './ArchiveColumn' +import { paramsGroups } from '../TelemetryView' + +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, 5) + let x1 = findIdx(end, x0, 3) + 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 ArchiveDisplay = ({data, startDate, interval, onWheel}) => { + const [chartData, setChartData] = useState([]) + + useEffect(() => { + const endDate = new Date(+startDate + interval) + setChartData(cutData(data, startDate, endDate)) + }, [data, startDate, interval]) + + return ( + + {paramsGroups.map((group, index) => ( + + + + ))} + + ) +} diff --git a/src/pages/Archive/index.jsx b/src/pages/Archive/index.jsx new file mode 100644 index 0000000..f22a53b --- /dev/null +++ b/src/pages/Archive/index.jsx @@ -0,0 +1,146 @@ +import moment from 'moment' +import { DatePicker } from 'antd' +import { useState, useEffect } from 'react' +import { TelemetryDataSaubService } from '../../services/api' +import { invokeWebApiWrapperAsync } from '../../components/factory' +import LoaderPortal from '../../components/LoaderPortal' +import { Flex } from '../../components/Grid' +import { PeriodPicker, defaultPeriod } from '../../components/PeriodPicker' +import { ArchiveDisplay, cutData } from './ArchiveDisplay' +import { normalizeData, sortByDate } from '../TelemetryView' + +const DATA_COUNT = 2048 // Колличество точек на подгрузку графика +const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков +const LOADING_TRIGGER = 0.5 +const WHEEL_SENSITIVITY = 1 / 530 + +const getLoadingInterval = (loaded, startDate, interval) => { + // Если данные загружены и дата не заходит за тригер дозагрузка не требуется + if ( + loaded && + +startDate - interval * LOADING_TRIGGER > loaded.start && + +startDate + interval * (LOADING_TRIGGER + 1) < loaded.end + ) + return { loadingStartDate: startDate, newLoaded: loaded, loadingInterval: 0 } + + let loadingStartDate = +startDate - interval * ADDITIVE_PAGES + let loadingEndDate = +startDate + interval * (ADDITIVE_PAGES + 1) + + const newLoaded = { + start: loadingStartDate, + end: loadingEndDate + } + + if (loaded) { + 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 + } +} + +export default function Archive({idWell}) { + const [dataSaub, setDataSaub] = useState([]) + const [chartInterval, setChartInterval] = useState(parseInt(defaultPeriod) * 1000) + const [startDate, setStartDate] = useState(new Date(+new Date() - chartInterval)) + const [showLoader, setShowLoader] = useState(false) + const [loaded, setLoaded] = useState(null) + + const onGraphWheel = (e) => { + if (loaded) { + setStartDate((prevStartDate) => { + const offset = e.deltaY * chartInterval * WHEEL_SENSITIVITY + const nextStartDate = new Date(+prevStartDate + offset) + const lastPossibleDate = new Date(Math.min(loaded.end, new Date()) - chartInterval) + return new Date(Math.max(loaded.start, Math.min(nextStartDate, lastPossibleDate))) + }) + } + } + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const dates = await TelemetryDataSaubService.getDataDatesRange(idWell) + let startDate + if (dates?.from && dates?.to) + startDate = Math.max(new Date(dates.from), +new Date(dates.to) - chartInterval) + else + startDate = +new Date() - chartInterval + setStartDate(new Date(startDate)) + }, + setShowLoader, + `Не удалось загрузить диапозон телеметрии для скважины "${idWell}"` + ), []) + + useEffect(() => { + setStartDate((startDate) => new Date(Math.min(+new Date() - chartInterval, startDate))) + setDataSaub([]) + }, [chartInterval]) + + useEffect(() => { + if (showLoader) return + const { loadingStartDate, loadingInterval, newLoaded } = getLoadingInterval(loaded, startDate, chartInterval) + if (loadingInterval <= 0) return + invokeWebApiWrapperAsync( + async () => { + const data = await TelemetryDataSaubService.getData(idWell, 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) => { + const newData = [...prevDataSaub, ...normalizeData(data)] + newData.sort(sortByDate) + return cutData(newData, loadedStartDate, loadedEndDate) + }) + } + + }, + setShowLoader, + `Не удалось загрузить данные по скважине "${idWell}" c ${startDate.toISOString()} по ${new Date(+startDate + chartInterval).toISOString()}` + ) + }, [idWell, chartInterval, loaded, startDate]) + + return ( + <> + +
+ Начальная дата:  + setStartDate(new Date(startDate))} + value={moment(startDate)} + /> +
+
+ Период:  + setChartInterval(parseInt(val) * 1000)} /> +
+
+ + + + + ) +} diff --git a/src/pages/TelemetryView/Column.jsx b/src/pages/TelemetryView/Column.jsx deleted file mode 100644 index 621d541..0000000 --- a/src/pages/TelemetryView/Column.jsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState, useEffect } from 'react' -import { Grid, GridItem } from '../../components/Grid' -import { ChartTimeBase } from '../../components/charts/ChartTimeBase' -import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter' - -const stroke = (sz = '2px', c = 'white') => ({ textShadow: `-${sz} -${sz} 0 ${c}, ${sz} -${sz} 0 ${c}, -${sz} ${sz} 0 ${c}, ${sz} ${sz} 0 ${c}` }) - -const chartPluginsOptions = { - plugins: { - datalabels: { - backgroundColor: 'transparent', - borderRadius: 4, - color: '#000B', - display: context => (context.dataset.label === 'wellDepth') && 'auto', - formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`, - padding: 6, - align: 'left', - anchor: 'center', - clip: true - }, - legend:{ display: false }, - tooltip: { enable: true } - } -} - -const GetLimitShape = (flowChartData, points, accessor) => { - const min = [], max = [] - - for (let point of points) { - const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd) - if (!program) continue - - min.push({ x: program[`${accessor}Min`], y: new Date(point.y), label: point.label }) - max.push({ x: program[`${accessor}Max`], y: new Date(point.y), label: point.label }) - } - - return min.concat(max.reverse()) ?? [] -} - -const GetRandomColor = () => '#' + Math.floor(Math.random()*16777215).toString(16) -const GetOrCreateDatasetByLineConfig = (data, lineConfig) => { - let dataset = data?.datasets.find(d => d.label === lineConfig.label) - if(!dataset) { - let color = lineConfig.borderColor - ?? lineConfig.backgroundColor - ?? lineConfig.color - ?? GetRandomColor() - - dataset = { - label: lineConfig.label, - data: [], - backgroundColor: lineConfig.backgroundColor ?? color, - borderColor: lineConfig.borderColor ?? color, - borderWidth: lineConfig.borderWidth ?? 1, - borderDash: lineConfig.dash ?? [], - showLine: lineConfig.showLine ?? !lineConfig.isShape, - fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none') - } - data.datasets.push(dataset); - } - return dataset -} - -export const Column = ({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, yDisplay }) => { - const [dataParams, setDataParams] = useState({data: {datasets:[]}, yStart: new Date(), }) - - let dataLast = data?.[data.length - 1] - let pv = lineGroup.filter(line => line.showLabels).map(line => ({ - color: line.color, - label: line.label, - unit: line.units, - value: dataLast?.[line.xAccessorName] - })) - - useEffect(()=>{ - if((lineGroup.length === 0) || (data.length === 0)) return - - setDataParams((preDataParams) => { - lineGroup.forEach(lineCfg => { - if (lineCfg.isShape) return - const dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, lineCfg) - const points = data.map(dataItem => ({ - x: lineCfg.xConstValue ?? dataItem[lineCfg.xAccessorName], - label: dataItem[lineCfg.xAccessorName], - y: new Date(dataItem[lineCfg.yAccessorName]), - depth: dataItem.wellDepth - })).filter(point => (point.x ?? null) !== null && (point.y ?? null) !== null) - - const lineData = [ ...dataset.data, ...points,] - if(points?.length > 2) - lineData.sort((a,b) => a.y > b.y ? 1 : -1) - if(lineData.length > 1024) - lineData.splice(0, (1024 - lineData.length)) - - dataset.data = lineData - - //Area - lineGroup.filter(cfg => cfg.isShape && cfg.xAccessorName === lineCfg.xAccessorName).forEach(areaCfg => { - const dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, areaCfg) - dataset.data = GetLimitShape(flowChartData, lineData, areaCfg.xAccessorName) - }) - }) - - preDataParams.yStart = new Date(Math.max(new Date(dataLast.date), preDataParams.yStart ?? new Date(0))) - preDataParams.yStart.setSeconds(preDataParams.yStart.getSeconds() - interval * 0.97) - preDataParams.yInterval = interval - preDataParams.displayLabels = yDisplay ?? false - return {...preDataParams} - }) - - }, [data, lineGroup, interval, yDisplay, flowChartData, dataLast]) - - return ( -
- - {pv?.map((v, idx) => ( - {v.label} - ))} - -
- - {pv?.map((v, idx) => ( - {v.value?.toFixed(2) ?? '--'} {v.unit} - ))} - - -
- -
- ) -} diff --git a/src/pages/TelemetryView/MonitoringColumn.jsx b/src/pages/TelemetryView/MonitoringColumn.jsx new file mode 100644 index 0000000..97e4be0 --- /dev/null +++ b/src/pages/TelemetryView/MonitoringColumn.jsx @@ -0,0 +1,95 @@ +import { Grid, GridItem } from '../../components/Grid' +import { Column, GetOrCreateDatasetByLineConfig } from '../../components/charts/Column' +import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter' +import { useEffect, useState } from 'react' + +const stroke = (sz = '2px', c = 'white') => ({ textShadow: `-${sz} -${sz} 0 ${c}, ${sz} -${sz} 0 ${c}, -${sz} ${sz} 0 ${c}, ${sz} ${sz} 0 ${c}` }) + +const GetLimitShape = (flowChartData, points, accessor) => { + const min = [], max = [] + + for (let point of points) { + const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd) + if (!program) continue + + min.push({ x: program[`${accessor}Min`], y: new Date(point.y), label: point.label }) + max.push({ x: program[`${accessor}Max`], y: new Date(point.y), label: point.label }) + } + + return min.concat(max.reverse()) ?? [] +} + +const RemoveSimilar = (input) => { + const data = [input[0]] + for (let i = 1; i < input.length; i++) + if (input[i].y !== input[i - 1].y) + data.push(input[i]) + return data +} + +export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, pointCount }) => { + const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([]) + const dataLast = data?.[data.length - 1] + const yStart = new Date(+(dataLast?.date ? new Date(dataLast.date) : new Date()) - interval * 1000 * 0.97) + const pv = lineGroup.filter(line => line.showLabels).map(line => ({ + color: line.color, + label: line.label, + unit: line.units, + value: dataLast?.[line.xAccessorName] + })) + + const addPointData = (point) => ({ depth: point.wellDepth }) + + const postParsing = (data) => { + lineGroupWithoutShapes.forEach(lineCfg => { + const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg) + + lineDataSet.data = RemoveSimilar(lineDataSet.data) + if (lineDataSet.data.length > pointCount) + lineDataSet.data.splice(0, pointCount - lineDataSet.data.length) + + if (flowChartData) { + lineGroup.filter(cfg => cfg.isShape && cfg.xAccessorName === lineCfg.xAccessorName).forEach(areaCfg => { + const dataset = GetOrCreateDatasetByLineConfig(data.data, areaCfg) + dataset.data = GetLimitShape(flowChartData, lineDataSet.data, areaCfg.xAccessorName) + }) + } + }) + } + + useEffect(() => { + setLineGroupWithoutShapes(lineGroup.filter(cfg => !cfg.isShape)) + }, [lineGroup]) + + return ( +
+ + {pv?.map((v, idx) => ( + {v.label} + ))} + +
+ + {pv?.map((v, idx) => ( + {v.value?.toFixed(2) ?? '--'} {v.unit} + ))} + + +
+ +
+ ) +} + +MonitoringColumn.defaultProps = { + pointCount: 2048 +} diff --git a/src/pages/TelemetryView/index.jsx b/src/pages/TelemetryView/index.jsx index f3056c8..a4bc8f1 100644 --- a/src/pages/TelemetryView/index.jsx +++ b/src/pages/TelemetryView/index.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { Select } from 'antd' -import { Column } from './Column' +import { MonitoringColumn } from './MonitoringColumn' import { CustomColumn } from './CustomColumn' import ActiveMessagesOnline from './ActiveMessagesOnline' import { ModeDisplay } from './ModeDisplay' @@ -22,6 +22,7 @@ import MomentStabPicEnabled from '../../images/DempherOn.png' import MomentStabPicDisabled from '../../images/DempherOff.png' import SpinPicEnabled from '../../images/SpinEnabled.png' import SpinPicDisabled from '../../images/SpinDisabled.png' +import { PeriodPicker, defaultPeriod } from '../../components/PeriodPicker' import '../../styles/message.css' @@ -250,21 +251,15 @@ const rotorTorqueGroup = [ } ] -const paramsGroups = [blockHeightGroup, blockSpeedGroup, pressureGroup, axialLoadGroup, hookWeightGroup, rotorTorqueGroup] - -const timePeriodCollection = [ - { value: '60', label: '1 минута' }, - { value: '300', label: '5 минут' }, - { value: '600', label: '10 минут' }, - { value: '1800', label: '30 минут' }, - { value: '3600', label: '1 час' }, - { value: '21600', label: '6 часов' }, - { value: '43200', label: '12 часов' }, - { value: '86400', label: '24 часа' } +export const paramsGroups = [ + blockHeightGroup, + blockSpeedGroup, + pressureGroup, + axialLoadGroup, + hookWeightGroup, + rotorTorqueGroup ] -const defaultChartInterval = '600' - const getLast = (data) => Array.isArray(data) ? data.slice(-1)[0] : data @@ -295,27 +290,27 @@ const getIndexOfDrillingBy = (dataSaub) => { return order[idFeedRegulator] ?? -1 } +export const sortByDate = (a, b) => a.date > b.date ? 1 : -1 +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) +})) ?? [] + export default function TelemetryView({ idWell }) { const [dataSaub, setDataSaub] = useState([]) const [dataSpin, setDataSpin] = useState([]) - const [chartInterval, setChartInterval] = useState(defaultChartInterval) + const [chartInterval, setChartInterval] = useState(defaultPeriod) const [wellData, setWellData] = useState({ idState: 0 }) const [showLoader, setShowLoader] = useState(false) const [flowChartData, setFlowChartData] = useState([]) - const options = timePeriodCollection.map((line) => ) - const handleDataSaub = (data) => { if (data) { - data.forEach((_, idx) => { - if (data[idx].rotorSpeed < 1) - data[idx].rotorSpeed = 0; - if (data[idx].rotorTorque < 1) - data[idx].rotorTorque = 0; - data[idx].blockSpeed = Math.abs(data[idx].blockSpeed) - }) - data.sort((a, b) => a.date > b.date ? 1 : -1) - setDataSaub(data) + const dataSaub = normalizeData(data) + dataSaub.sort(sortByDate) + setDataSaub(dataSaub) } } @@ -375,9 +370,7 @@ export default function TelemetryView({ idWell }) {
Интервал:  - +
Статус:  @@ -399,14 +392,16 @@ export default function TelemetryView({ idWell }) { {paramsGroups.map((group, index) => - + showBorder={getIndexOfDrillingBy(dataSaub) === index} + /> )} diff --git a/src/services/api/models/ClusterDtoPaginationContainer.ts b/src/services/api/models/ClusterDtoPaginationContainer.ts index ae329ff..5b6d4e3 100644 --- a/src/services/api/models/ClusterDtoPaginationContainer.ts +++ b/src/services/api/models/ClusterDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type ClusterDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/CompanyDtoPaginationContainer.ts b/src/services/api/models/CompanyDtoPaginationContainer.ts index 006ad77..e3a7dcc 100644 --- a/src/services/api/models/CompanyDtoPaginationContainer.ts +++ b/src/services/api/models/CompanyDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type CompanyDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/DepositDtoPaginationContainer.ts b/src/services/api/models/DepositDtoPaginationContainer.ts index 861b9bd..704269c 100644 --- a/src/services/api/models/DepositDtoPaginationContainer.ts +++ b/src/services/api/models/DepositDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type DepositDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/FilePublishInfoDto.ts b/src/services/api/models/FilePublishInfoDto.ts index 8d0a093..59099f9 100644 --- a/src/services/api/models/FilePublishInfoDto.ts +++ b/src/services/api/models/FilePublishInfoDto.ts @@ -6,4 +6,4 @@ export type FilePublishInfoDto = { publisherLogin?: string | null; date?: string; webStorageFileUrl?: string | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/TelemetryDtoPaginationContainer.ts b/src/services/api/models/TelemetryDtoPaginationContainer.ts index 0d23b24..ef7a641 100644 --- a/src/services/api/models/TelemetryDtoPaginationContainer.ts +++ b/src/services/api/models/TelemetryDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type TelemetryDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/UserDtoPaginationContainer.ts b/src/services/api/models/UserDtoPaginationContainer.ts index 650abe0..d1b285d 100644 --- a/src/services/api/models/UserDtoPaginationContainer.ts +++ b/src/services/api/models/UserDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type UserDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/UserRoleDtoPaginationContainer.ts b/src/services/api/models/UserRoleDtoPaginationContainer.ts index d77dd06..aa0e25a 100644 --- a/src/services/api/models/UserRoleDtoPaginationContainer.ts +++ b/src/services/api/models/UserRoleDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type UserRoleDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +} diff --git a/src/services/api/models/WellDtoPaginationContainer.ts b/src/services/api/models/WellDtoPaginationContainer.ts index f3f299d..d0bec27 100644 --- a/src/services/api/models/WellDtoPaginationContainer.ts +++ b/src/services/api/models/WellDtoPaginationContainer.ts @@ -9,4 +9,4 @@ export type WellDtoPaginationContainer = { take?: number; count?: number; items?: Array | null; -} \ No newline at end of file +}