From 3d1d3bbb488b9308b692d2310e8912d8cc308fdc Mon Sep 17 00:00:00 2001 From: zhaohui Date: Sat, 29 Oct 2022 15:51:44 +0800 Subject: [PATCH] Modify Device doc file and Code Signed-off-by: zhaohui --- device/README_zh.md | 85 + device/device_command/BUILD.gn | 12 +- device/device_command/ByTrace.cpp | 95 +- device/device_command/CPU.cpp | 163 +- device/device_command/Capture.cpp | 36 + device/device_command/DDR.cpp | 16 +- device/device_command/FPS.cpp | 324 ++-- device/device_command/GPU.cpp | 99 +- device/device_command/Power.cpp | 98 +- device/device_command/RAM.cpp | 84 +- device/device_command/README_zh.md | 89 +- device/device_command/Temperature.cpp | 102 +- device/device_command/gp_utils.cpp | 145 -- device/device_command/include/ByTrace.h | 55 +- device/device_command/include/CPU.h | 56 +- device/device_command/include/Capture.h | 37 + device/device_command/include/DDR.h | 28 +- device/device_command/include/FPS.h | 47 +- device/device_command/include/GPU.h | 35 +- device/device_command/include/Power.h | 31 +- device/device_command/include/RAM.h | 58 +- device/device_command/include/Temperature.h | 37 +- device/device_command/include/common.h | 103 + device/device_command/include/gp_data.h | 23 - device/device_command/include/gp_utils.h | 38 - .../include/smartperf_command.h | 100 +- .../{socket_profiler.h => sp_csv_util.h} | 77 +- .../include/{gp_constant.h => sp_data.h} | 24 +- device/device_command/include/sp_profiler.h | 28 + .../include/sp_profiler_factory.h | 30 + .../device_command/include/sp_server_socket.h | 45 + .../device_command/include/sp_thread_socket.h | 90 + device/device_command/include/sp_utils.h | 100 + device/device_command/profiler.cpp | 140 -- device/device_command/smartperf_command.cpp | 210 +- device/device_command/smartperf_main.cpp | 29 +- device/device_command/socket_profiler.cpp | 213 --- device/device_command/sp_profiler_factory.cpp | 123 ++ .../profiler.h => sp_server_socket.cpp} | 94 +- device/device_command/sp_utils.cpp | 165 ++ device/device_ui/.gitignore | 13 - .../AppScope/{app.json5 => app.json} | 2 +- device/device_ui/BUILD.gn | 43 + device/device_ui/README_zh.md | 190 +- device/device_ui/build-profile.json5 | 26 - device/device_ui/entry/build-profile.json | 14 + device/device_ui/entry/build-profile.json5 | 22 - .../entry/src/main/cpp/CMakeLists.txt | 10 - device/device_ui/entry/src/main/cpp/FPS.cpp | 172 -- .../device_ui/entry/src/main/cpp/gp_utils.cpp | 108 -- .../device_ui/entry/src/main/cpp/profiler.cpp | 151 -- .../main/cpp/types/libsmartperf/package.json | 4 - .../src/main/ets/MainAbility/MainAbility.ts | 45 +- .../main/ets/common/FloatWindowComponent.ets | 5 +- .../main/ets/common/database/DatabaseUtils.ts | 14 +- .../ets/common/entity/LocalConfigEntity.ts | 19 +- .../main/ets/common/profiler/ProfilerTask.ets | 32 +- .../main/ets/common/profiler/WorkerHandler.ts | 45 + .../ets/common/profiler/base/BaseProfiler.ets | 2 +- .../profiler/base/BaseProfilerUtils.ets | 6 +- .../common/profiler/base/ProfilerFactory.ets | 16 +- .../src/main/ets/common/profiler/item/CPU.ets | 17 +- .../src/main/ets/common/profiler/item/FPS.ets | 28 +- .../src/main/ets/common/profiler/item/GPU.ets | 54 +- .../profiler/item/{NetWork.ets => NetWork.ts} | 0 .../main/ets/common/profiler/item/Power.ets | 5 +- .../src/main/ets/common/profiler/item/RAM.ets | 20 +- .../src/main/ets/common/ui/detail/Load.ets | 1049 ++++++++++ .../main/ets/common/ui/detail/Performance.ets | 251 +++ .../src/main/ets/common/ui/detail/Power.ets | 148 ++ .../src/main/ets/common/ui/detail/Summary.ets | 122 +- .../main/ets/common/ui/detail/Temperature.ets | 708 +++++++ .../detail/chart/animation/ChartAnimator.ets | 203 ++ .../common/ui/detail/chart/charts/Chart.ets | 1687 +++++++++++++++++ .../ui/detail/chart/charts/LineChart.ets | 318 ++++ .../ui/detail/chart/components/AxisBase.ets | 801 ++++++++ .../detail/chart/components/ComponentBase.ets | 180 ++ .../detail/chart/components/Description.ets | 102 + .../ui/detail/chart/components/IMarker.ets | 59 + .../ui/detail/chart/components/Legend.ets | 997 ++++++++++ .../detail/chart/components/LegendEntry.ets | 90 + .../ui/detail/chart/components/LegendView.ets | 102 + .../ui/detail/chart/components/LimitLine.ets | 204 ++ .../ui/detail/chart/components/PathView.ets | 445 +++++ .../ui/detail/chart/components/XAxis.ets | 132 ++ .../ui/detail/chart/components/YAxis.ets | 471 +++++ .../chart/components/renderer/XAxisView.ets | 128 ++ .../chart/components/renderer/YAxisView.ets | 151 ++ .../data/BarLineScatterCandleBubbleData.ets | 32 + .../BarLineScatterCandleBubbleDataSet.ets | 56 + .../ui/detail/chart/data/BaseDataSet.ets | 532 ++++++ .../common/ui/detail/chart/data/BaseEntry.ets | 91 + .../ui/detail/chart/data/CandleDataSet.ets | 291 +++ .../ui/detail/chart/data/CandleEntry.ets | 129 ++ .../common/ui/detail/chart/data/ChartData.ets | 823 ++++++++ .../common/ui/detail/chart/data/DataSet.ets | 424 +++++ .../common/ui/detail/chart/data/EntryOhos.ets | 98 + .../common/ui/detail/chart/data/LineData.ets | 29 + .../ui/detail/chart/data/LineDataSet.ets | 394 ++++ .../ui/detail/chart/data/LineRadarDataSet.ets | 148 ++ .../data/LineScatterCandleRadarDataSet.ets | 123 ++ .../ets/common/ui/detail/chart/data/Paint.ets | 712 +++++++ .../ui/detail/chart/data/RadarChartMode.ets | 372 ++++ .../common/ui/detail/chart/data/RadarData.ets | 50 + .../ui/detail/chart/data/RadarDataSet.ets | 132 ++ .../ui/detail/chart/data/RadarEntry.ets | 36 + .../ets/common/ui/detail/chart/data/Rect.ets | 223 +++ .../common/ui/detail/chart/data/Runnable.ets} | 44 +- .../common/ui/detail/chart/data/ScaleMode.ets | 235 +++ .../ui/detail/chart/data/ScatterDataSet.ets | 136 ++ .../common/ui/detail/chart/data/XAixsMode.ets | 83 + .../formatter/DefaultAxisValueFormatter.ets | 62 + .../chart/formatter/DefaultFillFormatter.ets} | 85 +- .../chart/formatter/DefaultValueFormatter.ets | 80 + .../chart/formatter/IAxisValueFormatter.ets | 34 + .../detail/chart/formatter/IFillFormatter.ets | 36 + .../chart/formatter/IValueFormatter.ets} | 56 +- .../chart/highlight/ChartHighlighter.ets | 257 +++ .../ui/detail/chart/highlight/Highlight.ets | 223 +++ .../detail/chart/highlight/IHighlighter.ets} | 28 +- .../ui/detail/chart/highlight/Range.ets} | 69 +- ...BarLineScatterCandleBubbleDataProvider.ets | 34 + .../dataprovider/ChartInterface.ets | 81 + .../dataprovider/LineDataProvider.ets | 25 + .../chart/interfaces/datasets/IBarDataSet.ets | 81 + .../IBarLineScatterCandleBubbleDataSet.ets | 27 + .../interfaces/datasets/IBubbleDataSet.ets | 38 + .../interfaces/datasets/ICandleDataSet.ets | 95 + .../chart/interfaces/datasets/IDataSet.ets | 492 +++++ .../interfaces/datasets/ILineDataSet.ets | 111 ++ .../interfaces/datasets/ILineRadarDataSet.ets | 71 + .../ILineScatterCandleRadarDataSet.ets | 45 + .../interfaces/datasets/IRadarDataSet.ets | 41 + .../interfaces/datasets/IScatterDataSet.ets | 49 + .../detail/chart/jobs/ViewPortJob.ets} | 76 +- .../chart/listener/ChartTouchListener.ets | 154 ++ .../chart/listener/OnChartGestureListener.ets | 88 + .../listener/OnChartValueSelectedListener.ets | 39 + .../ui/detail/chart/renderer/AxisRenderer.ets | 301 +++ .../BarLineScatterCandleBubbleRenderer.ets | 117 ++ .../ui/detail/chart/renderer/DataRenderer.ets | 190 ++ .../detail/chart/renderer/LegendRenderer.ets | 575 ++++++ .../chart/renderer/LineChartRenderer.ets | 583 ++++++ .../chart/renderer/LineRadarRenderer.ets | 53 + .../LineScatterCandleRadarRenderer.ets | 77 + .../chart/renderer/RadarChartRenderer.ets | 472 +++++ .../ui/detail/chart/renderer/Renderer.ets | 32 + .../detail/chart/renderer/XAxisRenderer.ets | 443 +++++ .../renderer/XAxisRendererRadarChart.ets | 83 + .../detail/chart/renderer/YAxisRenderer.ets | 400 ++++ .../renderer/YAxisRendererRadarChart.ets | 239 +++ .../chart/renderer/scatter/IShapeRenderer.ets | 34 + .../ui/detail/chart/utils/ArrayUtils.ets | 31 + .../ui/detail/chart/utils/ColorTemplate.ets | 262 +++ .../common/ui/detail/chart/utils/FSize.ets | 80 + .../ets/common/ui/detail/chart/utils/Fill.ets | 290 +++ .../ui/detail/chart/utils/JArrayList.ets | 196 ++ .../common/ui/detail/chart/utils/JList.ets | 156 ++ .../common/ui/detail/chart/utils/MPPointD.ets | 62 + .../common/ui/detail/chart/utils/MPPointF.ets | 117 ++ .../common/ui/detail/chart/utils/Matrix.ets | 159 ++ .../ui/detail/chart/utils/ObjectPool.ets | 226 +++ .../ui/detail/chart/utils/Poolable.ets} | 12 +- .../ui/detail/chart/utils/Transformer.ets | 456 +++++ .../common/ui/detail/chart/utils/Utils.ets | 719 +++++++ .../ui/detail/chart/utils/ViewPortHandler.ets | 687 +++++++ .../ui/floatwindow/FloatWindowConstant.ets | 56 +- .../common/ui/floatwindow/FloatWindowFun.ts | 94 +- .../ui/floatwindow/utils/FloatWindowUtils.ets | 76 +- .../src/main/ets/common/ui/main/Home.ets | 9 +- .../src/main/ets/common/ui/main/Mine.ets | 4 +- .../src/main/ets/common/ui/main/Report.ets | 4 + .../ets/common/utils/BundleMangerUtils.ts | 32 +- .../src/main/ets/common/utils/CSVUtils.ts | 1 + .../src/main/ets/pages/AppSelectPage.ets | 7 +- .../src/main/ets/pages/CPU0LineChartPage.ets | 18 +- .../src/main/ets/pages/CPU1LineChartPage.ets | 18 +- .../src/main/ets/pages/CPU2LineChartPage.ets | 18 +- .../ets/pages/CurrentNowLineChartPage.ets | 16 +- .../src/main/ets/pages/DDRLineChartPage.ets | 18 +- .../entry/src/main/ets/pages/FloatBall.ets | 140 +- .../src/main/ets/pages/FpsLineChartPage.ets | 95 +- .../src/main/ets/pages/GPULineChartPage.ets | 19 +- .../entry/src/main/ets/pages/LightAdjust.ets | 2 +- .../entry/src/main/ets/pages/LoginPage.ets | 10 +- .../entry/src/main/ets/pages/MainPage.ets | 8 +- .../src/main/ets/pages/RAMLineChartPage.ets | 20 +- .../entry/src/main/ets/pages/ReportDetail.ets | 37 +- .../entry/src/main/ets/pages/SettingsPage.ets | 2 +- .../ets/pages/ShellBackTempLineChartPage.ets | 18 +- .../src/main/ets/pages/StartTestPage.ets | 58 +- .../src/main/ets/pages/TitleWindowPage.ets | 275 +-- .../entry/src/main/ets/workers/worker.js | 134 +- .../src/main/{module.json5 => module.json} | 26 +- device/device_ui/hvigorfile.js | 2 - device/device_ui/package.json | 14 - .../signature/openharmony_smartperf.p7b | Bin 0 -> 3633 bytes device/figures/SmartPerfConfig1.png | Bin 0 -> 12535 bytes device/figures/SmartPerfConfig2.png | Bin 0 -> 11188 bytes device/figures/SmartPerfConfig3.png | Bin 0 -> 13194 bytes device/figures/SmartPerfControl1.png | Bin 0 -> 17170 bytes device/figures/SmartPerfControl2.png | Bin 0 -> 23308 bytes device/figures/SmartPerfReport1.png | Bin 0 -> 9972 bytes device/figures/SmartPerfReport2.png | Bin 0 -> 20418 bytes device/figures/SmartPerfStru.png | Bin 0 -> 12392 bytes 205 files changed, 25548 insertions(+), 3180 deletions(-) create mode 100644 device/README_zh.md create mode 100644 device/device_command/Capture.cpp delete mode 100644 device/device_command/gp_utils.cpp create mode 100644 device/device_command/include/Capture.h create mode 100644 device/device_command/include/common.h delete mode 100644 device/device_command/include/gp_data.h delete mode 100644 device/device_command/include/gp_utils.h rename device/device_command/include/{socket_profiler.h => sp_csv_util.h} (37%) rename device/device_command/include/{gp_constant.h => sp_data.h} (71%) create mode 100644 device/device_command/include/sp_profiler.h create mode 100644 device/device_command/include/sp_profiler_factory.h create mode 100644 device/device_command/include/sp_server_socket.h create mode 100644 device/device_command/include/sp_thread_socket.h create mode 100644 device/device_command/include/sp_utils.h delete mode 100644 device/device_command/profiler.cpp delete mode 100644 device/device_command/socket_profiler.cpp create mode 100644 device/device_command/sp_profiler_factory.cpp rename device/device_command/{include/profiler.h => sp_server_socket.cpp} (32%) create mode 100644 device/device_command/sp_utils.cpp delete mode 100644 device/device_ui/.gitignore rename device/device_ui/AppScope/{app.json5 => app.json} (85%) create mode 100644 device/device_ui/BUILD.gn delete mode 100644 device/device_ui/build-profile.json5 create mode 100644 device/device_ui/entry/build-profile.json delete mode 100644 device/device_ui/entry/build-profile.json5 delete mode 100644 device/device_ui/entry/src/main/cpp/CMakeLists.txt delete mode 100644 device/device_ui/entry/src/main/cpp/FPS.cpp delete mode 100644 device/device_ui/entry/src/main/cpp/gp_utils.cpp delete mode 100644 device/device_ui/entry/src/main/cpp/profiler.cpp delete mode 100644 device/device_ui/entry/src/main/cpp/types/libsmartperf/package.json create mode 100644 device/device_ui/entry/src/main/ets/common/profiler/WorkerHandler.ts rename device/device_ui/entry/src/main/ets/common/profiler/item/{NetWork.ets => NetWork.ts} (100%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/Load.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/Performance.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/Power.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/Temperature.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/animation/ChartAnimator.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/Chart.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/LineChart.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/AxisBase.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/ComponentBase.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Description.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/IMarker.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Legend.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendEntry.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendView.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LimitLine.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/PathView.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/XAxis.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/YAxis.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/XAxisView.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/YAxisView.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleData.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseEntry.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleEntry.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ChartData.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/DataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/EntryOhos.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineData.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineRadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineScatterCandleRadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Paint.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarChartMode.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarData.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarEntry.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Rect.ets rename device/device_ui/entry/src/main/{cpp/gp_utils.h => ets/common/ui/detail/chart/data/Runnable.ets} (56%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScaleMode.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScatterDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/XAixsMode.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/DefaultAxisValueFormatter.ets rename device/device_ui/entry/src/main/{cpp/RAM.cpp => ets/common/ui/detail/chart/formatter/DefaultFillFormatter.ets} (34%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/DefaultValueFormatter.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IAxisValueFormatter.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IFillFormatter.ets rename device/device_ui/entry/src/main/ets/common/{profiler/NativeTaskFun.ts => ui/detail/chart/formatter/IValueFormatter.ets} (33%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/ChartHighlighter.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Highlight.ets rename device/device_ui/entry/src/main/{cpp/RAM.h => ets/common/ui/detail/chart/highlight/IHighlighter.ets} (67%) rename device/device_ui/entry/src/main/{cpp/FPS.h => ets/common/ui/detail/chart/highlight/Range.ets} (43%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/ChartInterface.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/LineDataProvider.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBubbleDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ICandleDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineRadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineScatterCandleRadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IRadarDataSet.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IScatterDataSet.ets rename device/device_ui/entry/src/main/ets/common/{profiler/MainWorkTask.ets => ui/detail/chart/jobs/ViewPortJob.ets} (31%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/ChartTouchListener.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartGestureListener.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartValueSelectedListener.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/AxisRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/BarLineScatterCandleBubbleRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/DataRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LegendRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineChartRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineRadarRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineScatterCandleRadarRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/RadarChartRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/Renderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRendererRadarChart.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRendererRadarChart.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/scatter/IShapeRenderer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ArrayUtils.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ColorTemplate.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/FSize.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Fill.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JArrayList.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JList.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointD.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointF.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Matrix.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ObjectPool.ets rename device/device_ui/entry/src/main/{cpp/types/libsmartperf/index.d.ts => ets/common/ui/detail/chart/utils/Poolable.ets} (76%) create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Transformer.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Utils.ets create mode 100644 device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ViewPortHandler.ets rename device/device_ui/entry/src/main/{module.json5 => module.json} (82%) delete mode 100644 device/device_ui/hvigorfile.js delete mode 100644 device/device_ui/package.json create mode 100644 device/device_ui/signature/openharmony_smartperf.p7b create mode 100644 device/figures/SmartPerfConfig1.png create mode 100644 device/figures/SmartPerfConfig2.png create mode 100644 device/figures/SmartPerfConfig3.png create mode 100644 device/figures/SmartPerfControl1.png create mode 100644 device/figures/SmartPerfControl2.png create mode 100644 device/figures/SmartPerfReport1.png create mode 100644 device/figures/SmartPerfReport2.png create mode 100644 device/figures/SmartPerfStru.png diff --git a/device/README_zh.md b/device/README_zh.md new file mode 100644 index 0000000..cf54a23 --- /dev/null +++ b/device/README_zh.md @@ -0,0 +1,85 @@ +# SmartPerf(Device、Daemon) + +## 概述 + +为支撑OpenHarmony性能测试,我们向用户提供了符合功能需求并且可靠、易用的性能测试工具。支持开发者针对应用进行相应的性能数据采集以及指标计算功能。 + +## 简介 + +SmartPerf(Device、Daemon)是基于OpenHarmony系统开发的性能功耗测试工具,操作简单易用,可提供包括性能、功耗的关键KPI指标,给出具体指标的测试值,包括采集设备的FPS、CPU、GPU、Ftrace等指标数据; + +目前SmartPerf(Device、Daemon)工具提供了两种使用方式,分别为hap应用可视化操作方式(SmartPerf-Device)和shell命令行方式(SmartPerf-Daemon),其中SmartPerf-Device支持可视化操作、悬浮窗控制暂停,悬浮窗实时展示数据,SmartPerf-Daemon主要适用于无屏设备、性能较差的设备,对3568等设备同样支持。 + +## 实现原理 + +SmartPerf工具主要包括SmartPerf-Device、SmartPerf-Daemon两个部分,其中Device采集的FPS、RAM、Trace等指标需要通过发送消息给Daemon端去采集数据,然后接收Daemon回传的数据展示,同时Daemon端也提供了shell命令的方式单独执行采集,工具的主要功能组成如下图: + +![图片说明](figures/SmartPerfStru.png) + +## 约束与限制 + +1.SmartPerf-Device、SmartPerf-Daemon在3.2系统版本后开始预制使用。 + +2.其中SmartPerf-Device的使用必须是具备屏幕的设备。 + +## 环境准备 + +SmartPerf-Daemon执行需要PC连接OpenHarmony设备,如RK3568开发板等。 + +## 执行性能测试 + +**SmartPerf-Device应用可视化使用示例,详细请参考[SmartPerf-Device使用](device_ui/README_zh.md)** + +以下SmartPerf-Device应用内截图以RK3568设备为例。 + +1.应用采集配置。 + +启动SmartPerf-Device进入首页,选择测试应用、测试指标项,点击”开始测试“,拉起测试应用。 +![图片说明](figures/SmartPerfConfig1.png) +![图片说明](figures/SmartPerfConfig2.png) +![图片说明](figures/SmartPerfConfig3.png) + +2.悬浮窗控制采集。 + +点击悬浮窗”start“开始采集,单击悬浮窗”计时器“暂停采集,再次单击继续采集双击计时器,实时展示采集数据,可拖动悬浮框更改悬浮框位置,长按”计时器“,结束采集。 + +![图片说明](figures/SmartPerfControl1.png) +![图片说明](figures/SmartPerfControl2.png) + +3.查看报告。 + +点击“报告”,查看测试报告列表,点击“报告列表”,查看测试指标项详情。 + +![图片说明](figures/SmartPerfReport1.png) +![图片说明](figures/SmartPerfReport2.png) + +**SmartPerf-Daemon命令行使用示例,详细请参考[SmartPerf-Daemon使用](device_command/README_zh.md)** + +1.进入shell, 执行查看帮助命令。 +``` +:# SP_daemon --help +``` +2.执行采集命令。 +``` +:# SP_daemon -N 2 -PKG com.ohos.contacts -c -g -t -p -r +``` + +**采集命令使用示例解析** + +| 命令 | 功能 |是否必选| +| :-----| :--------------------- |:-----| +| -N | 设置采集次数 |是| +| -PKG | 设置包名 | 否| +| -PID | 设置进程pid(对于ram适用) |否| +| -c | 是否采集cpu | 否| +| -g | 是否采集gpu |否| +| -f | 是否采集fps |否| +| -t | 是否采集温度 |否| +| -p | 是否采集电流 |否| +| -r | 是否采集内存 |否| + +**测试结果默认输出路径如下** +``` +报告存放路径:/data/local/tmp/data.csv +``` + diff --git a/device/device_command/BUILD.gn b/device/device_command/BUILD.gn index 2975184..8b6466a 100644 --- a/device/device_command/BUILD.gn +++ b/device/device_command/BUILD.gn @@ -33,27 +33,27 @@ ohos_executable("SP_daemon") { sources = [ "ByTrace.cpp", "CPU.cpp", + "Capture.cpp", "DDR.cpp", "FPS.cpp", "GPU.cpp", "Power.cpp", "RAM.cpp", "Temperature.cpp", - "gp_utils.cpp", - "profiler.cpp", "smartperf_command.cpp", "smartperf_main.cpp", - "socket_profiler.cpp", + "sp_profiler_factory.cpp", + "sp_server_socket.cpp", + "sp_utils.cpp", ] include_dirs = [ "//developtools/profiler/host/smartperf/client/client_command/include", - "//utils/native/base/include", + "//commonlibrary/c_utils/base/include", ] configs = [ ":config" ] - deps = [ "//utils/native/base:utils" ] - subsystem_name = "${OHOS_PROFILER_SUBSYS_NAME}" part_name = "${OHOS_PROFILER_PART_NAME}" + external_deps = [ "c_utils:utils" ] } ## Build so }}} diff --git a/device/device_command/ByTrace.cpp b/device/device_command/ByTrace.cpp index e445884..1477d63 100644 --- a/device/device_command/ByTrace.cpp +++ b/device/device_command/ByTrace.cpp @@ -14,63 +14,78 @@ */ #include #include +#include +#include "include/sp_utils.h" #include "include/ByTrace.h" - namespace OHOS { namespace SmartPerf { -void ByTrace::thread_get_trace() +void ByTrace::SetTraceConfig(int mSum, int mInterval, long long mThreshold) const { - std::stringstream sstream; - sstream << "bytrace --trace_begin --overwrite"; - std::string cmd_trace = sstream.str(); - GPUtils::readFile(cmd_trace); + sum = mSum; + interval = mInterval; + threshold = mThreshold; } -void ByTrace::thread_finish_trace(std::string &pathName) + +void ByTrace::ThreadGetTrace() const { - std::stringstream sstream; - sstream << "bytrace --trace_finish"; - const std::string &cmd_trace_finish = sstream.str(); - GPUtils::readFile(cmd_trace_finish); - sstream.str(""); - sstream << "bytrace --overwrite sched ace app disk ohos graphic sync workq ability > /data/mynewtrace"; - sstream << pathName; - sstream << "s.ftrace"; - const std::string &cmd_trace_overwrite = sstream.str(); - GPUtils::readFile(cmd_trace_overwrite); + std::string result; + SPUtils::LoadCmd(std::string("bytrace --trace_begin --overwrite"), result); + std::cout << "TRACE threadGetTrace >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; } - -TraceStatus ByTrace::init_trace(bool isStart) +void ByTrace::ThreadEndTrace() const { - if (isStart) { - return TraceStatus::TRACE_START; - } else { - return TraceStatus::TRACE_NO; - } + std::string result; + SPUtils::LoadCmd(std::string("bytrace --trace_finish --overwrite"), result); + std::cout << "TRACE threadGetTrace >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; +} +void ByTrace::ThreadFinishTrace(const std::string &pathName) const +{ + std::string result; + std::stringstream sstream; + sstream.str(""); + sstream << "bytrace --overwrite sched ace app disk ohos graphic sync workq ability"; + sstream << " > /data/app/el2/100/base/com.ohos.gameperceptio/haps/entry/files/sptrace_"; + sstream << pathName << ".ftrace"; + const std::string &cmdTraceOverwrite = sstream.str(); + SPUtils::LoadCmd(cmdTraceOverwrite, result); + std::cout << "TRACE threadFinishTrace >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; } -TraceStatus ByTrace::check_fps_jitters(std::vector jitters, int curProfilerNum) +TraceStatus ByTrace::CheckFpsJitters(std::vector jitters) { - if (curNum <= sum) { + long long curTime = SPUtils::GetCurTime(); + if (curNum <= sum && curTriggerFlag < 0) { for (size_t i = 0; i < jitters.size(); i++) { long long normalJitter = jitters[i] / 1e6; if (normalJitter > threshold) { - if ((flagProfilerNum != -1) && curProfilerNum < (flagProfilerNum + interval)) { - // 如果不是第一次抓取 并且 小于抓取周期间隔 则放弃抓取 - return TraceStatus::TRACE_NO; - } - curNum++; - flagProfilerNum = curProfilerNum; - std::cout << "***************************************************************************" << std::endl; - std::cout << "***************************************************************************" << std::endl; - std::cout << "************* ByTrace::getTrace:curJitter:" << normalJitter << "*******************" << - std::endl; - std::cout << "***************************************************************************" << std::endl; - std::cout << "***************************************************************************" << std::endl; - return TraceStatus::TRACE_FINISH; + TriggerCatch(curTime); } } } - return TraceStatus::TRACE_NO; + std::cout << "TRACE CHECK RUNING >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; + std::cout << "TRACE CHECK lastTriggerTime:" << lastTriggerTime << " curTime:" << curTime << " curTriggerFlag:" << + curTriggerFlag << std::endl; + if ((curTime - lastTriggerTime) > cost && curTriggerFlag > 0) { + std::string result; + SPUtils::LoadCmd(std::string("bytrace --trace_finish"), result); + std::string pathName = std::to_string(curTime); + std::thread tFinish(&ByTrace::ThreadFinishTrace, this, std::ref(pathName)); + curTriggerFlag = -1; + std::cout << "TRACE FINISH >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; + tFinish.detach(); + } + return TraceStatus::TRACE_FINISH; +} +void ByTrace::TriggerCatch(long long curTime) const +{ + if ((curTime - lastTriggerTime) > interval) { + std::thread tStart(&ByTrace::ThreadGetTrace, this); + curTriggerFlag = 1; + lastTriggerTime = curTime; + curNum++; + std::cout << "TRACE START >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; + tStart.detach(); + } } } } diff --git a/device/device_command/CPU.cpp b/device/device_command/CPU.cpp index 0f8d556..6662988 100644 --- a/device/device_command/CPU.cpp +++ b/device/device_command/CPU.cpp @@ -13,72 +13,70 @@ * limitations under the License. */ -#include -#include -#include +#include +#include +#include +#include #include "securec.h" +#include "include/sp_utils.h" #include "include/CPU.h" namespace OHOS { namespace SmartPerf { -int CPU::get_cpu_num() +std::map CPU::ItemData() { - char cpu_node[128]; - int cpu_num = 0; + std::map result; + if (mCpuNum < 0) { + GetCpuNum(); + } + std::vector workloads = GetCpuLoad(); + std::string cpuLoadsStr; + std::string cpuFreqStr; + for (size_t i = 0; i < workloads.size(); i++) { + cpuLoadsStr = std::to_string(workloads[i]); + cpuFreqStr = std::to_string(GetCpuFreq(i)); + result["cpu" + std::to_string(i) + "Load"] = cpuLoadsStr; + result["cpu" + std::to_string(i) + "Frequency"] = cpuFreqStr; + } + return result; +} +int CPU::GetCpuNum() +{ + int cpuNum = 0; while (true) { - if (snprintf_s(cpu_node, sizeof(cpu_node), sizeof(cpu_node), "%s/cpu%d", CPU_BASE_PATH.c_str(), cpu_num) > 0) { - if (access(cpu_node, F_OK) == -1) { - break; - } + std::stringstream cpuNodeStr; + cpuNodeStr << cpuBasePath.c_str() << "/cpu" << cpuNum; + if (!SPUtils::FileAccess(cpuNodeStr.str())) { + break; } - ++cpu_num; + ++cpuNum; } - return m_cpu_num = cpu_num; + return mCpuNum = cpuNum; } -int CPU::get_cpu_freq(int cpu_id) +int CPU::GetCpuFreq(int cpuId) { - char buffer[128]; - if (access(CPU_SCALING_CUR_FREQ(cpu_id).c_str(), F_OK) == -1) { - return -1; - } - FILE *fp = fopen(CPU_SCALING_CUR_FREQ(cpu_id).c_str(), "r"); - if (fp == nullptr) { - return -1; - } - buffer[0] = '\0'; - while (fgets(buffer, sizeof(buffer), fp) == nullptr) { - std::cout << "fgets fail"; - } - if (fclose(fp) == EOF) { - return EOF; - } - return atoi(buffer); + std::string curFreq = "-1"; + SPUtils::LoadFile(CpuScalingCurFreq(cpuId), curFreq); + return atoi(curFreq.c_str()); } -std::vector CPU::get_cpu_load() +std::vector CPU::GetCpuLoad() { - if (m_cpu_num <= 0) { + if (mCpuNum <= 0) { std::vector workload; return workload; } std::vector workload; - static char pre_buffer[10][256] = { - "\0", - "\0", - "\0", - "\0", - "\0", - "\0", - "\0", - "\0", - "\0", - "\0", - }; - if (access(PROC_STAT.c_str(), F_OK) == -1) { + static char preBuffer[10][256] = {"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0"}; + if (!SPUtils::FileAccess(procStat)) { return workload; } - FILE *fp = fopen(PROC_STAT.c_str(), "r"); + char realPath[PATH_MAX] = {0x00}; + if (realpath(procStat.c_str(), realPath) == nullptr) { + std::cout << "" << std::endl; + } + FILE *fp = fopen(realPath, "r"); if (fp == nullptr) { - for (int i = 0; i <= m_cpu_num; ++i) { + for (int i = 0; i <= mCpuNum; ++i) { workload.push_back(-1.0f); } return workload; @@ -91,16 +89,17 @@ std::vector CPU::get_cpu_load() const int firstPos = 1; const int secondPos = 2; const int length = 3; - if (strlen(buffer) >= length && buffer[zeroPos] == 'c' && buffer[firstPos] == 'p' && buffer[secondPos] == 'u') { - float b = cac_workload(buffer, pre_buffer[line]); + if (strlen(buffer) >= length && buffer[zeroPos] == 'c' && buffer[firstPos] == 'p' && buffer[secondPos] == 'u' && + line != 0) { + float b = CacWorkload(buffer, preBuffer[line]); workload.push_back(b); - if (snprintf_s(pre_buffer[line], sizeof(pre_buffer[line]), sizeof(pre_buffer[line]), "%s", buffer) < 0) { - std::cout << "snprintf_s fail"; + if (snprintf_s(preBuffer[line], sizeof(preBuffer[line]), sizeof(preBuffer[line]), "%s", buffer) < 0) { + std::cout << "snprintf_s turn fail" << std::endl; } } ++line; - if (line >= m_cpu_num + 1) { + if (line >= mCpuNum + 1) { break; } } @@ -111,64 +110,60 @@ std::vector CPU::get_cpu_load() return workload; } -float CPU::cac_workload(const char *buffer, const char *pre_buffer) +float CPU::CacWorkload(const char *buffer, const char *preBuffer) const { - const size_t default_index = 4; - const size_t default_shift = 10; - const char default_start = '0'; - const char default_end = '9'; + const size_t defaultIndex = 4; + const size_t defaultShift = 10; + const char defaultStart = '0'; + const char defaultEnd = '9'; - size_t pre_len = strlen(pre_buffer); + size_t preLen = strlen(preBuffer); size_t len = strlen(buffer); - if (pre_len == 0 || len == 0) { + if (preLen == 0 || len == 0) { return -1.0f; } - size_t time[10]; - size_t pre_time[10]; + size_t time[10] = {0}; + size_t preTime[10] = {0}; size_t cnt = 0; - for (size_t i = default_index; i < len; ++i) { + for (size_t i = defaultIndex; i < len; ++i) { size_t tmp = 0; - if (buffer[i] < default_start || buffer[i] > default_end) { + if (buffer[i] < defaultStart || buffer[i] > defaultEnd) { continue; } - while (buffer[i] >= default_start && buffer[i] <= default_end) { - tmp = tmp * default_shift + (buffer[i] - default_start); + while (buffer[i] >= defaultStart && buffer[i] <= defaultEnd) { + tmp = tmp * defaultShift + (buffer[i] - defaultStart); i++; } time[cnt++] = tmp; } - size_t pre_cnt = 0; - for (size_t i = default_index; i < pre_len; ++i) { + size_t preCnt = 0; + for (size_t i = defaultIndex; i < preLen; ++i) { size_t tmp = 0; - if (pre_buffer[i] < default_start || pre_buffer[i] > default_end) { + if (preBuffer[i] < defaultStart || preBuffer[i] > defaultEnd) { continue; } - while (pre_buffer[i] >= default_start && pre_buffer[i] <= default_end) { - tmp = tmp * default_shift + (pre_buffer[i] - default_start); + while (preBuffer[i] >= defaultStart && preBuffer[i] <= defaultEnd) { + tmp = tmp * defaultShift + (preBuffer[i] - defaultStart); i++; } - pre_time[pre_cnt++] = tmp; + preTime[preCnt++] = tmp; } - size_t user = time[0] + time[1] - pre_time[0] - pre_time[1]; - size_t sys = time[2] - pre_time[2]; - size_t idle = time[3] - pre_time[3]; - size_t iowait = time[4] - pre_time[4]; - size_t irq = time[5] + time[6] - pre_time[5] - pre_time[6]; + size_t user = time[0] + time[1] - preTime[0] - preTime[1]; + size_t sys = time[2] - preTime[2]; + size_t idle = time[3] - preTime[3]; + size_t iowait = time[4] - preTime[4]; + size_t irq = time[5] + time[6] - preTime[5] - preTime[6]; size_t total = user + sys + idle + iowait + irq; - if (user < 0 || sys < 0 || idle < 0 || iowait < 0 || irq < 0) { - return 0.0f; - } - - double per_user = std::atof(std::to_string(user * 100.0 / total).c_str()); - double per_sys = std::atof(std::to_string(sys * 100.0 / total).c_str()); - double per_iowait = std::atof(std::to_string(iowait * 100.0 / total).c_str()); - double per_irq = std::atof(std::to_string(irq * 100.0 / total).c_str()); + double perUser = std::atof(std::to_string(user * 100.0 / total).c_str()); + double perSys = std::atof(std::to_string(sys * 100.0 / total).c_str()); + double periowait = std::atof(std::to_string(iowait * 100.0 / total).c_str()); + double perirq = std::atof(std::to_string(irq * 100.0 / total).c_str()); - double workload = per_user + per_sys + per_iowait + per_irq; + double workload = perUser + perSys + periowait + perirq; return static_cast(workload); } diff --git a/device/device_command/Capture.cpp b/device/device_command/Capture.cpp new file mode 100644 index 0000000..58db3d2 --- /dev/null +++ b/device/device_command/Capture.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include "include/sp_utils.h" +#include "include/Capture.h" +namespace OHOS { +namespace SmartPerf { +void Capture::ThreadGetCatch(const std::string curTime) const +{ + std::string result; + std::string cmdCapture = "snapshot_display -f /data/local/tmp/capture/" + curTime +".png"; + SPUtils::LoadCmd(cmdCapture, result); + std::cout << "Screen Capture Thread >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl; +} +void Capture::TriggerGetCatch(long long curTime) const +{ + std::string curTimeStr = std::to_string(curTime); + std::thread tStart(&Capture::ThreadGetCatch, this, curTimeStr); + tStart.detach(); +} +} +} diff --git a/device/device_command/DDR.cpp b/device/device_command/DDR.cpp index 80997f9..7bbcf40 100644 --- a/device/device_command/DDR.cpp +++ b/device/device_command/DDR.cpp @@ -12,14 +12,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "include/sp_utils.h" #include "include/DDR.h" namespace OHOS { namespace SmartPerf { -long long DDR::get_ddr_freq() +std::map DDR::ItemData() { - long long curFreq; - std::string ddr_freq = GPUtils::freadFile(std::string(ddr_cur_freq_path.c_str())); - curFreq = std::atoll(ddr_freq.c_str()); + std::map result; + result["ddrFrequency"] = std::to_string(GetDdrFreq()); + return result; +} +long long DDR::GetDdrFreq() +{ + long long curFreq = -1; + std::string ddrfreq; + SPUtils::LoadFile(ddrCurFreqPath, ddrfreq); + curFreq = std::atoll(ddrfreq.c_str()); return curFreq; } } diff --git a/device/device_command/FPS.cpp b/device/device_command/FPS.cpp index 93f1236..1108667 100644 --- a/device/device_command/FPS.cpp +++ b/device/device_command/FPS.cpp @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,175 +16,185 @@ #include #include #include -#include "securec.h" -#include "include/gp_utils.h" +#include +#include "include/sp_utils.h" +#include "include/ByTrace.h" +#include "include/Capture.h" #include "include/FPS.h" namespace OHOS { - namespace SmartPerf { - void FPS::setPackageName(std::string pkgName) - { - pkg_name = std::move(pkgName); - std::vector sps; - GPUtils::mSplit(pkg_name, ".", sps); - if (sps.size() > 0) { - cur_layer_name = std::string(""); - std::string cur_layer = sps[2]; - char cmd[100]; - if (snprintf_s(cmd, sizeof(cmd), sizeof(cmd), - "hidumper -s 10 |grep surface |grep %s", cur_layer.c_str()) < 0) { - std::cout << "snprintf fail"; - } - std::string layer_line = GPUtils::readFile(cmd); - int flag = 0; - for (size_t i = 0; i < layer_line.size(); i++) { - if (layer_line[i] == ']') { - flag = 0; - break; - } - if (flag) { - cur_layer_name += layer_line[i]; - } - if (layer_line[i] == '[') { - flag = 1; - } - } - } +namespace SmartPerf { +std::map FPS::ItemData() +{ + std::map result; + FpsInfo fpsInfo = GetFpsInfo(); + result["fps"] = std::to_string(fpsInfo.fps); + std::string jitterStr = ""; + std::string split = ""; + for (size_t i = 0; i < fpsInfo.jitters.size(); i++) { + if (i > 0) { + split = ";;"; } - FpsInfo FPS::getFpsInfo(int is_video, int is_camera) - { - FpsInfo fpsInfoMax; - fpsInfoMax.fps = -1; + jitterStr += split + std::to_string(fpsInfo.jitters[i]); + } + result["fpsJitters"] = jitterStr; + if (isCatchTrace > 0) { + ByTrace::GetInstance().CheckFpsJitters(fpsInfo.jitters); + } + if (isCapture > 0) { + Capture::GetInstance().TriggerGetCatch(SPUtils::GetCurTime()); + } + return result; +} +void FPS::SetTraceCatch() +{ + isCatchTrace = 1; +} +void FPS::SetCaptureOn() +{ + isCapture = 1; +} +void FPS::SetPackageName(std::string pName) +{ + pkgName = std::move(pName); +} +FpsInfo FPS::GetFpsInfo() +{ + FpsInfo fpsInfoMax; + fpsInfoMax.fps = -1; - std::string layerName; - if (is_video) { - layerName = std::string("RosenRenderTexture"); - } else if (is_camera) { - layerName = std::string("RosenRenderXComponent"); - } else { - std::vector sps; - GPUtils::mSplit(this->pkg_name, ".", sps); - std::string addEndChar = "0"; - const int pNameLastPos = 2; - layerName = std::string(sps[pNameLastPos].c_str() + addEndChar); - } - FpsInfo fpsInfo = GetSurfaceFrame(layerName); - if (fpsInfo.fps > fpsInfoMax.fps) { - fpsInfoMax = fpsInfo; - } - return fpsInfoMax; - } - FpsInfo FPS::GetSurfaceFrame(std::string name) - { - if (name == "") { - return FpsInfo(); - } - static std::map fps_map; - if (fps_map.count(name) == 0) { - FpsInfo tmp; - tmp.fps = 0; - tmp.pre_fps = 0; - fps_map[name] = tmp; - } - FpsInfo &fpsInfo = fps_map[name]; - fpsInfo.fps = 0; - FILE *fp; - static char tmp[1024]; - std::string cmd = "hidumper -s 10 -a \"fps " + name + "\""; - fp = popen(cmd.c_str(), "r"); - if (fp == nullptr) { - return fpsInfo; - } - long long MOD = 1e9; - long long lastReadyTime = -1; - int fps_gb = 0; - if (!(fpsInfo.time_stamp_q).empty()) { - lastReadyTime = (fpsInfo.time_stamp_q).back(); - } - bool jump = false; - bool refresh = false; + if (pkgName.empty()) { + return fpsInfoMax; + } - int cnt = 0; - int zeroNum = 0; - while (fgets(tmp, sizeof(tmp), fp) != nullptr) { - long long frameReadyTime = 0; - std::stringstream sstream; - sstream << tmp; - sstream >> frameReadyTime; - cnt++; - if (frameReadyTime == 0) { - zeroNum++; - continue; - } - if (lastReadyTime >= frameReadyTime) { - lastReadyTime = -1; - continue; - } - refresh = true; - long long t_frameReadyTime = frameReadyTime / MOD; - long long t_lastReadyTime = lastReadyTime / MOD; - long long lastFrame = -1; - if (t_frameReadyTime == t_lastReadyTime) { - (fpsInfo.time_stamp_q).push(frameReadyTime); - } else if (t_frameReadyTime == t_lastReadyTime + 1) { - jump = true; - lastFrame = fpsInfo.last_frame_ready_time; - lastReadyTime = frameReadyTime; - int fps_tmp = 0; - fpsInfo.jitters.clear(); - while (!(fpsInfo.time_stamp_q).empty()) { - fps_tmp++; - long long currFrame = (fpsInfo.time_stamp_q.front()); - if (lastFrame != -1) { - long long jitter = currFrame - lastFrame; - fpsInfo.jitters.push_back(jitter); - } - lastFrame = currFrame; - (fpsInfo.time_stamp_q).pop(); - } + std::string layerName; + std::vector sps; + SPUtils::StrSplit(this->pkgName, ".", sps); + std::string addEndChar = "0"; + const int pNameLastPos = 2; + std::string pkgSuffix = sps[pNameLastPos]; + layerName = std::string(pkgSuffix.c_str() + addEndChar); + if (pkgSuffix.find("camera") != std::string::npos) { + layerName = std::string("RosenRenderXComponent"); + } + + FpsInfo fpsInfo = GetSurfaceFrame(layerName); + if (fpsInfo.fps > fpsInfoMax.fps) { + fpsInfoMax = fpsInfo; + } + return fpsInfoMax; +} +FpsInfo FPS::GetSurfaceFrame(std::string name) +{ + if (name == "") { + return FpsInfo(); + } + static std::map fpsMap; + if (fpsMap.count(name) == 0) { + FpsInfo tmp; + tmp.fps = 0; + tmp.preFps = 0; + fpsMap[name] = tmp; + } + FpsInfo &fpsInfo = fpsMap[name]; + fpsInfo.fps = 0; + FILE *fp; + static char tmp[1024]; + std::string cmd = "hidumper -s 10 -a \"fps " + name + "\""; + fp = popen(cmd.c_str(), "r"); + if (fp == nullptr) { + return fpsInfo; + } + long long mod = 1e9; + long long lastReadyTime = -1; + int fpsGb = 0; + if (!(fpsInfo.timeStampQ).empty()) { + lastReadyTime = (fpsInfo.timeStampQ).back(); + } + bool jump = false; + bool refresh = false; - fps_gb = fps_tmp; + int cnt = 0; + int zeroNum = 0; + while (fgets(tmp, sizeof(tmp), fp) != nullptr) { + long long frameReadyTime = 0; + std::stringstream sstream; + sstream << tmp; + sstream >> frameReadyTime; + cnt++; + if (frameReadyTime == 0) { + zeroNum++; + continue; + } + if (lastReadyTime >= frameReadyTime) { + lastReadyTime = -1; + continue; + } + refresh = true; + long long tFrameReadyTime = frameReadyTime / mod; + long long tLastReadyTime = lastReadyTime / mod; + long long lastFrame = -1; + if (tFrameReadyTime == tLastReadyTime) { + (fpsInfo.timeStampQ).push(frameReadyTime); + } else if (tFrameReadyTime == tLastReadyTime + 1) { + jump = true; + lastFrame = fpsInfo.lastFrameReadyTime; + lastReadyTime = frameReadyTime; + int fpsTmp = 0; + fpsInfo.jitters.clear(); + while (!(fpsInfo.timeStampQ).empty()) { + fpsTmp++; + long long currFrame = (fpsInfo.timeStampQ.front()); + if (lastFrame != -1) { + long long jitter = currFrame - lastFrame; + fpsInfo.jitters.push_back(jitter); + } + lastFrame = currFrame; + (fpsInfo.timeStampQ).pop(); + } - (fpsInfo.time_stamp_q).push(frameReadyTime); + fpsGb = fpsTmp; - fpsInfo.last_frame_ready_time = lastFrame; - } else if (t_frameReadyTime > t_lastReadyTime + 1) { - jump = true; - lastReadyTime = frameReadyTime; + (fpsInfo.timeStampQ).push(frameReadyTime); - while (!(fpsInfo.time_stamp_q).empty()) { - (fpsInfo.time_stamp_q).pop(); - } + fpsInfo.lastFrameReadyTime = lastFrame; + } else if (tFrameReadyTime > tLastReadyTime + 1) { + jump = true; + lastReadyTime = frameReadyTime; - (fpsInfo.time_stamp_q).push(frameReadyTime); - } + while (!(fpsInfo.timeStampQ).empty()) { + (fpsInfo.timeStampQ).pop(); } - pclose(fp); - const int maxZeroNum = 120; - if (zeroNum >= maxZeroNum) { - while (!(fpsInfo.time_stamp_q.empty())) { - fpsInfo.time_stamp_q.pop(); - } - fpsInfo.fps = 0; - return fpsInfo; - } - const int minPrintLine = 5; - if (cnt < minPrintLine) { - fpsInfo.fps = fpsInfo.pre_fps; - return fpsInfo; - } + (fpsInfo.timeStampQ).push(frameReadyTime); + } + } - if (fps_gb > 0) { - fpsInfo.fps = fps_gb; - fpsInfo.pre_fps = fps_gb; - return fpsInfo; - } else if (refresh && !jump) { - fpsInfo.fps = fpsInfo.pre_fps; - return fpsInfo; - } else { - fpsInfo.fps = 0; - return fpsInfo; - } + pclose(fp); + const int maxZeroNum = 120; + if (zeroNum >= maxZeroNum) { + while (!(fpsInfo.timeStampQ.empty())) { + fpsInfo.timeStampQ.pop(); } + fpsInfo.fps = 0; + return fpsInfo; } + const int minPrintLine = 5; + if (cnt < minPrintLine) { + fpsInfo.fps = fpsInfo.preFps; + return fpsInfo; + } + + if (fpsGb > 0) { + fpsInfo.fps = fpsGb; + fpsInfo.preFps = fpsGb; + return fpsInfo; + } else if (refresh && !jump) { + fpsInfo.fps = fpsInfo.preFps; + return fpsInfo; + } else { + fpsInfo.fps = 0; + return fpsInfo; + } +} +} } diff --git a/device/device_command/GPU.cpp b/device/device_command/GPU.cpp index 4c13a7b..02d8996 100644 --- a/device/device_command/GPU.cpp +++ b/device/device_command/GPU.cpp @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -12,66 +12,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #include -#include -#include "include/GPU.h" - +#include "include/sp_utils.h" +#include "include/GPU.h" namespace OHOS { - namespace SmartPerf { - void GPU::init_gpu_node() - { - for (int i = 0; i < sizeof(GPU_CUR_FREQ_PATH) / sizeof(const char *); ++i) { - if (GPUtils::canOpen(GPU_CUR_FREQ_PATH[i])) { - gpu_cur_freq_path = std::string(GPU_CUR_FREQ_PATH[i]); - } - } +namespace SmartPerf { +std::map GPU::ItemData() +{ + std::map result; + int freq = GetGpuFreq(); + float load = GetGpuLoad(); + result["gpuFrequency"] = std::to_string(freq); + result["gpuLoad"] = std::to_string(load); + return result; +} - for (int i = 0; i < sizeof(GPU_CUR_WORKLOAD_PATH) / sizeof(const char *); ++i) { - if (GPUtils::canOpen(GPU_CUR_WORKLOAD_PATH[i])) { - gpu_cur_load_path = std::string(GPU_CUR_WORKLOAD_PATH[i]); - } - } - } - int GPU::get_gpu_freq() - { - std::string gpu_freq = GPUtils::readFile(std::string(gpu_cur_freq_path.c_str())); - return atoi(gpu_freq.c_str()); - } - float GPU::calc_workload(const char *buffer) const - { - std::vector sps; - std::string buffer_line = buffer; - GPUtils::mSplit(buffer, "@", sps); - if (sps.size() > 0) { - // rk3568 - float loadRk = std::stof(sps[0]); - return loadRk; - } else { - // wgr - float loadWgr = std::stof(buffer); - return loadWgr; - } - return -1.0; +int GPU::GetGpuFreq() +{ + std::string gpuFreq; + for (auto path : gpuCurFreqPaths) { + if (SPUtils::FileAccess(path)) { + SPUtils::LoadFile(path, gpuFreq); } - float GPU::get_gpu_load() - { - static char buffer[128]; - if (access(gpu_cur_load_path.c_str(), F_OK) == -1) { - return -1.0; - } - FILE *fp = fopen(gpu_cur_load_path.c_str(), "r"); - if (fp == nullptr) { - return EOF; - } - buffer[0] = '\0'; - while (fgets(buffer, sizeof(buffer), fp) == nullptr) { - std::cout << "fgets fail"; - } - if (fclose(fp) == EOF) { - return -1.0; - } - return calc_workload(buffer); + } + return atoi(gpuFreq.c_str()); +} +float GPU::GetGpuLoad() +{ + std::vector sps; + std::string bufferLine; + for (auto path : gpuCurLoadPaths) { + if (SPUtils::FileAccess(path)) { + SPUtils::LoadFile(path, bufferLine); } } + SPUtils::StrSplit(bufferLine, "@", sps); + if (sps.size() > 0) { + // rk3568 + float loadRk = std::stof(sps[0]); + return loadRk; + } else { + // wgr + float loadWgr = std::stof(bufferLine); + return loadWgr; + } + return -1.0; +} +} } diff --git a/device/device_command/Power.cpp b/device/device_command/Power.cpp index 6c10b6c..7bfb02e 100644 --- a/device/device_command/Power.cpp +++ b/device/device_command/Power.cpp @@ -13,98 +13,20 @@ * limitations under the License. */ #include -#include -#include -#include "securec.h" -#include "include/gp_utils.h" +#include "include/sp_utils.h" #include "include/Power.h" - namespace OHOS { namespace SmartPerf { -void Power::init_power() +std::map Power::ItemData() { - for (int i = 0; i < sizeof(power_path) / sizeof(const char *); ++i) { - if (GPUtils::canOpen(std::string(power_path[i]))) { - power_base_path = std::string(power_path[i]); - } - } - - char powerNode[256]; - for (int j = 0; j < sizeof(default_collect_power_info) / sizeof(const char *); ++j) { - if (snprintf_s(powerNode, sizeof(powerNode), sizeof(powerNode), "%s/%s", power_base_path.c_str(), - default_collect_power_info[j]) < 0) { - std::cout << "snprintf fail"; - } - // file exists - std::string type = std::string(default_collect_power_info[j]); - if (power_node_path_map.count(type) > 0) { - continue; - } - power_node_path_map[type] = std::string(powerNode); - } -} - -std::map Power::getPowerMap() -{ - std::map power_map; - FILE *fp = nullptr; - char buffer[256]; - std::map::iterator iter; - int charging = 1; - for (iter = power_node_path_map.begin(); iter != power_node_path_map.end(); ++iter) { - std::string type = iter->first; - std::string powerNode = power_node_path_map[type]; - if (access(powerNode.c_str(), F_OK) == -1) { - continue; - } - fp = fopen(powerNode.c_str(), "r"); - if (fp == nullptr) { - power_map[type] = "-1.0"; - continue; - } - buffer[0] = '\0'; - while (fgets(buffer, sizeof(buffer), fp) == nullptr) { - std::cout << "fgets fail"; - } - if (fclose(fp) == EOF) { - std::cout << "fclose fail"; - } - std::string power_value = std::string(buffer); - if (iter->first == "status") { - if (power_value.find("Charging") == std::string::npos && power_value.find("Full") == std::string::npos) { - charging = 0; - } - if (power_value.find("Discharging") != std::string::npos) { - charging = 0; - } - } else if (iter->first == "enable_hiz") { - if (strcmp(buffer, "1") == 0) { - charging = 0; - } - if (std::stoi(power_value) == 1) { - charging = 0; - } - } else if (iter->first == "current_now") { - // 若current now 大于 100000 单位归一化为 1000,大于10000 单位归一化为100,大于3000 单位归一化为10 - double tmp = fabs(std::stof(power_value)); - power_value = std::to_string(tmp); - } else if (iter->first == "voltage_now") { - // 若voltage now 大于100 单位归一化为100 - double tmp = std::stof(power_value); - power_value = std::to_string(tmp); - } - power_map[type] = power_value; - } - if (power_map.count("voltage_now") == 0 && power_map.count("bat_id_0") > 0 && power_map.count("bat_id_1") > 0) { - power_map["voltage_now"] = std::to_string(std::stof(power_map["bat_id_0"]) + std::stof(power_map["bat_id_1"])); - } - if (power_map.count("current_now") > 0 && charging) { - power_map["current_now"] = "-" + power_map["current_now"]; - } - if (power_map.count("status") > 0 && charging == 0) { - power_map["status"] = "Discharging"; - } - return power_map; + std::map result; + std::string currentNow; + SPUtils::LoadFile(currentNowPath, currentNow); + std::string voltageNow; + SPUtils::LoadFile(voltageNowPath, voltageNow); + result["currentNow"] = currentNow; + result["voltageNow"] = voltageNow; + return result; } } } \ No newline at end of file diff --git a/device/device_command/RAM.cpp b/device/device_command/RAM.cpp index 653182a..e9d9b91 100644 --- a/device/device_command/RAM.cpp +++ b/device/device_command/RAM.cpp @@ -12,69 +12,57 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include -#include -#include "securec.h" -#include "include/gp_utils.h" +#include +#include +#include +#include "include/sp_utils.h" #include "include/RAM.h" - namespace OHOS { namespace SmartPerf { -void RAM::setPkgName(std::string ss) +std::map RAM::ItemData() +{ + std::map result; + std::map ramInfo = RAM::GetRamInfo(); + result = ramInfo; + return result; +} +void RAM::SetProcessId(std::string pid) { - pkgName = std::move(ss); + processId = std::move(pid); } -std::map RAM::getRamInfo(std::string pkg_name, int pid) +std::map RAM::GetRamInfo() const { std::map ramInfo; + std::string pssValue = ""; ramInfo["pss"] = "-1"; - std::string pid_value = ""; - if (pid > 0) { - pid_value = std::to_string(pid); - } else { - char pidStr[100]; - if (snprintf_s(pidStr, sizeof(pidStr), sizeof(pidStr), - "ps -ef |grep -w %s", pkg_name.c_str()) < 0) { - std::cout << "snprintf_s fail"; + if (processId.size() > 0) { + const int zero = 0; + const int one = 1; + const int two = 2; + std::ostringstream cmdGrep; + cmdGrep.str(""); + cmdGrep << "/proc/" << processId << "/smaps_rollup"; + std::string cmdRam = cmdGrep.str(); + char realPath[PATH_MAX] = {0x00}; + if (realpath(cmdRam.c_str(), realPath) == nullptr) { + std::cout << "" << std::endl; } - std::string pidLine = GPUtils::readFile(pidStr); - std::vector sps; - GPUtils::mSplit(pidLine, " ", sps); - if (sps.size() > 0) { - pid_value = sps[1]; - } - } - - std::string tmp = "-1"; - if (atoi(pid_value.c_str()) > 0) { - char ram[50]; - if (snprintf_s(ram, sizeof(ram), sizeof(ram), "/proc/%s/smaps_rollup", pid_value.c_str()) < 0) { - std::cout << "snprintf fail"; - } - std::string path = ram; - FILE *fp; - if (access(path.c_str(), F_OK) == -1) { + std::ifstream infile(realPath, std::ios::in); + if (!infile) { return ramInfo; } - if ((fp = fopen(path.c_str(), "r")) != nullptr) { - char s[1024]; - s[0] = '\0'; - while (fgets(s, sizeof(s), fp) != nullptr) { - const int zeroPos = 0; - const int firstPos = 1; - const int secondPos = 2; - const int thirdPos = 3; - if (s[zeroPos] == 'P' && s[firstPos] == 's' && s[secondPos] == 's' && s[thirdPos] == ':') { - tmp += std::string(s); - } + std::string textline; + while (getline(infile, textline, '\n')) { + if (textline[zero] == 'P' && textline[one] == 's' && textline[two] == 's') { + pssValue = textline; + break; } } - if (fp != nullptr) { - pclose(fp); - } } - ramInfo["pss"] = GPUtils::getNumber(tmp); + if (pssValue.size() > 0) { + ramInfo["pss"] = SPUtils::ExtractNumber(pssValue.c_str()); + } return ramInfo; } } diff --git a/device/device_command/README_zh.md b/device/device_command/README_zh.md index 255edab..3b7cf07 100644 --- a/device/device_command/README_zh.md +++ b/device/device_command/README_zh.md @@ -1,21 +1,60 @@ -## 工具介绍 +# SmartPerf-Daemon(SP_daemon) +## 简介 -> OpenHarmony性能测试工具,通过采集设备性能指标,对采集数据进行实时展示、导出csv。 +- OpenHarmony性能测试工具SmartPerf 命令行版本,可采集CPU、GPU、Temperature、Power、应用RAM、FPS等指标,通过设置采集指标,对采集数据进行实时打印、导出csv。 -## 支持功能 +- 性能较差或无屏幕设备请使用命令行版本,带屏幕的设备且性能较好的设备推荐使用[UI版本](../device_ui/README_zh.md)。 -> 当前版本支持如下功能 +## 代码目录 +``` +/smartperf/device/device_command +├── include # 头文件目录 +├── BUILD.gn # SP_daemon bin打包配置文件 +├── ByTrace.cpp # trace抓取代码文件 +├── Capture.cpp # 截图代码文件 +├── CPU.cpp # CPU采集代码文件 +├── DDR.cpp # DDR采集代码文件 +├── FPS.cpp # FPS采集代码文件 +├── GPU.cpp # GPU采集代码文件 +├── Power.cpp # 功耗采集代码文件 +├── RAM.cpp # 内存采集代码文件 +├── smartperf_command.cpp # 程序执行文件 +├── smartperf_main.cpp # 程序入口文件 +├── sp_profiler_factory.cpp # 采集工厂文件 +├── sp_server_socket.cpp # 与SmartPerf hap通讯代码文件 +├── sp_utils.cpp # 工具类 +├── Temperature.cpp # 温度采集代码文件 +``` + +## 约束条件 + SmartPerf应用在3.2系统版本后开始预制使用。 + +## 功能特性 + +**1. 参数说明** + +| 命令 | 功能 |是否必选| +| :-----| :--------------------- |:-----| +| -N | 设置采集次数。 |是| +| -PKG | 设置包名。 |否| +| -PID | 设置进程pid(对于ram适用)。 |否| +| -OUT | 设置csv输出目录。 |否| +| -c | 是否采集cpu。 |否| +| -g | 是否采集gpu。 |否| +| -f | 是否采集fps。 |否| +| -t | 是否采集温度。 |否| +| -p | 是否采集电流。 |否| +| -r | 是否采集内存(需指定进程pid)。 |否| +| -snapshot | 是否截图。 |否| -- 支持RK3568、Hi3516; -- 支持Shell启动; -- 支持采集整机CPU、GPU、POWER、TEMPERATURE、应用的FPS、RAM; +--- -## 使用方式 ->1、首先检查系统是否默认预制了SP_daemon,如打印如下日志,系统已内置SP_daemon +**2. 使用方式**
+1)目前命令行版本已系统预制,可以进入shell,执行SP_daemon --help查看。 ```bash -C:\>hdc_std shell -SP_daemon --help +C:\Users\test>hdc_std shell +# SP_daemon --help usage: SP_daemon -------------------------------------------------------------------- These are common commands list: @@ -33,9 +72,11 @@ These are common commands list: -------------------------------------------------------------------- Example: SP_daemon -N 20 -PKG ohos.samples.ecg -c -g -t -p -f -------------------------------------------------------------------- + command exec finished! +# ``` ->2、执行示例命令:SP_daemon -N 20 -PKG ohos.samples.ecg -c -g -t -p -f +2)执行示例命令:SP_daemon -N 20 -PKG ohos.samples.ecg -c -g -t -p -f。 ``` ----------------------------------Print START------------------------------------ order:0 cpu0freq=1992000 @@ -72,7 +113,7 @@ order:13 timestamp=1501925597848 order:14 voltage_now=4123456.000000 ----------------------------------Print END-------------------------------------- ``` ->3、执行完毕后会在data/local/tmp生成data.csv文件,每次执行命令覆盖写入 +3)执行完毕后会在data/local/tmp生成data.csv文件,每次执行命令覆盖写入。 ```bash # cat /data/local/tmp/data.csv cpu0freq,cpu0load,cpu1freq,cpu1load,cpu2freq,cpu2load,cpu3freq,cpu3load,current_now,gpu-thermal,gpufreq,gpuload,soc-thermal,timestamp,voltage_now @@ -82,21 +123,9 @@ cpu0freq,cpu0load,cpu1freq,cpu1load,cpu2freq,cpu2load,cpu3freq,cpu3load,current_ ``` --- -## 参数说明 +## 发布版本 -| 命令 | 功能 |是否必选| -| :-----| :--------------------- |:-----| -| -N | 设置采集次数 |是| -| -PKG | 设置包名 |否| -| -PID | 设置进程pid(对于ram适用) |否| -| -c | 是否采集cpu |否| -| -g | 是否采集gpu |否| -| -f | 是否采集fps |否| -| -t | 是否采集温度 |否| -| -p | 是否采集电流 |否| -| -r | 是否采集内存(需指定进程pid) |否| -| -snapshot | 是否截图 |否| - ---- -## 构建方式 ->1、在OpenHarmony系统根目录执行全量编译命令(RK3568为例): ./build.sh --product-name rk3568 --ccache
+**3.2.0.0版本发布内容:预制SP_daemon bin文件,支持以下功能:**
+1. 支持RK3568、Hi3516。
+2. 支持Shell启动。
+3. 支持采集整机CPU、GPU、POWER、TEMPERATURE、应用的FPS、RAM。 \ No newline at end of file diff --git a/device/device_command/Temperature.cpp b/device/device_command/Temperature.cpp index f7149b6..c4b8b16 100644 --- a/device/device_command/Temperature.cpp +++ b/device/device_command/Temperature.cpp @@ -12,97 +12,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include #include -#include -#include "securec.h" +#include "include/sp_utils.h" #include "include/Temperature.h" namespace OHOS { namespace SmartPerf { -void Temperature::init_temperature() +std::map Temperature::ItemData() { - int cnt = sizeof(thermal_path) / sizeof(const char *); - for (int i = 0; i < cnt; ++i) { - if (GPUtils::canOpen(std::string(thermal_path[i]) + "/thermal_zone1/temp")) { - thermal_base_path = std::string(thermal_path[i]); - init_thermal_node(); - } - } -} - -void Temperature::init_thermal_node() -{ - char typeNode[256]; - char tempNode[256]; - char buffer[256]; - FILE *fp = nullptr; - const int zoneTravelNum = 100; - for (int zone = 0; zone < zoneTravelNum; ++zone) { - if (snprintf_s(typeNode, sizeof(typeNode), sizeof(typeNode), "%s/thermal_zone%d/type", - thermal_base_path.c_str(), zone) < 0) { - std::cout << "snprintf_s fail"; - } - if (access(typeNode, F_OK) == -1) { - continue; - } - fp = fopen(typeNode, "r"); - if (fp == nullptr) { - continue; - } - buffer[0] = '\0'; - while (fgets(buffer, sizeof(buffer), fp) == nullptr) { - std::cout << "fgets fail"; - } - if (fclose(fp) == EOF) { - std::cout << "fclose fail"; - } - - if (strlen(buffer) == 0) { - continue; - } - - std::string type = std::string(buffer); - if (collect_nodes.count(type) == 0) { - continue; - } - if (snprintf_s(tempNode, sizeof(tempNode), sizeof(tempNode), "%s/thermal_zone%d/temp", - thermal_base_path.c_str(), zone) < 0) { - std::cout << "snprintf_s fail"; - } - thermal_node_path_map[type] = std::string(tempNode); - } -} + std::map result; + std::vector dirs; + SPUtils::ForDirFiles(thermalBasePath, dirs); + for (auto dir : dirs) { + std::string dirType = dir + "/type"; + std::string dirTemp = dir + "/temp"; -std::map Temperature::getThermalMap() -{ - std::map thermal_map; - - FILE *fp = nullptr; - char buffer[256]; - std::map::iterator iter; - for (iter = thermal_node_path_map.begin(); iter != thermal_node_path_map.end(); ++iter) { - std::string type = iter->first; - std::string tempNode = thermal_node_path_map[type]; - fp = fopen(tempNode.c_str(), "r"); - if (fp == nullptr) { - thermal_map[type] = -1.0f; - continue; - } - buffer[0] = '\0'; - while (fgets(buffer, sizeof(buffer), fp) == nullptr) { - std::cout << "fgets fail"; - } - float temp = std::fabs(atof(buffer)); - if (fclose(fp) == EOF) { - std::cout << "fclose fail"; - } - if (strlen(buffer) == 0) { - thermal_map[type] = -1.0f; - continue; + if (SPUtils::FileAccess(dirType)) { + std::string type; + std::string temp; + SPUtils::LoadFile(dirType, type); + SPUtils::LoadFile(dirTemp, temp); + for (auto node : collectNodes) { + if (type.find(node) != std::string::npos) { + result[type] = temp; + } + } } - thermal_map[type] = temp; } - return thermal_map; + return result; } } } diff --git a/device/device_command/gp_utils.cpp b/device/device_command/gp_utils.cpp deleted file mode 100644 index e2954ee..0000000 --- a/device/device_command/gp_utils.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2021 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include "securec.h" -#include "include/gp_utils.h" - -namespace OHOS { - namespace SmartPerf { - void GPUtils::mSplit(const std::string &content, const std::string &sp, std::vector &out) - { - size_t index = 0; - while (index != std::string::npos) { - size_t t_end = content.find_first_of(sp, index); - std::string tmp = content.substr(index, t_end - index); - if (tmp != "" && tmp != " ") { - out.push_back(tmp); - } - if (t_end == std::string::npos) { - break; - } - index = t_end + 1; - } - } - - bool GPUtils::canOpen(const std::string &path) - { - if (access(path.c_str(), F_OK) == -1) { - return false; - } - FILE *fp = fopen(path.c_str(), "r"); - if (fp == nullptr) { - return false; - } - if (fclose(fp) == EOF) { - return false; - } - return true; - } - - // popen - std::string GPUtils::readFile(const std::string &cmd) - { - const int buffLengh = 1024; - std::string res = "NA"; - FILE *fp = popen(cmd.c_str(), "r"); - char line[buffLengh]; - line[0] = '\0'; - while (fgets(line, buffLengh, fp) != nullptr) { - res = std::string(line); - } - - if (pclose(fp) == EOF) { - return ""; - } - return res; - } - - // fopen - std::string GPUtils::freadFile(const std::string &path) - { - std::string res = "NA"; - const int buffLengh = 1024; - if (access(path.c_str(), F_OK) == -1) { - return res; - } - FILE *fp; - if ((fp = fopen(path.c_str(), "r")) != nullptr) { - char s[buffLengh]; - s[0] = '\0'; - while (fgets(s, sizeof(s), fp) != nullptr) { - res += std::string(s); - } - } - if (fp != nullptr) { - fclose(fp); - } - return res; - } - - // get number_str from str - std::string GPUtils::getNumber(const std::string &str) - { - int cntInt = 0; - const int shift = 10; - - for (int i = 0; str[i] != '\0'; ++i) { - if (str[i] >= '0' && str[i] <= '9') { - cntInt *= shift; - cntInt += str[i] - '0'; - } - } - return std::to_string(cntInt); - } - - // wirte to csv by path - void GPUtils::writeCsv(const std::string &path, std::vector &vmap) - { - std::ofstream outFile; - char realPath[PATH_MAX + 1] = {0x00}; - if (strlen(path.c_str()) > PATH_MAX || realpath(path.c_str(), realPath) == NULL) { - return; - } - - outFile.open(path.c_str(), std::ios::out); - int i = 0; - std::string title = ""; - for (GPData gpdata : vmap) { - std::map::iterator iter; - std::string line_content = ""; - for (iter = gpdata.values.begin(); iter != gpdata.values.end(); ++iter) { - if (i == 0) { - title += iter->first + ","; - } - line_content += iter->second + ","; - } - if (i == 0) { - title.pop_back(); - outFile << title << std::endl; - } - line_content.pop_back(); - outFile << line_content << std::endl; - ++i; - } - outFile.close(); - } - } -} diff --git a/device/device_command/include/ByTrace.h b/device/device_command/include/ByTrace.h index 1a451f9..8e727d4 100644 --- a/device/device_command/include/ByTrace.h +++ b/device/device_command/include/ByTrace.h @@ -14,37 +14,48 @@ */ #ifndef BY_TRACE_H #define BY_TRACE_H -#include "gp_utils.h" -#include "singleton.h" +#include "common.h" namespace OHOS { namespace SmartPerf { -enum class TraceStatus { - TRACE_START, - TRACE_FINISH, - TRACE_NO -}; -class ByTrace : public DelayedSingleton { +class ByTrace { public: + static ByTrace &GetInstance() + { + static ByTrace instance; + return instance; + } + // trace配置 + void SetTraceConfig(int mSum, int mInterval, long long mThreshold) const; // 开始抓trace线程 - void thread_get_trace(); + void ThreadGetTrace() const; + // 结束trace线程 + void ThreadEndTrace() const; // 结束抓trace线程 - void thread_finish_trace(std::string &pathName); - // 初始化抓取 - TraceStatus init_trace(bool isStart); + void ThreadFinishTrace(const std::string &pathName) const; // 校验fps-jitters - TraceStatus check_fps_jitters(std::vector jitters, int curProfilerNum); - // 抓trace总次数 默认2次 - int sum = 2; - // 当前触发的次数 - int curNum = 0; + TraceStatus CheckFpsJitters(std::vector jitters); + // 触发trace + void TriggerCatch(long long curTime) const; private: - // 抓trace间隔 默认60s - int interval = 60; - // 当前触发标记次数 - int flagProfilerNum = -1; + ByTrace() {}; + ByTrace(const ByTrace &); + ByTrace &operator = (const ByTrace &); + + // 抓trace总次数 默认2次 + mutable int sum = 2; + // 当前触发的次数 + mutable int curNum = 0; + // 抓trace间隔(两次抓取的间隔时间 默认60*1000 ms) + mutable int interval = 60000; + // 抓trace耗时(start 到 finish的预留时间 默认10*1000 ms) + mutable int cost = 10000; // 抓trace触发条件:默认 某一帧的某个jitter>100 ms触发 - long long threshold = 100; + mutable long long threshold = 100; + // 上一次触发时间 + mutable long long lastTriggerTime = -1; + // 当前是否触发 + mutable long long curTriggerFlag = -1; }; } } diff --git a/device/device_command/include/CPU.h b/device/device_command/include/CPU.h index e6172a3..35bddf9 100644 --- a/device/device_command/include/CPU.h +++ b/device/device_command/include/CPU.h @@ -14,48 +14,38 @@ */ #ifndef CPU_H #define CPU_H -#include -#include -#include -#include "gp_utils.h" -#include "singleton.h" - - +#include +#include +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { -class CPU : public DelayedSingleton { +class CPU : public SpProfiler { public: - int get_cpu_num(); - int get_cpu_freq(int cpu_id); - std::vector get_cpu_load(); + static CPU &GetInstance() + { + static CPU instance; + return instance; + } + std::map ItemData() override; + + int GetCpuNum(); + int GetCpuFreq(int cpuId); + std::vector GetCpuLoad(); private: + CPU() {}; + CPU(const CPU &); + CPU &operator = (const CPU &); - const std::string CPU_BASE_PATH = "/sys/devices/system/cpu"; - const std::string PROC_STAT = "/proc/stat"; - inline const std::string CPU_SCALING_CUR_FREQ(int CPUID) - { - return CPU_BASE_PATH + "/cpu" + std::to_string(CPUID) + "/cpufreq/scaling_cur_freq"; - } - inline const std::string CPU_SCALING_MAX_FREQ(int CPUID) - { - return CPU_BASE_PATH + "/cpu" + std::to_string(CPUID) + "/cpufreq/scaling_max_freq"; - } - inline const std::string CPU_SCALING_MIN_FREQ(int CPUID) - { - return CPU_BASE_PATH + "/cpu" + std::to_string(CPUID) + "/cpufreq/scaling_min_freq"; - } - inline const std::string CPUINFO_MAX_FREQ(int CPUID) - { - return CPU_BASE_PATH + "/cpu" + std::to_string(CPUID) + "/cpufreq/cpuinfo_max_freq"; - } - inline const std::string CPUINFO_MIN_FREQ(int CPUID) + const std::string cpuBasePath = "/sys/devices/system/cpu"; + const std::string procStat = "/proc/stat"; + inline const std::string CpuScalingCurFreq(int cpuId) const { - return CPU_BASE_PATH + "/cpu" + std::to_string(CPUID) + "/cpufreq/cpuinfo_min_freq"; + return cpuBasePath + "/cpu" + std::to_string(cpuId) + "/cpufreq/scaling_cur_freq"; } - int m_cpu_num; - float cac_workload(const char *buffer, const char *pre_buffer); + int mCpuNum = -1; + float CacWorkload(const char *buffer, const char *preBuffer) const; }; } } diff --git a/device/device_command/include/Capture.h b/device/device_command/include/Capture.h new file mode 100644 index 0000000..fbf0e63 --- /dev/null +++ b/device/device_command/include/Capture.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef CAPTURE_H +#define CAPTURE_H +namespace OHOS { +namespace SmartPerf { +class Capture { +public: + static Capture &GetInstance() + { + static Capture instance; + return instance; + } + // 截图线程 + void ThreadGetCatch(const std::string curTime) const; + // 触发线程 + void TriggerGetCatch(long long curTime) const; +private: + Capture() {}; + Capture(const Capture &); + Capture &operator = (const Capture &); +}; +} +} +#endif \ No newline at end of file diff --git a/device/device_command/include/DDR.h b/device/device_command/include/DDR.h index 434b67b..85932df 100644 --- a/device/device_command/include/DDR.h +++ b/device/device_command/include/DDR.h @@ -14,26 +14,24 @@ */ #ifndef DDR_H #define DDR_H - -#include -#include -#include -#include "gp_utils.h" -#include "singleton.h" +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { -class DDR : public DelayedSingleton { +class DDR : public SpProfiler { public: - static constexpr const char *DDR_CUR_FREQ_PATH[] = { - "/sys/class/devfreq/ddrfreq/cur_freq", - }; - static constexpr const char *DDR_AVAILABLE_FREQ_PATH[] = { - "/sys/class/devfreq/ddrfreq/available_frequencies", - }; - long long get_ddr_freq(); + long long GetDdrFreq(); + static DDR &GetInstance() + { + static DDR instance; + return instance; + } + std::map ItemData() override; private: - std::string ddr_cur_freq_path; + DDR() {}; + DDR(const DDR &); + DDR &operator = (const DDR &); + std::string ddrCurFreqPath = "/sys/class/devfreq/ddrfreq/cur_freq"; }; }; } diff --git a/device/device_command/include/FPS.h b/device/device_command/include/FPS.h index 38cdd08..56a4332 100644 --- a/device/device_command/include/FPS.h +++ b/device/device_command/include/FPS.h @@ -14,38 +14,49 @@ */ #ifndef FPS_H #define FPS_H - -#include -#include #include #include -#include "gp_utils.h" -#include "singleton.h" +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { struct FpsInfo { int fps; - int pre_fps; + int preFps; std::vector jitters; - std::queue time_stamp_q; - long long last_frame_ready_time; - long long current_fps_time; + std::queue timeStampQ; + long long lastFrameReadyTime; + long long currentFpsTime; FpsInfo() { fps = 0; - pre_fps = 0; - last_frame_ready_time = 0; - current_fps_time = 0; + preFps = 0; + lastFrameReadyTime = 0; + currentFpsTime = 0; } }; -class FPS : public DelayedSingleton { +class FPS : public SpProfiler { public: - void setPackageName(std::string pkgName); - FpsInfo getFpsInfo(int is_video, int is_camera); - FpsInfo m_fpsInfo; + void SetPackageName(std::string pName); + void SetCaptureOn(); + void SetTraceCatch(); + FpsInfo GetFpsInfo(); + FpsInfo mFpsInfo; + static FPS &GetInstance() + { + static FPS instance; + return instance; + } + std::map ItemData() override; + private: - std::string pkg_name; - std::string cur_layer_name; + FPS() {}; + FPS(const FPS &); + FPS &operator = (const FPS &); + + std::string pkgName; + std::string curLayerName; + int isCatchTrace = 0; + int isCapture = 0; FpsInfo GetSurfaceFrame(std::string name); }; } diff --git a/device/device_command/include/GPU.h b/device/device_command/include/GPU.h index aedcb97..1cc6608 100644 --- a/device/device_command/include/GPU.h +++ b/device/device_command/include/GPU.h @@ -15,31 +15,34 @@ #ifndef GPU_H #define GPU_H -#include -#include -#include -#include "gp_utils.h" -#include "singleton.h" - +#include +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { -class GPU : public DelayedSingleton { +class GPU : public SpProfiler { public: - static constexpr const char *GPU_CUR_FREQ_PATH[] = { + std::map ItemData() override; + static GPU &GetInstance() + { + static GPU instance; + return instance; + } + int GetGpuFreq(); + float GetGpuLoad(); + +private: + GPU() {}; + GPU(const GPU &); + GPU &operator = (const GPU &); + + const std::vector gpuCurFreqPaths = { "/sys/class/devfreq/fde60000.gpu/cur_freq", // rk3568 "/sys/class/devfreq/gpufreq/cur_freq", // wgr }; - static constexpr const char *GPU_CUR_WORKLOAD_PATH[] = { + const std::vector gpuCurLoadPaths = { "/sys/class/devfreq/gpufreq/gpu_scene_aware/utilisation", // wgr "/sys/class/devfreq/fde60000.gpu/load", // rk3568 }; - int get_gpu_freq(); - float get_gpu_load(); - void init_gpu_node(); -private: - std::string gpu_cur_freq_path; - std::string gpu_cur_load_path; - float calc_workload(const char *buffer) const; }; } } diff --git a/device/device_command/include/Power.h b/device/device_command/include/Power.h index 286f4ba..19cdbaa 100644 --- a/device/device_command/include/Power.h +++ b/device/device_command/include/Power.h @@ -14,30 +14,25 @@ */ #ifndef POWER_H #define POWER_H - -#include #include -#include -#include "singleton.h" +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { -class Power : public DelayedSingleton { +class Power : public SpProfiler { public: - static constexpr const char *power_path[] = { - "/sys/class/power_supply/battery", - "/sys/class/power_supply/Battery", - "/data/local/tmp/battery" - }; + std::map ItemData() override; + static Power &GetInstance() + { + static Power instance; + return instance; + } - static constexpr const char *default_collect_power_info[] = { - "current_now", - "voltage_now", - }; - std::map getPowerMap(); - void init_power(); private: - std::string power_base_path; - std::map power_node_path_map; + Power() {}; + Power(const Power &); + Power &operator = (const Power &); + const std::string currentNowPath = "/sys/class/power_supply/Battery/current_now"; + const std::string voltageNowPath = "/sys/class/power_supply/Battery/voltage_now"; }; } } diff --git a/device/device_command/include/RAM.h b/device/device_command/include/RAM.h index f880185..5bc048e 100644 --- a/device/device_command/include/RAM.h +++ b/device/device_command/include/RAM.h @@ -1,32 +1,40 @@ /* -* Copyright (C) 2021 Huawei Device Co., Ltd. -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #ifndef RAM_H #define RAM_H -#include -#include -#include "singleton.h" - +#include "sp_profiler.h" namespace OHOS { - namespace SmartPerf { - class RAM : public DelayedSingleton { - public: - void setPkgName(std::string ss); - std::map getRamInfo(std::string pkg_name, int pid); - private: - std::string pkgName; - }; +namespace SmartPerf { +class RAM : public SpProfiler { +public: + void SetProcessId(std::string pid); + std::map GetRamInfo() const + ; + static RAM &GetInstance() + { + static RAM instance; + return instance; } + std::map ItemData() override; + +private: + RAM() {}; + RAM(const RAM &); + RAM &operator = (const RAM &); + std::string processId = ""; +}; +} } #endif diff --git a/device/device_command/include/Temperature.h b/device/device_command/include/Temperature.h index df5935a..ad8066a 100644 --- a/device/device_command/include/Temperature.h +++ b/device/device_command/include/Temperature.h @@ -15,31 +15,28 @@ #ifndef TEMPERATURE_H #define TEMPERATURE_H -#include #include -#include -#include -#include "gp_utils.h" -#include "singleton.h" +#include "sp_profiler.h" namespace OHOS { namespace SmartPerf { -class Temperature : public DelayedSingleton { +class Temperature : public SpProfiler { public: - static constexpr const char *thermal_path[] = { - "/sys/devices/virtual/thermal", - "/sys/class/thermal" - }; - const std::map collect_nodes = { - { "soc_thermal", "soc_thermal" }, { "system_h", "system_h" }, { "soc-thermal", "soc-thermal" }, - { "gpu-thermal", "gpu-thermal" }, { "shell_frame", "shell_frame" }, { "shell_front", "shell_front" }, - { "shell_back", "shell_back" } - }; - std::map getThermalMap(); - void init_thermal_node(); - void init_temperature(); + std::vector collectNodes = { std::string("soc_thermal"), std::string("system_h"), + std::string("soc-thermal"), std::string("gpu-thermal"), + std::string("shell_frame"), std::string("shell_front"), + std::string("shell_back") }; + std::map ItemData() override; + static Temperature &GetInstance() + { + static Temperature instance; + return instance; + } + private: - std::string thermal_base_path; - std::map thermal_node_path_map; + Temperature() {}; + Temperature(const Temperature &); + Temperature &operator = (const Temperature &); + std::string thermalBasePath = "/sys/class/thermal"; }; } } diff --git a/device/device_command/include/common.h b/device/device_command/include/common.h new file mode 100644 index 0000000..2f0d5d7 --- /dev/null +++ b/device/device_command/include/common.h @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef COMMON_H +#define COMMON_H +#include +#include +namespace OHOS { +namespace SmartPerf { +enum class MessageType { + GET_CPU_NUM, + GET_CPU_FREQ, + GET_CPU_LOAD, + SET_PKG_NAME, + SET_PROCESS_ID, + GET_FPS_AND_JITTERS, + GET_GPU_FREQ, + GET_GPU_LOAD, + GET_DDR_FREQ, + GET_RAM_INFO, + GET_TEMPERATURE, + GET_POWER, + GET_CAPTURE, + CATCH_TRACE_START, + CATCH_TRACE_FINISH, +}; + +const std::unordered_map messageMap = { + { MessageType::GET_CPU_NUM, std::string("get_cpu_num") }, + { MessageType::GET_CPU_FREQ, std::string("get_cpu_freq") }, + { MessageType::GET_CPU_LOAD, std::string("get_cpu_load") }, + { MessageType::SET_PKG_NAME, std::string("set_pkgName") }, + { MessageType::SET_PROCESS_ID, std::string("set_pid") }, + { MessageType::GET_FPS_AND_JITTERS, std::string("get_fps_and_jitters") }, + { MessageType::GET_GPU_FREQ, std::string("get_gpu_freq") }, + { MessageType::GET_GPU_LOAD, std::string("get_gpu_load") }, + { MessageType::GET_DDR_FREQ, std::string("get_ddr_freq") }, + { MessageType::GET_RAM_INFO, std::string("get_ram_info") }, + { MessageType::GET_TEMPERATURE, std::string("get_temperature") }, + { MessageType::GET_POWER, std::string("get_power") }, + { MessageType::GET_CAPTURE, std::string("get_capture") }, + { MessageType::CATCH_TRACE_START, std::string("catch_trace_start") }, + { MessageType::CATCH_TRACE_FINISH, std::string("catch_trace_end") }, +}; + +enum class CommandType { + CT_N, + CT_PKG, + CT_PID, + CT_OUT, + CT_C, + CT_G, + CT_D, + CT_F, + CT_F1, + CT_F2, + CT_T, + CT_P, + CT_R, + CT_TTRACE, + CT_SNAPSHOT, + CT_HW +}; +enum class CommandHelp { + HELP, + VERSION +}; + +const std::unordered_map commandMap = { + { std::string("-N"), CommandType::CT_N }, { std::string("-PKG"), CommandType::CT_PKG }, + { std::string("-PID"), CommandType::CT_PID }, { std::string("-OUT"), CommandType::CT_OUT }, + { std::string("-c"), CommandType::CT_C }, { std::string("-g"), CommandType::CT_G }, + { std::string("-f"), CommandType::CT_F }, { std::string("-f1"), CommandType::CT_F1 }, + { std::string("-f2"), CommandType::CT_F1 }, { std::string("-t"), CommandType::CT_T }, + { std::string("-p"), CommandType::CT_P }, { std::string("-r"), CommandType::CT_R }, + { std::string("-trace"), CommandType::CT_TTRACE }, { std::string("-snapshot"), CommandType::CT_SNAPSHOT }, + { std::string("-hw"), CommandType::CT_HW }, { std::string("-d"), CommandType::CT_D }, +}; + +const std::unordered_map commandHelpMap = { + { CommandHelp::HELP, std::string("--help") }, + { CommandHelp::VERSION, std::string("--version") }, +}; + +enum class TraceStatus { + TRACE_START, + TRACE_FINISH, + TRACE_NO +}; +} +} +#endif \ No newline at end of file diff --git a/device/device_command/include/gp_data.h b/device/device_command/include/gp_data.h deleted file mode 100644 index 05c939b..0000000 --- a/device/device_command/include/gp_data.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -* Copyright (C) 2021 Huawei Device Co., Ltd. -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -#ifndef GPDATA_H -#define GPDATA_H -#include - -class GPData { -public: - std::map values; -}; -#endif \ No newline at end of file diff --git a/device/device_command/include/gp_utils.h b/device/device_command/include/gp_utils.h deleted file mode 100644 index 37b0650..0000000 --- a/device/device_command/include/gp_utils.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -* Copyright (C) 2021 Huawei Device Co., Ltd. -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -#ifndef GP_UTILS_H -#define GP_UTILS_H -#include -#include -#include "gp_data.h" -namespace OHOS { - namespace SmartPerf { - namespace GPUtils { - void mSplit(const std::string &content, const std::string &sp, std::vector &out); - - bool canOpen(const std::string &path); - - std::string readFile(const std::string &cmd); - - std::string freadFile(const std::string &path); - - std::string getNumber(const std::string &str); - - void writeCsv(const std::string &path, std::vector &vmap); - }; - } -} - -#endif // GP_UTILS_H diff --git a/device/device_command/include/smartperf_command.h b/device/device_command/include/smartperf_command.h index f8453ff..2840dc9 100644 --- a/device/device_command/include/smartperf_command.h +++ b/device/device_command/include/smartperf_command.h @@ -18,57 +18,55 @@ #include #include -#include "profiler.h" -#include "socket_profiler.h" +#include "common.h" + namespace OHOS { - namespace SmartPerf { - class SmartPerfCommand { - public: - const std::string SmartPerf_EXE_NAME = "SP_daemon"; - const std::string SmartPerf_VERSION = "1.0.1\n"; - const std::string SmartPerf_MSG_ERR = "error input!\n use command '--help' get more information\n"; - const std::string SmartPerf_MSG = "usage: SP_daemon \n" - "--------------------------------------------------------------------\n" - "These are common commands list:\n" - " -N set num of profiler \n" - " -PKG set pkgname of profiler \n" - " -PID set process id of profiler \n" - " -OUT set output path of CSV\n" - " -c get cpuFreq and cpuLoad \n" - " -g get gpuFreq and gpuLoad \n" - " -f get fps and fps jitters \n" - " -t get soc-temp gpu-temp .. \n" - " -p get current_now and voltage_now \n" - " -r get ram(pss) \n" - " -snapshot get screen capture\n" - "--------------------------------------------------------------------\n" - "Example: SP_daemon -N 20 -PKG ohos.samples.ecg -c -g -t -p -f \n" - "--------------------------------------------------------------------\n"; - const int ONE_PARAM = 1; - const int TWO_PARAM = 2; - const int THREE_PARAM_MORE = 3; - SmartPerfCommand(int argc, char *argv[]); - ~SmartPerfCommand() {}; - void initSomething(); - std::string ExecCommand(); - // 采集次数 - int num = 0; - // 包名 - std::string pkgName = ""; - // 是否开启trace 抓取 - int trace = 0; - // csv输出路径 - std::string outPath = "/data/local/tmp/data.csv"; - std::string outPathParam = ""; - // 指定进程pid - int pid = 0; - // 采集配置项 - std::vector configs; - // 命令式采集 - std::shared_ptr profiler = nullptr; - // Socket采集 - std::shared_ptr socketProfiler = nullptr; - }; - } +namespace SmartPerf { +class SmartPerfCommand { +public: + const std::string smartPerfExeName = "SP_daemon"; + const std::string smartPerfVersion = "1.0.1\n"; + const std::string smartPerfMsgErr = "error input!\n use command '--help' get more information\n"; + const std::string smartPerfMsg = "usage: SP_daemon \n" + "--------------------------------------------------------------------\n" + "These are common commands list:\n" + " -N set num of profiler \n" + " -PKG set pkgname of profiler \n" + " -PID set process id of profiler \n" + " -OUT set output path of CSV\n" + " -c get cpuFreq and cpuLoad \n" + " -g get gpuFreq and gpuLoad \n" + " -f get fps and fps jitters \n" + " -t get soc-temp gpu-temp .. \n" + " -p get current_now and voltage_now \n" + " -r get ram(pss) \n" + " -snapshot get screen capture\n" + "--------------------------------------------------------------------\n" + "Example: SP_daemon -N 20 -PKG ohos.samples.ecg -c -g -t -p -f \n" + "--------------------------------------------------------------------\n"; + const int oneParam = 1; + const int twoParam = 2; + const int threeParamMore = 3; + SmartPerfCommand(int argc, char *argv[]); + ~SmartPerfCommand() {}; + static void InitSomething(); + std::string ExecCommand(); + void HelpCommand(CommandHelp type) const; + void HandleCommand(std::string argStr, std::string argStr1); + // 采集次数 + int num = 0; + // 包名 + std::string pkgName = ""; + // 是否开启trace 抓取 + int trace = 0; + // csv输出路径 + std::string outPath = "/data/local/tmp/data.csv"; + std::string outPathParam = ""; + // 指定进程pid + std::string pid = ""; + // 采集配置项 + std::vector configs; +}; +} } #endif // SMARTPERF_COMMAND_H \ No newline at end of file diff --git a/device/device_command/include/socket_profiler.h b/device/device_command/include/sp_csv_util.h similarity index 37% rename from device/device_command/include/socket_profiler.h rename to device/device_command/include/sp_csv_util.h index 84a91f5..626d3f1 100644 --- a/device/device_command/include/socket_profiler.h +++ b/device/device_command/include/sp_csv_util.h @@ -12,45 +12,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef SOCKET_PROFILER_H -#define SOCKET_PROFILER_H +#ifndef SP_CSV_UTIL_H +#define SP_CSV_UTIL_H +#include +#include +#include #include -#include -#include "CPU.h" -#include "GPU.h" -#include "DDR.h" -#include "FPS.h" -#include "RAM.h" -#include "Temperature.h" -#include "Power.h" -#include "ByTrace.h" +#include +#include +#include +#include "common.h" +#include "sp_data.h" namespace OHOS { namespace SmartPerf { -class SocketProfiler : public DelayedSingleton { -public: - void initSocketProfiler(); - void bufsendto(int sockLocal, const char *bufsend, int length, struct sockaddr *clientLocal, socklen_t len); - void callSend(std::stringstream &sstream, std::string &str1, std::string &str2); - void thread_udp_server(); - void initSocket(); - - std::shared_ptr mCpu = nullptr; - std::shared_ptr mGpu = nullptr; - std::shared_ptr mDdr = nullptr; - std::shared_ptr mFps = nullptr; - std::shared_ptr mRam = nullptr; - std::shared_ptr mTemperature = nullptr; - std::shared_ptr mPower = nullptr; - std::shared_ptr mByTrace = nullptr; - - int sock; - struct sockaddr_in local; - struct sockaddr_in client; - const size_t SOCK_PORT = 8283; - const size_t BUFF_SIZE_RECV = 256; - const size_t BUFF_SIZE_SEND = 2048; - +namespace SpCsvUtil { +void WriteCsv(const std::string &path, const std::vector &vmap) +{ + std::ofstream outFile; + char realPath[PATH_MAX] = {0x00}; + if (realpath(path.c_str(), realPath) == nullptr) { + std::cout << "" << std::endl; + } + outFile.open(path.c_str(), std::ios::out); + int i = 0; + std::string title = ""; + for (SPData spdata : vmap) { + std::string lineContent = ""; + for (auto iter = spdata.values.cbegin(); iter != spdata.values.cend(); ++iter) { + if (i == 0) { + title += iter->first + ","; + } + lineContent += iter->second + ","; + } + if (i == 0) { + title.pop_back(); + outFile << title << std::endl; + } + lineContent.pop_back(); + outFile << lineContent << std::endl; + ++i; + } + outFile.close(); +} }; } } -#endif \ No newline at end of file + +#endif // SP_CSV_UTILS_H diff --git a/device/device_command/include/gp_constant.h b/device/device_command/include/sp_data.h similarity index 71% rename from device/device_command/include/gp_constant.h rename to device/device_command/include/sp_data.h index ca08557..9ab8f77 100644 --- a/device/device_command/include/gp_constant.h +++ b/device/device_command/include/sp_data.h @@ -12,18 +12,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef GP_CONSTANT_H -#define GP_CONSTANT_H -enum NumberConstant { - ZERO = 0X00, - ONE = 0X01, - TWO = 0X02, - THREE = 0X03 -}; -enum FunConstant { - SUCCESS_ONE = 0X01, - SUCCESS_ZERO = 0X00, - ERROR_ZERO = 0X00, - ERROR_MINUX = 0xFFFFFFFF +#ifndef SP_DATA_H +#define SP_DATA_H +#include +#include +namespace OHOS { +namespace SmartPerf { +class SPData { +public: + std::map values; }; +} +} #endif \ No newline at end of file diff --git a/device/device_command/include/sp_profiler.h b/device/device_command/include/sp_profiler.h new file mode 100644 index 0000000..0f6853a --- /dev/null +++ b/device/device_command/include/sp_profiler.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SP_PROFILER_H +#define SP_PROFILER_H +#include +namespace OHOS { +namespace SmartPerf { +class SpProfiler { +public: + SpProfiler() {}; + virtual ~SpProfiler() {}; + virtual std::map ItemData() = 0; +}; +} +} +#endif \ No newline at end of file diff --git a/device/device_command/include/sp_profiler_factory.h b/device/device_command/include/sp_profiler_factory.h new file mode 100644 index 0000000..0db239a --- /dev/null +++ b/device/device_command/include/sp_profiler_factory.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SP_PROFILER_FACTORY_H +#define SP_PROFILER_FACTORY_H +#include "common.h" +#include "sp_profiler.h" +namespace OHOS { +namespace SmartPerf { +class SpProfilerFactory { +public: + static SpProfiler *GetProfilerItem(MessageType messageType); + static void SetProfilerPkg(std::string pkg); + static void SetProfilerPid(std::string pid); + static SpProfiler *GetCmdProfilerItem(CommandType commandType); +}; +} +} +#endif \ No newline at end of file diff --git a/device/device_command/include/sp_server_socket.h b/device/device_command/include/sp_server_socket.h new file mode 100644 index 0000000..9967c0b --- /dev/null +++ b/device/device_command/include/sp_server_socket.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SP_SERVERSOCKET_H +#define SP_SERVERSOCKET_H +#include +#include +namespace OHOS { +namespace SmartPerf { +class SpServerSocket { +public: + SpServerSocket(); + ~SpServerSocket(); + // 创建一个套接字 + int Init(); + // IO操作 + int Sendto(std::string &sendBuf); + int Recvfrom(); + // 关闭 + void Close() const; + std::string RecvBuf() const; + +private: + int sock = -1; + struct sockaddr_in local; + struct sockaddr_in client; + const int sockPort = 8283; + const static int buffSizeRecv = 256; + char rbuf[buffSizeRecv] = ""; +}; +} +} + +#endif // !SPSERVERSOCKET \ No newline at end of file diff --git a/device/device_command/include/sp_thread_socket.h b/device/device_command/include/sp_thread_socket.h new file mode 100644 index 0000000..b568e4a --- /dev/null +++ b/device/device_command/include/sp_thread_socket.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SP_THREAD_SOCKET_H +#define SP_THREAD_SOCKET_H +#include "sp_profiler_factory.h" +#include "sp_server_socket.h" +#include "sp_utils.h" +namespace OHOS { +namespace SmartPerf { +class SpThreadSocket { +public: + std::string MapToString(std::map dataMap) const + { + std::string result; + int i = 0; + std::string splitStr = ""; + for (auto iter = dataMap.cbegin(); iter != dataMap.cend(); ++iter) { + printf("%s = %s\n", iter->first.c_str(), iter->second.c_str()); + if (i > 0) { + splitStr = "$$"; + } + result += splitStr + iter->first.c_str() + "||" + iter->second.c_str(); + i++; + } + return result; + } + std::string ResPkgOrPid(SpServerSocket &spSocket) const + { + std::vector sps; + SPUtils::StrSplit(spSocket.RecvBuf(), "::", sps); + return sps[1]; + } + + void Process() + { + SpServerSocket spSocket; + spSocket.Init(); + while (1) { + spSocket.Recvfrom(); + HandleMsg(spSocket); + } + std::cout << "Socket Process finished!" << std::endl; + spSocket.Close(); + } + void HandleMsg(SpServerSocket &spSocket) const + { + auto iterator = messageMap.begin(); + while (iterator != messageMap.end()) { + if (SPUtils::IsSubString(spSocket.RecvBuf(), iterator->second)) { + SpProfiler *profiler = SpProfilerFactory::GetProfilerItem(iterator->first); + if (profiler == nullptr && (iterator->first == MessageType::SET_PKG_NAME)) { + std::string curPkgName = ResPkgOrPid(spSocket); + SpProfilerFactory::SetProfilerPkg(curPkgName); + std::string pidCmd = "pidof " + curPkgName; + std::string pidResult; + if (SPUtils::LoadCmd(pidCmd, pidResult)) { + SpProfilerFactory::SetProfilerPid(pidResult); + } + spSocket.Sendto(curPkgName); + } else if (profiler == nullptr && (iterator->first == MessageType::SET_PROCESS_ID)) { + SpProfilerFactory::SetProfilerPid(ResPkgOrPid(spSocket)); + } else if (profiler == nullptr) { + std::string returnStr = iterator->second; + spSocket.Sendto(returnStr); + } else { + std::map data = profiler->ItemData(); + std::string sendData = MapToString(data); + spSocket.Sendto(sendData); + } + break; + } + ++iterator; + } + } +}; +} +} +#endif \ No newline at end of file diff --git a/device/device_command/include/sp_utils.h b/device/device_command/include/sp_utils.h new file mode 100644 index 0000000..63b3d7b --- /dev/null +++ b/device/device_command/include/sp_utils.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SP_UTILS_H +#define SP_UTILS_H +#include +#include +namespace OHOS { +namespace SmartPerf { +namespace SPUtils { +/** + * @brief Check if the file has permission to access + * + * @param fileName + * @return true + * @return false + */ +bool FileAccess(const std::string &fileName); +/** + * @brief Load content from file node + * + * @param filePath + * @param content + * @return true + * @return false + */ +bool LoadFile(const std::string &filePath, std::string &content); +/** + * @brief read command return result + * + * @param cmd + * @param result + * @return true + * @return false + */ +bool LoadCmd(const std::string &cmd, std::string &result); +/** + * @brief + * + * @param path + * @return std::string + */ +std::string IncludePathDelimiter(const std::string &path); +/** + * @brief + * @param path + * @param files + */ +void ForDirFiles(const std::string &path, std::vector &files); + +/** + * @brief check if substr in parentstr + * + * @param str + * @param sub + * @return true + * @return false + */ +bool IsSubString(const std::string &str, const std::string &sub); +/** + * @brief split content by delimiter + * + * @param content + * @param sp + * @param out + */ +void StrSplit(const std::string &content, const std::string &sp, std::vector &out); +/** + * @brief extract number from str + * + * @param str + * @return std::string + */ +std::string ExtractNumber(const std::string &str); +/** + * @brief replace '' \r\n from str + * @param res + */ +void ReplaceString(std::string &res); +/** + * @brief get cur Time longlong + * + */ +long long GetCurTime(); +}; +} +} + +#endif // SP_UTILS_H diff --git a/device/device_command/profiler.cpp b/device/device_command/profiler.cpp deleted file mode 100644 index 62cf658..0000000 --- a/device/device_command/profiler.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2021 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include -#include -#include -#include "securec.h" -#include "include/profiler.h" -namespace OHOS { -namespace SmartPerf { -void Profiler::initProfiler() -{ - // get singleton instance - mCpu = CPU::GetInstance(); - mGpu = GPU::GetInstance(); - mDdr = DDR::GetInstance(); - mFps = FPS::GetInstance(); - mRam = RAM::GetInstance(); - mTemperature = Temperature::GetInstance(); - mPower = Power::GetInstance(); - mByTrace = ByTrace::GetInstance(); - - // some init methods - mTemperature->init_temperature(); - mGpu->init_gpu_node(); - mPower->init_power(); - if (mByTrace->init_trace(true) == TraceStatus::TRACE_START) { - std::thread pInitTrace(&ByTrace::thread_get_trace, mByTrace); - } -} - -void Profiler::createCpu(std::map &gpMap) -{ - int cpuCoreNum = mCpu->get_cpu_num(); - for (int i = 0; i < cpuCoreNum; i++) { - int curFreq = mCpu->get_cpu_freq(i); - char desc[10]; - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "cpu%dfreq", i) > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(curFreq))); - } - } - - std::vector workloads = mCpu->get_cpu_load(); - - for (size_t i = 1; i < workloads.size(); ++i) { - char desc[10]; - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "cpu%dload", i - 1) > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(workloads[i]))); - } - } -} -void Profiler::createGpu(std::map &gpMap) -{ - int ret = mGpu->get_gpu_freq(); - float workload = mGpu->get_gpu_load(); - char desc[10]; - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "gpufreq") > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(ret))); - } - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "gpuload") > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(workload))); - } -} -void Profiler::createDdr(std::map &gpMap) -{ - long long ret = mDdr->get_ddr_freq(); - char desc[10]; - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "ddrfreq") > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(ret))); - } -} -void Profiler::createFps(int isVideo, int isCamera, int isCatchTrace, int curProfilerNum, - std::map &gpMap) -{ - FpsInfo gfpsInfo = mFps->getFpsInfo(isVideo, isCamera); - char desc[10]; - if (snprintf_s(desc, sizeof(desc), sizeof(desc), "fps") > 0) { - gpMap.insert(std::pair(std::string(desc), std::to_string(gfpsInfo.fps))); - } - if (isCatchTrace > 0) { - if (mByTrace->check_fps_jitters(gfpsInfo.jitters, curProfilerNum) == TraceStatus::TRACE_FINISH) { - std::string profilerNum = std::to_string(curProfilerNum); - std::thread pFinishTrace(&ByTrace::thread_finish_trace, mByTrace, std::ref(profilerNum)); - } - } -} -void Profiler::createTemp(std::map &gpMap) -{ - std::map tempInfo = mTemperature->getThermalMap(); - std::map::iterator iter; - for (iter = tempInfo.begin(); iter != tempInfo.end(); ++iter) { - float value = iter->second; - gpMap.insert(std::pair(iter->first, std::to_string(value))); - } -} -void Profiler::createPower(std::map &gpMap) -{ - std::map powerInfo; - powerInfo = mPower->getPowerMap(); - std::map::iterator iter; - for (iter = powerInfo.begin(); iter != powerInfo.end(); ++iter) { - gpMap.insert(std::pair(iter->first, iter->second)); - } -} -void Profiler::createRam(const std::string &pkg_name, std::map &gpMap, int pid) -{ - std::map gramInfo; - gramInfo = mRam->getRamInfo(pkg_name, pid); - std::map::iterator iter; - for (iter = gramInfo.begin(); iter != gramInfo.end(); ++iter) { - gpMap.insert(std::pair(iter->first, iter->second)); - } -} -void Profiler::createSnapshot(std::map &gpMap, long long timestamp) -{ - char pathstr[50]; - std::string path; - if (snprintf_s(pathstr, sizeof(pathstr), sizeof(pathstr), "/data/local/tmp/capture/%lld", timestamp) > 0) { - path = pathstr; - } - char cmdCapture[100]; - if (snprintf_s(cmdCapture, sizeof(cmdCapture), sizeof(cmdCapture), - "snapshot_display -f /data/local/tmp/capture/%lld.png", timestamp) > 0) { - GPUtils::readFile(cmdCapture); - } - gpMap.insert(std::pair(std::string("snapshotPath"), path)); -} -} -} \ No newline at end of file diff --git a/device/device_command/smartperf_command.cpp b/device/device_command/smartperf_command.cpp index 20cc479..af3765e 100644 --- a/device/device_command/smartperf_command.cpp +++ b/device/device_command/smartperf_command.cpp @@ -12,154 +12,144 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include +#include #include -#include "sys/time.h" +#include #include "unistd.h" -#include "include/gp_data.h" -#include "include/gp_utils.h" +#include "include/sp_utils.h" +#include "include/sp_csv_util.h" +#include "include/sp_profiler_factory.h" +#include "include/sp_thread_socket.h" +#include "include/ByTrace.h" #include "include/smartperf_command.h" - namespace OHOS { namespace SmartPerf { SmartPerfCommand::SmartPerfCommand(int argc, char *argv[]) { - if (argc == ONE_PARAM) { - socketProfiler = SocketProfiler::GetInstance(); - socketProfiler->initSocketProfiler(); + if (argc == oneParam) { daemon(0, 0); - std::thread t_udp(&SocketProfiler::thread_udp_server, socketProfiler); - t_udp.join(); + InitSomething(); + SpThreadSocket spThreadSocket; + std::thread tSocket(&SpThreadSocket::Process, spThreadSocket); + tSocket.join(); } - if (argc == TWO_PARAM) { - char *cmd = argv[1]; - if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { - std::cout << SmartPerf_MSG; - } else if (strcmp(cmd, "--version") == 0) { - std::cout << SmartPerf_VERSION; - } else { - std::cout << SmartPerf_MSG_ERR; + if (argc == twoParam) { + auto iterator = commandHelpMap.begin(); + while (iterator != commandHelpMap.end()) { + if (strcmp(argv[1], iterator->second.c_str()) == 0) { + HelpCommand(iterator->first); + break; + } + ++iterator; } } - if (argc >= THREE_PARAM_MORE) { - profiler = Profiler::GetInstance(); - profiler->initProfiler(); + if (argc >= threeParamMore) { for (int i = 1; i <= argc - 1; i++) { - if ((strcmp(argv[i], "-N") == 0) || (strcmp(argv[i], "--num") == 0)) { - num = atoi(argv[i + 1]); - if (num > 0) { - std::cout << "set num:" << num << std::endl; - } else { - std::cout << "error input args: -N" << std::endl; - } - } - if ((strcmp(argv[i], "-PKG") == 0) || (strcmp(argv[i], "--pkgname") == 0)) { - pkgName = argv[i + 1]; - if (strcmp(pkgName.c_str(), "") != 0) { - profiler->mFps->setPackageName(pkgName); - std::cout << "set pkg name:" << pkgName << std::endl; - } else { - std::cout << "empty input args: -PKG" << std::endl; - } - } - if ((strcmp(argv[i], "-PID") == 0) || (strcmp(argv[i], "--processid") == 0)) { - pid = atoi(argv[i + 1]); - if (pid > 0) { - std::cout << "set test pid:" << pid << std::endl; - } else { - std::cout << "error input args: -PID " << std::endl; - } + std::string argStr = argv[i]; + std::string argStr1; + if (i < argc - 1) { + argStr1 = argv[i + 1]; } - - if ((strcmp(argv[i], "-OUT") == 0) || (strcmp(argv[i], "--output") == 0)) { - outPathParam = argv[i + 1]; - if (strcmp(outPathParam.c_str(), "") != 0) { - outPath = outPathParam + std::string(".csv"); - } - } - - if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "-g") == 0 || strcmp(argv[i], "-d") == 0 || - strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "-f1") == 0 || strcmp(argv[i], "-f2") == 0 || - strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "-r") == 0 || - strcmp(argv[i], "-trace") == 0 || strcmp(argv[i], "-snapshot") == 0) { - configs.push_back(argv[i]); + if (commandMap.count(argStr) > 0) { + HandleCommand(argStr, argStr1); } } } } +void SmartPerfCommand::HelpCommand(CommandHelp type) const +{ + if (type == CommandHelp::HELP) { + std::cout << smartPerfMsg << std::endl; + } + if (type == CommandHelp::VERSION) { + std::cout << smartPerfVersion << std::endl; + } +} +void SmartPerfCommand::HandleCommand(std::string argStr, std::string argStr1) +{ + switch (commandMap.at(argStr)) { + case CommandType::CT_N: + num = atoi(argStr1.c_str()); + break; + case CommandType::CT_PKG: + pkgName = argStr1; + break; + case CommandType::CT_PID: + pid = argStr1; + break; + case CommandType::CT_OUT: + outPathParam = argStr1; + if (strcmp(outPathParam.c_str(), "") != 0) { + outPath = outPathParam + std::string(".csv"); + } + break; + case CommandType::CT_C: + case CommandType::CT_G: + case CommandType::CT_D: + case CommandType::CT_F: + case CommandType::CT_T: + case CommandType::CT_P: + case CommandType::CT_R: + case CommandType::CT_TTRACE: + case CommandType::CT_SNAPSHOT: + case CommandType::CT_HW: + configs.push_back(argStr); + break; + default: + std::cout << "other unknown args:" << argStr << std::endl; + break; + } + SpProfilerFactory::SetProfilerPid(pid); + SpProfilerFactory::SetProfilerPkg(pkgName); +} + std::string SmartPerfCommand::ExecCommand() { int index = 0; - std::vector vmap; + std::vector vmap; while (index < num) { - std::map gpMap; - struct timeval tv; - gettimeofday(&tv, nullptr); - long long timestamp = tv.tv_sec * 1000 + tv.tv_usec / 1000; - gpMap.insert(std::pair(std::string("timestamp"), std::to_string(timestamp))); + std::map spMap; + long long timestamp = SPUtils::GetCurTime(); + spMap.insert(std::pair(std::string("timestamp"), std::to_string(timestamp))); for (size_t j = 0; j < configs.size(); j++) { std::string curParam = configs[j]; - if (strcmp(curParam.c_str(), "-trace") == 0) { - trace = 1; - } - if (strcmp(curParam.c_str(), "-c") == 0) { - profiler->createCpu(gpMap); - } - if (strcmp(curParam.c_str(), "-g") == 0) { - profiler->createGpu(gpMap); - } - if (strcmp(curParam.c_str(), "-d") == 0) { - profiler->createDdr(gpMap); - } - if (strcmp(curParam.c_str(), "-f") == 0) { - profiler->createFps(0, 0, trace, index, gpMap); - } - if (strcmp(curParam.c_str(), "-f1") == 0) { - profiler->createFps(1, 0, trace, index, gpMap); - } - if (strcmp(curParam.c_str(), "-f2") == 0) { - profiler->createFps(0, 1, trace, index, gpMap); - } - if (strcmp(curParam.c_str(), "-t") == 0) { - profiler->createTemp(gpMap); - } - if (strcmp(curParam.c_str(), "-p") == 0) { - profiler->createPower(gpMap); - } - if (strcmp(curParam.c_str(), "-r") == 0) { - if (strcmp(pkgName.c_str(), "") != 0 || pid > 0) { - profiler->createRam(pkgName, gpMap, pid); - } - } - if (strcmp(curParam.c_str(), "-snapshot") == 0) { - profiler->createSnapshot(gpMap, timestamp); + SpProfiler *profiler = SpProfilerFactory::GetCmdProfilerItem(commandMap.at(curParam)); + if (profiler != nullptr) { + std::map data = profiler->ItemData(); + std::map tempData = spMap; + tempData.insert(data.cbegin(), data.cend()); + spMap = tempData; } } - printf("----------------------------------Print START------------------------------------\n"); - std::map::iterator iter; + std::cout << std::endl; int i = 0; - for (iter = gpMap.begin(); iter != gpMap.end(); ++iter) { + for (auto iter = spMap.cbegin(); iter != spMap.cend(); ++iter) { printf("order:%d %s=%s\n", i, iter->first.c_str(), iter->second.c_str()); i++; } - printf("----------------------------------Print END--------------------------------------\n"); + std::cout << std::endl; - GPData gpdata; - gpdata.values = gpMap; - vmap.push_back(gpdata); + SPData spdata; + spdata.values = spMap; + vmap.push_back(spdata); sleep(1); index++; } - - GPUtils::writeCsv(std::string(outPath.c_str()), vmap); - + SpCsvUtil::WriteCsv(std::string(outPath.c_str()), vmap); return std::string("command exec finished!"); } -void SmartPerfCommand::initSomething() +void SmartPerfCommand::InitSomething() { - GPUtils::readFile("chmod a+w /proc/stat"); + std::string cmdResult; + if (SPUtils::LoadCmd("chmod o+r /proc/stat", cmdResult)) { + printf("Privilege escalation! \n"); + }; + if (!SPUtils::FileAccess("/data/local/tmp/capture")) { + SPUtils::LoadCmd("mkdir /data/local/tmp/capture", cmdResult); + printf("/data/local/tmp/capture created! \n"); + }; } } } diff --git a/device/device_command/smartperf_main.cpp b/device/device_command/smartperf_main.cpp index fcca632..f645958 100644 --- a/device/device_command/smartperf_main.cpp +++ b/device/device_command/smartperf_main.cpp @@ -1,24 +1,21 @@ /* -* Copyright (C) 2021 Huawei Device Co., Ltd. -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "include/smartperf_command.h" - int main(int argc, char *argv[]) { OHOS::SmartPerf::SmartPerfCommand cmd(argc, argv); - cmd.initSomething(); std::cout << cmd.ExecCommand() << std::endl; return 0; } diff --git a/device/device_command/socket_profiler.cpp b/device/device_command/socket_profiler.cpp deleted file mode 100644 index 08cb87c..0000000 --- a/device/device_command/socket_profiler.cpp +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2021 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include -#include -#include -#include -#include "include/gp_utils.h" -#include "include/socket_profiler.h" -namespace OHOS { -namespace SmartPerf { -void SocketProfiler::initSocketProfiler() -{ - mCpu = CPU::GetInstance(); - mGpu = GPU::GetInstance(); - mDdr = DDR::GetInstance(); - mFps = FPS::GetInstance(); - mRam = RAM::GetInstance(); - mTemperature = Temperature::GetInstance(); - mPower = Power::GetInstance(); - mByTrace = ByTrace::GetInstance(); - - mTemperature->init_temperature(); - mGpu->init_gpu_node(); - mPower->init_power(); -} - -void SocketProfiler::bufsendto(int sockLocal, const char *bufsend, int length, - struct sockaddr *clientLocal, socklen_t len) -{ - ssize_t echo_size = sendto(sockLocal, bufsend, length, 0, clientLocal, len); - if (echo_size < 0) { - printf("sendto error, buf is %s\n", bufsend); - } -} - -void SocketProfiler::callSend(std::stringstream &sstream, std::string &str1, std::string &str2) -{ - sstream.str(""); - sstream.clear(); - sstream << str1 << "::" << str2; - std::string streamSend = sstream.str(); - bufsendto(sock, streamSend.c_str(), streamSend.size(), - reinterpret_cast(&client), sizeof(sockaddr_in)); -} - -void SocketProfiler::initSocket() -{ - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock < 0) { - perror("socket error"); - } - local.sin_family = AF_INET; - local.sin_port = htons(SOCK_PORT); - local.sin_addr.s_addr = htonl(INADDR_ANY); - if (::bind(sock, reinterpret_cast(&local), sizeof(local)) < 0) { - perror("bind error"); - } -} - -void SocketProfiler::thread_udp_server() -{ - std::shared_ptr SP = SocketProfiler::GetInstance(); - SP->initSocket(); - socklen_t len = sizeof(sockaddr_in); - std::stringstream sstream; - const int loopforever = 1; - printf("enter while loop forever\n"); - while (loopforever) { - char recvbuf[BUFF_SIZE_RECV]; - recvbuf[0] = '\0'; - ssize_t _size = recvfrom(sock, recvbuf, sizeof(recvbuf) - 1, 0, - reinterpret_cast(&client), &len); - if (_size > 0) { - recvbuf[_size] = '\0'; - printf("server recvbuf:%s\n", recvbuf); - } - sstream.str(""); - sstream.clear(); - std::string recv = std::string(recvbuf); - if (recv.find("get_cpu_num") != std::string::npos) { - std::string recvStr = "get_cpu_num"; - int ret = SP->mCpu->get_cpu_num(); - std::string str2 = std::to_string(ret); - SP->callSend(sstream, recvStr, str2); - } else if (recv.find("get_cpu_freq") != std::string::npos) { - std::vector sps; - GPUtils::mSplit(recv, "_", sps); - int cpu_id = std::stoi(sps[sps.size() - 1]); - int ret = SP->mCpu->get_cpu_freq(cpu_id); - std::string str2 = std::to_string(ret); - SP->callSend(sstream, recv, str2); - } else if (recv.find("get_cpu_load") != std::string::npos) { - std::vector workloads = SP->mCpu->get_cpu_load(); - std::string res = ""; - for (size_t i = 0; i < workloads.size(); ++i) { - if (i != 0) { - res += "=="; - } - res += std::to_string(workloads[i]); - } - SP->callSend(sstream, recv, res); - } else if (recv.find("set_pkgName") != std::string::npos) { - std::vector sps; - GPUtils::mSplit(recv, "::", sps); - if (sps.size() > 1) { - SP->mFps->setPackageName(sps[1]); - SP->mRam->setPkgName(sps[1]); - } - } else if (recv.find("get_fps_and_jitters") != std::string::npos) { - std::vector sps; - GPUtils::mSplit(recv, "::", sps); - if (sps.size() > 2) { - int is_video = atoi(sps[1].c_str()); - int is_camera = atoi(sps[2].c_str()); - - FpsInfo gfpsInfo = SP->mFps->getFpsInfo(is_video, is_camera); - std::string res = ""; - res += "timestamp|"; - res += std::to_string(gfpsInfo.current_fps_time); - res += ";"; - res += "fps|"; - res += std::to_string(gfpsInfo.fps); - res += ";"; - res += "jitter|"; - for (size_t i = 0; i < gfpsInfo.jitters.size(); ++i) { - res += std::to_string(gfpsInfo.jitters[i]); - res += "=="; - } - std::string recvStr = "get_fps_and_jitters"; - SP->callSend(sstream, recvStr, res); - } - } else if (recv.find("get_gpu_freq") != std::string::npos) { - int ret = SP->mGpu->get_gpu_freq(); - std::string str2 = std::to_string(ret); - SP->callSend(sstream, recv, str2); - } else if (recv.find("get_gpu_load") != std::string::npos) { - float workload = SP->mGpu->get_gpu_load(); - std::string str2 = std::to_string(workload); - SP->callSend(sstream, recv, str2); - } else if (recv.find("get_ddr_freq") != std::string::npos) { - long long ret = SP->mDdr->get_ddr_freq(); - std::string str2 = std::to_string(ret); - SP->callSend(sstream, recv, str2); - } else if (recv.find("get_ram_info") != std::string::npos) { - std::vector sps; - GPUtils::mSplit(recv, "::", sps); - if (sps.size() > 1) { - std::map gramInfo = SP->mRam->getRamInfo(sps[1], 0); - std::string res = "Pss"; - std::map::iterator iter; - int i = 0; - for (iter = gramInfo.begin(); iter != gramInfo.end(); ++iter) { - if (i != 0) - res += "=="; - res += iter->second; - ++i; - } - SP->callSend(sstream, sps[0], res); - } - } else if (recv.find("get_temperature") != std::string::npos) { - std::map tempInfo = SP->mTemperature->getThermalMap(); - std::string res = ""; - std::map::iterator iter; - int i = 0; - for (iter = tempInfo.begin(); iter != tempInfo.end(); ++iter) { - if (i != 0) { - res += "=="; - } - res += (iter->first + ",," + std::to_string(iter->second)); - ++i; - } - SP->callSend(sstream, recv, res); - } else if (recv.find("get_power") != std::string::npos) { - std::map powerInfo; - powerInfo = SP->mPower->getPowerMap(); - std::string res = ""; - std::map::iterator iter; - int i = 0; - for (iter = powerInfo.begin(); iter != powerInfo.end(); ++iter) { - if (i != 0) { - res += "=="; - } - res += (iter->first + ",," + iter->second); - ++i; - } - SP->callSend(sstream, recv, res); - } else if (recv.find("get_capture") != std::string::npos) { - sstream << "snapshot_display"; - std::string cmd_capture = sstream.str(); - GPUtils::readFile(cmd_capture); - } else if (recv.find("catch_trace_start") != std::string::npos) { - std::thread tStart(&ByTrace::thread_get_trace, SP->mByTrace); - } else if (recv.find("catch_trace_finish") != std::string::npos) { - std::vector traces; - GPUtils::mSplit(recv, "::", traces); - std::thread tFinish(&ByTrace::thread_finish_trace, SP->mByTrace, std::ref(traces[1])); - } - } -} -} -} diff --git a/device/device_command/sp_profiler_factory.cpp b/device/device_command/sp_profiler_factory.cpp new file mode 100644 index 0000000..fcb5024 --- /dev/null +++ b/device/device_command/sp_profiler_factory.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include "include/CPU.h" +#include "include/DDR.h" +#include "include/GPU.h" +#include "include/FPS.h" +#include "include/RAM.h" +#include "include/Power.h" +#include "include/Temperature.h" +#include "include/ByTrace.h" +#include "include/sp_utils.h" +#include "include/sp_profiler_factory.h" +namespace OHOS { +namespace SmartPerf { +SpProfiler *SpProfilerFactory::GetProfilerItem(MessageType messageType) +{ + SpProfiler *profiler = nullptr; + switch (messageType) { + case MessageType::GET_CPU_NUM: + case MessageType::GET_CPU_FREQ: + case MessageType::GET_CPU_LOAD: + profiler = &CPU::GetInstance(); + break; + case MessageType::GET_FPS_AND_JITTERS: + profiler = &FPS::GetInstance(); + break; + case MessageType::GET_GPU_FREQ: + case MessageType::GET_GPU_LOAD: + profiler = &GPU::GetInstance(); + break; + case MessageType::GET_DDR_FREQ: + profiler = &DDR::GetInstance(); + break; + case MessageType::GET_RAM_INFO: + profiler = &RAM::GetInstance(); + break; + case MessageType::GET_TEMPERATURE: + profiler = &Temperature::GetInstance(); + break; + case MessageType::GET_POWER: + profiler = &Power::GetInstance(); + break; + case MessageType::CATCH_TRACE_START: + ByTrace::GetInstance().ThreadGetTrace(); + break; + case MessageType::CATCH_TRACE_FINISH: { + ByTrace::GetInstance().ThreadEndTrace(); + long long curTimeStamp = SPUtils::GetCurTime(); + std::string curTime = std::to_string(curTimeStamp); + ByTrace::GetInstance().ThreadFinishTrace(curTime); + break; + } + case MessageType::GET_CAPTURE: + FPS::GetInstance().SetCaptureOn(); + break; + default: + break; + } + return profiler; +} +void SpProfilerFactory::SetProfilerPkg(std::string pkg) +{ + FPS &fps = FPS::GetInstance(); + fps.SetPackageName(pkg); +} +void SpProfilerFactory::SetProfilerPid(std::string pid) +{ + RAM &ram = RAM::GetInstance(); + ram.SetProcessId(pid); +} +SpProfiler *SpProfilerFactory::GetCmdProfilerItem(CommandType commandType) +{ + SpProfiler *profiler = nullptr; + switch (commandType) { + case CommandType::CT_C: + profiler = &CPU::GetInstance(); + break; + case CommandType::CT_G: + profiler = &GPU::GetInstance(); + break; + case CommandType::CT_F: + case CommandType::CT_F1: + case CommandType::CT_F2: + profiler = &FPS::GetInstance(); + break; + case CommandType::CT_D: + profiler = &DDR::GetInstance(); + break; + case CommandType::CT_P: + profiler = &Power::GetInstance(); + break; + case CommandType::CT_T: + profiler = &Temperature::GetInstance(); + break; + case CommandType::CT_R: + profiler = &RAM::GetInstance(); + break; + case CommandType::CT_TTRACE: + FPS::GetInstance().SetTraceCatch(); + break; + case CommandType::CT_SNAPSHOT: + FPS::GetInstance().SetCaptureOn(); + break; + default: + break; + } + return profiler; +} +} +} diff --git a/device/device_command/include/profiler.h b/device/device_command/sp_server_socket.cpp similarity index 32% rename from device/device_command/include/profiler.h rename to device/device_command/sp_server_socket.cpp index c5db299..2ba9374 100644 --- a/device/device_command/include/profiler.h +++ b/device/device_command/sp_server_socket.cpp @@ -12,43 +12,61 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef PROFILER_H -#define PROFILER_H -#include -#include -#include "CPU.h" -#include "GPU.h" -#include "DDR.h" -#include "FPS.h" -#include "RAM.h" -#include "Temperature.h" -#include "Power.h" -#include "ByTrace.h" -#include "gp_data.h" - +#include +#include +#include +#include "include/sp_server_socket.h" namespace OHOS { namespace SmartPerf { -class Profiler : public DelayedSingleton{ -public: - void initProfiler(); - void createCpu(std::map &gpMap); - void createGpu(std::map &gpMap); - void createDdr(std::map &gpMap); - void createFps(int isVideo, int isCamera, int isCatchTrace, int curProfilerNum, - std::map &gpMap); - void createTemp(std::map &gpMap); - void createPower(std::map &gpMap); - void createRam(const std::string &pkg_name, std::map &gpMap, int pid); - void createSnapshot(std::map &gpMap, long long timestamp); - std::shared_ptr mCpu = nullptr; - std::shared_ptr mGpu = nullptr; - std::shared_ptr mDdr = nullptr; - std::shared_ptr mFps = nullptr; - std::shared_ptr mRam = nullptr; - std::shared_ptr mTemperature = nullptr; - std::shared_ptr mPower = nullptr; - std::shared_ptr mByTrace = nullptr; -}; -} -} -#endif \ No newline at end of file +SpServerSocket::SpServerSocket() {} + +SpServerSocket::~SpServerSocket() +{ + Close(); +} + +int SpServerSocket::Init() +{ + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + std::cout << "Socket Create Failed:" << sock << std::endl; + } + local.sin_family = AF_INET; + local.sin_port = htons(sockPort); + local.sin_addr.s_addr = htonl(INADDR_ANY); + if (::bind(sock, reinterpret_cast(&local), sizeof(local)) < 0) { + std::cout << "Socket Bind failed:" << sock << std::endl; + } + return 0; +} + +int SpServerSocket::Sendto(std::string &sendBuf) +{ + socklen_t len = sizeof(sockaddr_in); + sendto(sock, sendBuf.c_str(), sendBuf.size(), 0, reinterpret_cast(&client), len); + return 0; +} + +void SpServerSocket::Close() const +{ + shutdown(sock, SHUT_RD); +} + +int SpServerSocket::Recvfrom() +{ + bzero(rbuf, sizeof(rbuf)); + socklen_t len = sizeof(sockaddr_in); + int l = recvfrom(sock, rbuf, sizeof(rbuf) - 1, 0, reinterpret_cast(&client), &len); + if (l > 0) { + std::cout << "Client:" << rbuf << std::endl; + } + return l; +} + +std::string SpServerSocket::RecvBuf() const +{ + std::string recvBuf = rbuf; + return recvBuf; +} +} +} diff --git a/device/device_command/sp_utils.cpp b/device/device_command/sp_utils.cpp new file mode 100644 index 0000000..a1615e5 --- /dev/null +++ b/device/device_command/sp_utils.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sys/time.h" +#include "include/sp_utils.h" +namespace OHOS { +namespace SmartPerf { +bool SPUtils::FileAccess(const std::string &fileName) +{ + return (access(fileName.c_str(), F_OK) == 0); +} +bool SPUtils::LoadFile(const std::string &filePath, std::string &content) +{ + char realPath[PATH_MAX] = {0x00}; + if (realpath(filePath.c_str(), realPath) == nullptr) { + std::cout << "" << std::endl; + } + std::ifstream file(realPath); + if (!file.is_open()) { + return false; + } + + file.seekg(0, std::ios::end); + file.tellg(); + + content.clear(); + file.seekg(0, std::ios::beg); + copy(std::istreambuf_iterator(file), std::istreambuf_iterator(), std::back_inserter(content)); + // remove '' \n\r + ReplaceString(content); + return true; +} +bool SPUtils::LoadCmd(const std::string &cmd, std::string &result) +{ + std::string cmdExc = cmd; + FILE *fd = popen(cmdExc.c_str(), "r"); + if (fd == nullptr) { + return false; + } + char buf[1024] = {'\0'}; + int ret = fread(buf, sizeof(buf), 1, fd); + if (ret >= 0) { + result = buf; + } + if (pclose(fd) == -1) { + std::cout << "" << std::endl; + } + // remove '' \n\r + ReplaceString(result); + return ret >= 0 ? true : false; +} + +std::string SPUtils::IncludePathDelimiter(const std::string &path) +{ + if (path.rfind("/") != path.size() - 1) { + return path + "/"; + } + + return path; +} +void SPUtils::ForDirFiles(const std::string &path, std::vector &files) +{ + std::string pathStringWithDelimiter; + DIR *dir = opendir(path.c_str()); + if (dir == nullptr) { + return; + } + + while (true) { + struct dirent *ptr = readdir(dir); + if (ptr == nullptr) { + break; + } + + // current dir OR parent dir + if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0)) { + continue; + } else if (ptr->d_type == DT_DIR) { + pathStringWithDelimiter = IncludePathDelimiter(path) + std::string(ptr->d_name); + ForDirFiles(pathStringWithDelimiter, files); + } else { + files.push_back(IncludePathDelimiter(path) + std::string(ptr->d_name)); + } + } + closedir(dir); +} +bool SPUtils::IsSubString(const std::string &str, const std::string &sub) +{ + if (sub.empty() || str.empty()) { + return false; + } + + return str.find(sub) != std::string::npos; +} +void SPUtils::StrSplit(const std::string &content, const std::string &sp, std::vector &out) +{ + size_t index = 0; + while (index != std::string::npos) { + size_t tEnd = content.find_first_of(sp, index); + std::string tmp = content.substr(index, tEnd - index); + if (tmp != "" && tmp != " ") { + out.push_back(tmp); + } + if (tEnd == std::string::npos) { + break; + } + index = tEnd + 1; + } +} +std::string SPUtils::ExtractNumber(const std::string &str) +{ + int cntInt = 0; + const int shift = 10; + for (int i = 0; str[i] != '\0'; ++i) { + if (str[i] >= '0' && str[i] <= '9') { + cntInt *= shift; + cntInt += str[i] - '0'; + } + } + return std::to_string(cntInt); +} +void SPUtils::ReplaceString(std::string &res) +{ + std::string flagOne = "\r"; + std::string flagTwo = "\n"; + std::string::size_type ret = res.find(flagOne); + while (ret != res.npos) { + res.replace(ret, 1, ""); + ret = res.find(flagOne); + } + ret = res.find(flagTwo); + while (ret != res.npos) { + res.replace(ret, 1, ""); + ret = res.find(flagTwo); + } +} +long long SPUtils::GetCurTime() +{ + struct timeval tv; + gettimeofday(&tv, nullptr); + long long timestamp = tv.tv_sec * 1000 + tv.tv_usec / 1000; + return timestamp; +} +} +} diff --git a/device/device_ui/.gitignore b/device/device_ui/.gitignore deleted file mode 100644 index ce9e170..0000000 --- a/device/device_ui/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*/build -/build/* -/entry/build/* -/gp_daemon_for_hap/libs/* -/gp_daemon_for_hap/obj/* -/gp_daemon_for_hap/push_x64.bat -/.idea -/local.properties -/node_modules/* -/.deveco/* -/entry/.preview -/entry/.preview/* - diff --git a/device/device_ui/AppScope/app.json5 b/device/device_ui/AppScope/app.json similarity index 85% rename from device/device_ui/AppScope/app.json5 rename to device/device_ui/AppScope/app.json index f1c18a3..9dd7fce 100644 --- a/device/device_ui/AppScope/app.json5 +++ b/device/device_ui/AppScope/app.json @@ -1,6 +1,6 @@ { "app": { - "bundleName": "com.ohos.gameperceptio", + "bundleName": "com.ohos.smartperf", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", diff --git a/device/device_ui/BUILD.gn b/device/device_ui/BUILD.gn new file mode 100644 index 0000000..c01164a --- /dev/null +++ b/device/device_ui/BUILD.gn @@ -0,0 +1,43 @@ +# Copyright (c) 2021 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import("//build/ohos.gni") +import("../../../../build/config.gni") + +ohos_hap("SmartPerf") { + hap_profile = "entry/src/main/module.json" + deps = [ + ":smartperf_js_assets", + ":smartperf_resources", + ] + certificate_profile = "signature/openharmony_smartperf.p7b" + subsystem_name = "${OHOS_PROFILER_SUBSYS_NAME}" + part_name = "${OHOS_PROFILER_PART_NAME}" + module_install_dir = "app/com.ohos.gameperceptio" + js_build_mode = "debug" +} + +ohos_js_assets("smartperf_js_assets") { + source_dir = "entry/src/main/ets" + ets2abc = true +} + +ohos_app_scope("smartperf_app_profile") { + app_profile = "AppScope/app.json" + sources = [ "AppScope/resources" ] +} + +ohos_resources("smartperf_resources") { + sources = [ "entry/src/main/resources" ] + deps = [ ":smartperf_app_profile" ] + hap_profile = "entry/src/main/module.json" +} diff --git a/device/device_ui/README_zh.md b/device/device_ui/README_zh.md index 0772ae0..c26c2e9 100644 --- a/device/device_ui/README_zh.md +++ b/device/device_ui/README_zh.md @@ -1,73 +1,147 @@ -## 一、简介 -- OpenHarmony性能测试工具 SmartPerf UI版本,可采集CPU、GPU、Temperature、Power、应用RAM、FPS等指标,可使用悬浮窗口实时展示测试数据并支持本地测试报告图表展示。 +# SmartPerf.hap +## 简介 +- OpenHarmony性能测试工具 SmartPerf UI版本,可采集CPU、GPU、Temperature、Power、应用RAM、FPS等指标,可使用悬浮窗口控制并实时展示测试数据以及支持本地测试报告图表展示功能。 -## 二、如何使用 +- 带屏幕的设备且性能较好的设备推荐使用UI版本,性能较差或无屏幕设备请使用[命令行版本](https://gitee.com/openharmony/developtools_profiler/blob/master/host/smartperf/client/client_command/README_zh.md)。 -- 步骤1:打开桌面预制的Smartperf工具;
-- 步骤2:点击登录,进入首页,点击开始测试页,选择被测应用,配置采集项(目前支持CPU、GPU、FPS、POWER、TEMP、RAM、截图能力、trace);
-- 步骤3:选择应用后根据应用类型选择是否是视频应用、是否是相机应用,点击开始测试、启动悬浮窗(左上角展示),会自动拉起应用,左上角悬浮窗展示start后点击即可开始,展示会变为计时状态,显示当前采集计时;(双击展开、关闭详情悬浮窗;长按结束任务保存数据;单击暂停、继续任务;点击详情悬浮窗后任意采集项会展开相应采集折线图)
-- 步骤4:长按计时器,可以保存采集项数据,悬浮框消失;
-- 步骤5:报告列表页查看报告,原始数据可在概览页对应路径自行使用hdc命令pull出来
+## 代码目录 -## 三、功能介绍 -**1、采集指标说明**
+``` +/developtools/profiler/host/smartperf/client/client_ui +├── AppScope # SmartPerf的包名、应用名称等配置目录 +├── entry # SmartPerf主要代码文件 +│ └── src +│ └── main +│ ├── ets # ets代码目录 +│ ├──Application +│ ├── abilityStage.ts # 应用abilityStage.ts文件 +│ ├── MainAbility +│ ├── MainAbility.ts # 应用MainAbility.ts文件 +│ ├── common +│ ├── constant # 一些常量类文件 +│ ├── database # 数据库操作类 +│ ├── entity # ServiceAbility方法 +│ ├── profiler # 采集逻辑封装目录 +│ ├── ui # 嵌套子页面目录 +│ ├── uitls # 工具类目录 +│ ├── pages +│ ├── AppSelectPage.ets # 应用选择页面 +│ ├── CPU0LineChartPage.ets # CPU折线图页面 +│ ├── LightAdjust.ets # 亮度调整页面 +│ ├── LoginPage.ets # 登录首页面 +│ ├── MainPage.ets # 主页面(包含主页、报告、我的三个子页面) +│ ├── ReportDetail.ets # 报告详情页页面 +│ .... # 其他一些页面 +│ ├── workers +│ ├── worker.js # 负责socket通信的worker线程 +│ ├── resources # 资源配置文件存放目录 +| ├── base # 默认图片资源,字体大小,颜色资源存放目录 +├── signature +| ├── openharmony_smartperf.p7b # 应用签名文件 +├── BUILD.gn # 应用打包配置文件 +├── README_zh.md # readme文件 +``` + +## 约束条件 +**1. SmartPerf应用在3.2系统版本后开始预制使用。**
+**2. 使用设备必须是具备屏幕的设备。**
+## 功能特性 +**1. 采集指标说明**
| 采集项 | 说明 | | :----- | :--------------------- | -| FPS | 1秒内游戏画面或者应用界面真实刷新次数 | -| CPU频率 | 每1秒读取一次设备节点下各CPU的频点信息 | -| CPU负载 | 每1秒读取一次设备节点下各CPU的负载信息 | -| GPU频率 | 每1秒读取一次设备节点下GPU的频点信息 | -| GPU负载 | 每1秒读取一次设备节点下GPU的负载信息 | -| DDR频点 |每1秒读取一次设备节点下DDR的频点信息 | -| RAM |每1秒读取一次设备节点下DDR的频点信息 | -| 温度 | 每1秒读取一次读取一次设备节点下的soc温度等信息 | -| 电流 | 每1秒读取一次设备节点下的电流信息 | -| 电压 | 每1秒读取一次设备节点下电池的电压信息 | -| 截图 |每1秒截取一张截图 | -|trace采集|当帧绘制时间超过100ms以上会自动抓取trace,1min内只抓取1次 | - - -**2、计算指标说明**
+| FPS | 1秒内游戏画面或者应用界面真实刷新次数。 | +| CPU频率 | 每1秒读取一次设备节点下各CPU的频点信息。 | +| CPU负载 | 每1秒读取一次设备节点下各CPU的负载信息。 | +| GPU频率 | 每1秒读取一次设备节点下GPU的频点信息。 | +| GPU负载 | 每1秒读取一次设备节点下GPU的负载信息。 | +| DDR频点 | 每1秒读取一次设备节点下DDR的频点信息。 | +| RAM | 每1秒读取一次应用进程的实际物理内存。 | +| 温度 | 每1秒读取一次读取一次设备节点下的soc温度等信息。 | +| 电流 | 每1秒读取一次设备节点下的电流信息。 | +| 电压 | 每1秒读取一次设备节点下电池的电压信息。 | +| 截图 |每1秒截取一张截图。 | +|trace采集|当帧绘制时间超过100ms以上会自动抓取trace,1min内只抓取1次。 | + + +**2. 计算指标说明**
| 计算指标 | 说明 | | :----- | :--------------------- | | 归一化电流 | 归一化电流=电压 * 电流 / 3.8。 | -| 平均帧率 | 有效时间内的帧率之和除以时间 | -| 最高帧率 | 测试数据中帧率的最大值 | -| 低帧率 | 根据测试数据帧率数据最大数据计算满帧,然后满帧对应的的低帧基线,计算低于基线数量除以总的帧率个数 | -| 抖动率 | 根据测试数据帧率数据的最大数据计算满帧,按照满帧对应抖动率基线,然后计算得到抖动率 | -| 卡顿次数 | 目前卡顿次数依赖于每帧的绘制时间,帧绘制时间>100ms的计算为卡顿 | +| 平均帧率 | 有效时间内的帧率之和除以时间。 | +| 最高帧率 | 测试数据中帧率的最大值。 | +| 低帧率 | 根据测试数据帧率数据最大数据计算满帧,按照满帧对应低帧率基线,然后计算得到低帧率。 | +| 抖动率 | 根据测试数据帧率数据的最大数据计算满帧,按照满帧对应抖动率基线,然后计算得到抖动率。 | +| 卡顿次数 | 目前卡顿次数依赖于每帧的绘制时间,帧绘制时间>100ms的计算为卡顿。 | -**3、原始数据说明**
+**3. 原始数据说明**
| 原始数据 | 说明 | | :----- | :--------------------- | -| timestamp | 当前时间戳,对应于采集时间 | -| taskId | 任务Id,对应于网站端报告的任务ID| -| cpu0Frequency | cpu0核心的频率 单位一般是HZ| -| cpu0Load | cpu0核心的负载占比,计算得出 单位%| -| gpuFrequency | gpu频率| -| gpuLoad | gpu负载占比 单位%| -| ddrFrequency | ddr频率 | -| socThermalTemp | soc温度| -| gpuTemp | gpu温度| -| batteryTemp |电池温度| -| currentNow | 当前读到的电流值,单位一般是mA| -| voltageNow | 当前读到的电压值,单位一般是uV(微伏)| -| pss | 应用实际使用的物理内存, 单位一般是KB| -| fps | 帧率| -| fpsJitters | 每一帧绘制间隔,单位ns| - - -## 四、其他一些说明 -**1、关于截图、FPS、RAM、trace采集能力**
-- 截图目录默认在/data/local/tmp/capture下。
- - **Trace抓取:当帧绘制时间超过100ms以上会自动抓取trace,1min内只抓取1次,目录在/data目录下,trace文件较大,建议定期清除。** - **文件名称为:mynewtrace + 当前采集s数 + s:mynewtrace950s.ftrace** - - -**2、fps采集不到如何解决**
-- 方法一:后台必须只能存在一个被测试应用,同应用不能挂许多后台(如果有多个,需清除所有应用,重新启动测试);
-- 方法二:执行hidumper -s 10是否出现connect error等error错误,有error报错的话(尝试kill 掉 hidumper_servic进程,系统会自动再次拉起)
+| timestamp | 当前时间戳,对应于采集时间。 | +| taskId | 任务Id,对应于网站端报告的任务ID。| +| cpu0Frequency | cpu0核心的频率,单位一般是HZ。| +| cpu0Load | cpu0核心的负载占比,计算得出 单位%。| +| gpuFrequency | gpu频率。| +| gpuLoad | gpu负载占比,单位%。| +| ddrFrequency | ddr频率。 | +| socThermalTemp | soc温度。| +| gpuTemp | gpu温度。| +| batteryTemp |电池温度。| +| currentNow | 当前读到的电流值,单位一般是mA。| +| voltageNow | 当前读到的电压值,单位一般是uV(微伏)。| +| pss | 应用实际使用的物理内存,.单位一般是KB。| +| fps | 帧率。| +| fpsJitters | 每一帧绘制间隔,单位ns。| + +**4. 使用操作流程**
+步骤1:进入shell中查看SP_daemon进程是否存在,没有则需启动SP_daemon进程(SP_daemon用于采集fps、ram等指标), 然后打开桌面的Smartperf工具。(采集指标少的时候一般为SP_daemon未启动,或者被后台清理)
+ +查看SP_daemon进程是否存在 +``` +C:\Users\test>hdc_std shell +# ps -ef | grep SP_daemon +root 4707 1 0 09:41:12 ? 00:00:00 SP_daemon +root 4711 4704 25 09:41:19 pts/0 00:00:00 grep SP_daemon +# + +``` +如果SP_daemon进程不存在启动SP_daemon +``` +C:\Users\test>hdc_std shell +# ps -ef | grep SP_daemon +root 4720 4718 6 09:48:43 pts/0 00:00:00 grep SP_daemon +# SP_daemon +# ps -ef | grep SP_daemon +root 4723 1 0 09:48:50 ? 00:00:00 SP_daemon +root 4727 4718 5 09:48:53 pts/0 00:00:00 grep SP_daemon +# +``` +步骤2:点击桌面SmartPerf应用图标启动应用,然后点击登录,进入首页,点击开始测试页,选择被测应用,配置采集项(目前支持CPU、GPU、FPS、POWER、TEMP、RAM、截图能力、trace)。
+ +步骤3:选择应用后,点击开始测试、启动悬浮窗(左上角展示),会自动拉起应用,左上角悬浮窗展示start后点击即可开始,展示会变为计时状态,显示当前采集计时。(提示:单击暂停、继续任务,双击展开、关闭详情悬浮窗、点击详情悬浮窗后任意采集项会展开相应采集折线图)。
+ +步骤4:长按计时器,可以保存采集项数据,悬浮框消失。
+ +步骤5:报告列表页查看报告,原始数据可在概览页对应路径自行使用hdc命令pull出来。 + + +**5. 其他一些说明**
+ 1)截图目录默认在/data/local/tmp/capture下。
+ 2)Trace抓取:当帧绘制时间超过100ms以上会自动抓取trace,1min内只抓取1次,目录在/data目录下,trace文件较大,建议定期清除(文件名称为:mynewtrace[当前时间戳].ftrace)。
+ 3) fps采集不到如何解决
+ 方法一:后台只能存在一个被测试应用,同应用不能运行多个后台(如果有多个,需清除所有应用,重新启动测试)。
+ 方法二:执行hidumper -s 10如出现connect error等错误,尝试kill 掉 hidumper_servic进程,系统会自动再次拉起。 + + +## 发布版本 +**3.2.0.0版本布内容:预制smartperf应用,支持以下功能:**
+1. 支持采集CPU、GPU、Temperature、Power、应用RAM、FPS等指标数据采集。
+2. 悬浮窗控制采集及实时展示数据。
+3. 本地图表展示。
+ + + + diff --git a/device/device_ui/build-profile.json5 b/device/device_ui/build-profile.json5 deleted file mode 100644 index 8da18f8..0000000 --- a/device/device_ui/build-profile.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - "app": { - "signingConfigs": [], - "compileSdkVersion": 9, - "compatibleSdkVersion": 9, - "products": [ - { - "name": "default", - }, - ], - }, - "modules": [ - { - "name": "entry", - "srcPath": "./entry", - "targets": [ - { - "name": "default", - "applyToProducts": [ - "default", - ], - }, - ], - }, - ], -} \ No newline at end of file diff --git a/device/device_ui/entry/build-profile.json b/device/device_ui/entry/build-profile.json new file mode 100644 index 0000000..aec46fb --- /dev/null +++ b/device/device_ui/entry/build-profile.json @@ -0,0 +1,14 @@ +{ + "apiType": "stageMode", + "buildOption": { + "arkEnable": true + }, + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} \ No newline at end of file diff --git a/device/device_ui/entry/build-profile.json5 b/device/device_ui/entry/build-profile.json5 deleted file mode 100644 index eeeb460..0000000 --- a/device/device_ui/entry/build-profile.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - "apiType": 'stageMode', - "buildOption": { - arkEnable: true, - "externalNativeOptions": { - "path": "./src/main/cpp/CMakeLists.txt", - "arguments": "-v -DOHOS_STL=c++_shared", - "abiFilters": [ - "armeabi-v7a", - ], - "cppFlags": "", - } - }, - "targets": [ - { - "name": "default", - }, - { - "name": "ohosTest", - } - ] -} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/cpp/CMakeLists.txt b/device/device_ui/entry/src/main/cpp/CMakeLists.txt deleted file mode 100644 index 96ab538..0000000 --- a/device/device_ui/entry/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) -project(XComponent) - -set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) - -include_directories(${NATIVERENDER_ROOT_PATH} - ${NATIVERENDER_ROOT_PATH}/include - ) -add_library(smartperf SHARED profiler.cpp gp_utils.cpp FPS.cpp RAM.cpp) -target_link_libraries(smartperf PUBLIC libace_napi.z.so libc++.a libhilog_ndk.z.so) \ No newline at end of file diff --git a/device/device_ui/entry/src/main/cpp/FPS.cpp b/device/device_ui/entry/src/main/cpp/FPS.cpp deleted file mode 100644 index 4fda886..0000000 --- a/device/device_ui/entry/src/main/cpp/FPS.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2022 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -#include -#include -#include -#include -#include "gp_utils.h" -#include "FPS.h" - -pthread_mutex_t FPS::mutex; -FPS *FPS::instance = nullptr; -FPS *FPS::getInstance() -{ - if (instance == nullptr) { - pthread_mutex_lock(&mutex); - if (instance == nullptr) { - instance = new FPS(); - } - pthread_mutex_unlock(&mutex); - } - return instance; -} -FPS::FPS() -{ - pthread_mutex_init(&mutex, nullptr); -} -void FPS::setPackageName(std::string pkgName) -{ - pkg_name = std::move(pkgName); -} -FpsInfo FPS::getFpsInfo() -{ - FpsInfo fpsInfoMax; - fpsInfoMax.fps = -1; - - std::string layerName; - - std::vector sps; - gpUtils::mSplit(this->pkg_name, ".", sps); - std::string addEndChar = "0"; - const int pNameLastPos = 2; - std::string pkgSuffix = sps[pNameLastPos]; - layerName = std::string( pkgSuffix.c_str()+ addEndChar); - if (pkgSuffix.find("camera")!=std::string::npos) { - layerName = std::string("RosenRenderXComponent"); - } - - FpsInfo fpsInfo = GetSurfaceFrameDataGB(layerName); - if (fpsInfo.fps > fpsInfoMax.fps) { - fpsInfoMax = fpsInfo; - } - return fpsInfoMax; -} -FpsInfo FPS::GetSurfaceFrameDataGB(std::string name) -{ - if (name == "") { - return FpsInfo(); - } - static std::map fps_map; - if (fps_map.count(name) == 0) { - FpsInfo tmp; - tmp.fps = 0; - tmp.pre_fps = 0; - fps_map[name] = tmp; - } - FpsInfo &fpsInfo = fps_map[name]; - fpsInfo.fps = 0; - FILE *fp; - char tmp[1024]; - tmp[0] = '\0'; - std::string cmd = "hidumper -s 10 -a \"fps " + name + "\""; - fp = popen(cmd.c_str(), "r"); - if (fp == nullptr) { - return fpsInfo; - } - long long MOD = 1e9; - long long lastReadyTime = -1; - int fps_gb = 0; - if (!(fpsInfo.time_stamp_q).empty()) { - lastReadyTime = (fpsInfo.time_stamp_q).back(); - } - bool jump = false; - bool refresh = false; - int cnt = 0; - int zeroNum = 0; - while (fgets(tmp, sizeof(tmp), fp) != nullptr) { - long long frameReadyTime = 0; - sscanf(tmp, "%lld", &frameReadyTime); - cnt++; - if (frameReadyTime == 0) { - zeroNum++; - continue; - } - if (lastReadyTime >= frameReadyTime) { - lastReadyTime = -1; - continue; - } - refresh = true; - long long t_frameReadyTime = frameReadyTime / MOD; - long long t_lastReadyTime = lastReadyTime / MOD; - long long lastFrame = -1; - if (t_frameReadyTime == t_lastReadyTime) { - (fpsInfo.time_stamp_q).push(frameReadyTime); - } else if (t_frameReadyTime == t_lastReadyTime + 1) { - jump = true; - lastFrame = fpsInfo.last_frame_ready_time; - lastReadyTime = frameReadyTime; - int fps_tmp = 0; - fpsInfo.jitters.clear(); - while (!(fpsInfo.time_stamp_q).empty()) { - fps_tmp++; - long long currFrame = (fpsInfo.time_stamp_q.front()); - if (lastFrame != -1) { - long long jitter = currFrame - lastFrame; - fpsInfo.jitters.push_back(jitter); - } - lastFrame = currFrame; - (fpsInfo.time_stamp_q).pop(); - } - fps_gb = fps_tmp; - (fpsInfo.time_stamp_q).push(frameReadyTime); - fpsInfo.last_frame_ready_time = lastFrame; - } else if (t_frameReadyTime > t_lastReadyTime + 1) { - jump = true; - lastReadyTime = frameReadyTime; - while (!(fpsInfo.time_stamp_q).empty()) { - (fpsInfo.time_stamp_q).pop(); - } - (fpsInfo.time_stamp_q).push(frameReadyTime); - } - } - pclose(fp); - const int maxZeroNum = 120; - if (zeroNum >= maxZeroNum) { - while (!(fpsInfo.time_stamp_q.empty())) { - fpsInfo.time_stamp_q.pop(); - } - fpsInfo.fps = 0; - return fpsInfo; - } - const int minPrintLine = 5; - if (cnt < minPrintLine) { - fpsInfo.fps = fpsInfo.pre_fps; - return fpsInfo; - } - if (fps_gb > 0) { - fpsInfo.fps = fps_gb; - fpsInfo.pre_fps = fps_gb; - return fpsInfo; - } else if (refresh && !jump) { - fpsInfo.fps = fpsInfo.pre_fps; - return fpsInfo; - } else { - fpsInfo.fps = 0; - return fpsInfo; - } -} - - diff --git a/device/device_ui/entry/src/main/cpp/gp_utils.cpp b/device/device_ui/entry/src/main/cpp/gp_utils.cpp deleted file mode 100644 index 3845f21..0000000 --- a/device/device_ui/entry/src/main/cpp/gp_utils.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2022 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ -#include -#include -#include -#include -#include "gp_utils.h" - -#define LOG_DOMAIN 0x200 // 标识业务领域,范围0x0~0xFFFFF -#define LOG_TAG "gpUtils" - -namespace gpUtils { - void mSplit(const std::string &content, const std::string &sp, std::vector &out) { - int index = 0; - while (index != std ::string::npos) { - int t_end = content.find_first_of(sp, index); - std::string tmp = content.substr(index, t_end - index); - if (tmp != "" && tmp != " ") - out.push_back(tmp); - if (t_end == std::string::npos) - break; - index = t_end + 1; - } - } - - bool canOpen(const std::string &path) { - FILE* fp; - fp = fopen(path.c_str(), "r"); - if(fp == nullptr){ - return false; - } - fclose(fp); - return true; - } - - bool canCmd(const std::string &cmd) { - FILE* pp; - pp = popen(cmd.c_str(), "r"); - if(pp == nullptr){ - return false; - } - pclose(pp); - return true; - } -} -// popen -std::string gpUtils::readCmd(const std::string &cmd) -{ - const int buffLength = 1024; - std::string res = "NA"; - FILE *pp = popen(cmd.c_str(), "r"); - if (pp == nullptr) { - return res; - } else { - char line[buffLength]; - line[0] = '\0'; - while (fgets(line, buffLength, pp) != nullptr) { - res = std::string(line); - } - } - pclose(pp); - return res; -} - -// fopen -std::string gpUtils::readFile(const std::string &path) -{ - std::string res = "NA"; - const int buffLengh = 1024; - FILE *fp; - if ((fp = fopen(path.c_str(), "r")) != nullptr) { - char s[buffLengh]; - s[0] = '\0'; - while (fgets(s, sizeof(s), fp) != nullptr) { - res += std::string(s); - } - } - if (fp != nullptr) { - fclose(fp); - } - return res; -} -// get number_str from str -std::string gpUtils::extractNumber(const std::string &str) -{ - int cntInt = 0; - const int shift = 10; - - for (int i = 0; str[i] != '\0'; ++i) { - if (str[i] >= '0' && str[i] <= '9') { - cntInt *= shift; - cntInt += str[i] - '0'; - } - } - return std::to_string(cntInt); -} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/cpp/profiler.cpp b/device/device_ui/entry/src/main/cpp/profiler.cpp deleted file mode 100644 index f96aba3..0000000 --- a/device/device_ui/entry/src/main/cpp/profiler.cpp +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2022 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "napi/native_api.h" -#include "gp_utils.h" - -#define LOG_DOMAIN 0x200 // 标识业务领域,范围0x0~0xFFFFF -#define LOG_TAG "PROFILER" - -namespace { - void collectFpsThread(std::promise &promiseObj){ - FpsInfo fpsInfo = FPS::getInstance()->getFpsInfo(); - promiseObj.set_value(fpsInfo); - } -} - - -static napi_value getFpsData(napi_env env, napi_callback_info info) -{ - size_t argc = 1; - napi_value args[1] = { nullptr }; - napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); - napi_valuetype valuetype0; - napi_typeof(env, args[0], &valuetype0); - char pkgName[64] = {0}; - size_t typeLen = 0; - napi_get_value_string_utf8(env, args[0], pkgName, 63, &typeLen); - FPS::getInstance()->setPackageName(pkgName); - - std::promise promiseObj; - std::future futureObj = promiseObj.get_future(); - std::thread tFps(collectFpsThread, ref(promiseObj)); - tFps.join(); - - FpsInfo fpsInfo = futureObj.get(); - std::string fps = std::to_string(fpsInfo.fps); - std::vector fpsJitters = fpsInfo.jitters; - std::string fps_str = fps + "|"; - for (int i = 0; i < fpsJitters.size(); ++i) { - fps_str += std::to_string(fpsJitters[i]); - fps_str += "=="; - } - napi_value fps_result; - napi_create_string_utf8(env, fps_str.c_str(), fps_str.size(), &fps_result); - return fps_result; -} - -static napi_value getRamData(napi_env env, napi_callback_info info) -{ - size_t argc = 1; - napi_value args[1] = { nullptr }; - napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); - napi_valuetype valuetype0; - napi_typeof(env, args[0], &valuetype0); - char pidNumber[64] = {0}; - size_t typeLen = 0; - napi_get_value_string_utf8(env, args[0], pidNumber, 63, &typeLen); - std::map gramInfo = RAM::getInstance()->getRamInfo(pidNumber); - std::string ram_pss = gramInfo["pss"]; - napi_value ram_result; - napi_create_string_utf8(env, ram_pss.c_str(), ram_pss.size(), &ram_result); - return ram_result; -} - -static napi_value checkDaemon(napi_env env, napi_callback_info info) -{ - std::string status = "Dead"; - std::string spRunning = gpUtils::readCmd(std::string("ps -ef |grep SP_daemon |grep -v grep")); - if (spRunning.find("NA")!= std::string::npos) { - gpUtils::canCmd(std::string("SP_daemon")); - } else { - status = "Running"; - } - napi_value result; - napi_create_string_utf8(env, status.c_str(), status.size(), &result); - return result; -} - -static napi_value checkAccess(napi_env env, napi_callback_info info) -{ - size_t argc = 1; - napi_value args[1] = { nullptr }; - napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); - napi_valuetype valuetype0; - napi_typeof(env, args[0], &valuetype0); - char pathName[64] = {0}; - size_t typeLen = 0; - napi_get_value_string_utf8(env, args[0], pathName, 63, &typeLen); - std::string pathNameStr = pathName; - std::string status = "PermissionDenied"; - bool isAccess = gpUtils::canOpen(pathNameStr); - if (isAccess) { - status = "PermissionAccessed"; - } - napi_value result; - napi_create_string_utf8(env, status.c_str(), status.size(), &result); - return result; -} - -EXTERN_C_START -static napi_value Init(napi_env env, napi_value exports) -{ - napi_property_descriptor desc[] = { - { "getFpsData", nullptr, getFpsData, nullptr, nullptr, nullptr, napi_default, nullptr }, - { "getRamData", nullptr, getRamData, nullptr, nullptr, nullptr, napi_default, nullptr }, - { "checkDaemon", nullptr, checkDaemon, nullptr, nullptr, nullptr, napi_default, nullptr }, - { "checkAccess", nullptr, checkAccess, nullptr, nullptr, nullptr, napi_default, nullptr }, - }; - napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); - return exports; -} -EXTERN_C_END - -static napi_module demoModule = { -.nm_version = 1, -.nm_flags = 0, -.nm_filename = nullptr, -.nm_register_func = Init, -.nm_modname = "libsmartperf", -.nm_priv = ((void *)0), -.reserved = { 0 }, -}; - -extern "C" __attribute__((constructor)) void RegisterModule(void) -{ -napi_module_register(&demoModule); -} diff --git a/device/device_ui/entry/src/main/cpp/types/libsmartperf/package.json b/device/device_ui/entry/src/main/cpp/types/libsmartperf/package.json deleted file mode 100644 index 3af17c2..0000000 --- a/device/device_ui/entry/src/main/cpp/types/libsmartperf/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "libsmartperf.so", - "types": "./index.d.ts" -} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/MainAbility/MainAbility.ts b/device/device_ui/entry/src/main/ets/MainAbility/MainAbility.ts index b085e60..0614b86 100644 --- a/device/device_ui/entry/src/main/ets/MainAbility/MainAbility.ts +++ b/device/device_ui/entry/src/main/ets/MainAbility/MainAbility.ts @@ -16,65 +16,46 @@ import Ability from '@ohos.application.Ability' import { initDb } from '../common/database/LocalRepository' import { FloatWindowFun } from '../common/ui/floatwindow/FloatWindowFun' -import { NativeTaskFun } from "../common/profiler/NativeTaskFun" import { NetWork } from '../common/profiler/item/NetWork'; import BundleManager from '../common/utils/BundleMangerUtils'; -import SPLogger from '../common/utils/SPLogger' +import WorkerHandler from '../common/profiler/WorkerHandler'; +import worker from '@ohos.worker'; +let MainWorker = new worker.Worker("/entry/ets/workers/worker.js") +globalThis.MainWorker = MainWorker -var abilityWindowStage -const TAG = "MainAbility" +MainWorker.onmessage = function (result) { + WorkerHandler.socketHandler(result) +} +var abilityWindowStage export default class MainAbility extends Ability { - onCreate(want, launchParam) { globalThis.showFloatingWindow=false - // Ability is creating, initialize resources for this ability BundleManager.getAppList().then(appList => { globalThis.appList = appList - }) - SPLogger.DEBUG(TAG,"getReportListDb" + "--> MainAbility createDd 1") } - onDestroy() { - // Ability is destroying, release resources for this ability - SPLogger.DEBUG(TAG,"[MyApplication] MainAbility onDestroy") + MainWorker.terminate() } - onWindowStageCreate(windowStage) { globalThis.abilityContext = this.context - - // Main window is created, set main page for this ability - SPLogger.DEBUG(TAG,"[MyApplication] MainAbility onWindowStageCreate") abilityWindowStage = windowStage; abilityWindowStage.setUIContent(this.context, "pages/LoginPage", null) + globalThis.useDaemon = false } - - onWindowStageDestroy() { - // Main window is destroyed, release UI related resources - SPLogger.DEBUG(TAG,"[MyApplication] MainAbility onWindowStageDestroy") - } - + onWindowStageDestroy() {} onForeground() { initDb() - // Ability has brought to foreground FloatWindowFun.initAllFun() - NativeTaskFun.initAllFun() - //check netWork NetWork.getInstance().init() + MainWorker.postMessage({ "testConnection": true }) } - - onBackground() { - // Ability has back to background - SPLogger.DEBUG(TAG,"[MyApplication] MainAbility onBackground") - } + onBackground() {} }; -export function initTest() { - -} diff --git a/device/device_ui/entry/src/main/ets/common/FloatWindowComponent.ets b/device/device_ui/entry/src/main/ets/common/FloatWindowComponent.ets index 09e24e6..aa68615 100644 --- a/device/device_ui/entry/src/main/ets/common/FloatWindowComponent.ets +++ b/device/device_ui/entry/src/main/ets/common/FloatWindowComponent.ets @@ -13,7 +13,7 @@ * limitations under the License. */ -import {moveFloatWindow,setFloatWindow} from './ui/floatwindow/utils/floatwindowutils' +import {moveFloatWindow,setFloatWindow} from './ui/floatwindow/utils/FloatWindowUtils' @@ -52,11 +52,13 @@ export struct FloatWindowComponent { //Y轴 this.context.clearRect(this.XPoint + 0.5, this.YPoint - this.YLength, this.XLength, this.YLength) this.context.beginPath() + this.context.strokeStyle="#ffffff" this.context.moveTo(this.XPoint, this.YPoint - this.YLength) this.context.lineTo(this.XPoint, this.YPoint) this.context.stroke() //X轴 this.context.beginPath() + this.context.strokeStyle="#ffffff" this.context.moveTo(this.XPoint, this.YPoint) this.context.lineTo(this.XPoint + this.XLength, this.YPoint) this.context.stroke() @@ -64,6 +66,7 @@ export struct FloatWindowComponent { if (this.data.length > 1) { for (let i = 1; i < this.data.length; i++) { this.context.beginPath() + this.context.strokeStyle="#ffffff" console.log("GestureEvent--------------beginPath:" + this.data[i-1]); this.context.moveTo(this.XPoint + (i - 1) * this.XScale, this.YPoint - this.data[i-1]) this.context.lineTo(this.XPoint + i * this.XScale, this.YPoint - this.data[i]) diff --git a/device/device_ui/entry/src/main/ets/common/database/DatabaseUtils.ts b/device/device_ui/entry/src/main/ets/common/database/DatabaseUtils.ts index 1881f30..a733165 100644 --- a/device/device_ui/entry/src/main/ets/common/database/DatabaseUtils.ts +++ b/device/device_ui/entry/src/main/ets/common/database/DatabaseUtils.ts @@ -30,9 +30,9 @@ export default { dataRdb.getRdbStore(globalThis.abilityContext, STORE_CONFIG_Index, dbVersion) .then(rdbStore => { rdbStore.executeSql(sql_t_index_info, null).catch(err => { - SPLogger.DEBUG(TAG,"--> createTable t_index_info err:" + err) +// SPLogger.DEBUG(TAG,"--> createTable t_index_info err:" + err) }) - SPLogger.DEBUG(TAG,"--> createTable start execute sql_t_index_info:" + sql_t_index_info) +// SPLogger.DEBUG(TAG,"--> createTable start execute sql_t_index_info:" + sql_t_index_info) return rdbStore }) }, @@ -77,11 +77,11 @@ export default { name: dbName } dataRdb.getRdbStore(globalThis.abilityContext, STORE_CONFIG, dbVersion, (err, rdbStore) => { - SPLogger.DEBUG(TAG,"--> insert into insertGeneraData :tableName:" + tableName + "| valueInsert:" + JSON.stringify(valueInsert)) +// SPLogger.DEBUG(TAG,"--> insert into insertGeneraData :tableName:" + tableName + "| valueInsert:" + JSON.stringify(valueInsert)) rdbStore.insert(tableName, valueInsert).catch(err => { - SPLogger.DEBUG(TAG,"--> insert into insertGeneraData err:" + err) +// SPLogger.DEBUG(TAG,"--> insert into insertGeneraData err:" + err) }).then(() => { - SPLogger.DEBUG(TAG,"--> insert into insertGeneraData success:") +// SPLogger.DEBUG(TAG,"--> insert into insertGeneraData success:") }) }) }, @@ -185,7 +185,7 @@ export default { }) } catch (err) { - SPLogger.ERROR(TAG,"resultSet queryGeneralData:err" + err) +// SPLogger.ERROR(TAG,"resultSet queryGeneralData:err" + err) } }, @@ -276,7 +276,7 @@ export default { return results }) } catch (err) { - SPLogger.ERROR(TAG,"resultSet queryIndexInfo Data:err" + err) +// SPLogger.ERROR(TAG,"resultSet queryIndexInfo Data:err" + err) } }, /** diff --git a/device/device_ui/entry/src/main/ets/common/entity/LocalConfigEntity.ts b/device/device_ui/entry/src/main/ets/common/entity/LocalConfigEntity.ts index 55bb4bb..9d5c46f 100644 --- a/device/device_ui/entry/src/main/ets/common/entity/LocalConfigEntity.ts +++ b/device/device_ui/entry/src/main/ets/common/entity/LocalConfigEntity.ts @@ -131,8 +131,6 @@ export const otherSupportList = new Array( export let switchList = new Array( new SwitchItem("trace", '是否抓取trace', $r("app.media.icon_average_frame_b"), false, true), - new SwitchItem("is_camera", '是否为相机应用', $r("app.media.icon_camera"), false, true), - new SwitchItem("is_video", '是否为视频应用', $r("app.media.icon_video"), false, true), new SwitchItem("screen_capture", '是否开启截图', $r("app.media.icon_screencap"), false, true) ) @@ -147,23 +145,26 @@ export class QuestionItem { } export const questionList = new Array( - new QuestionItem('1.SP工具支持FPS采集吗?', '可以,fps依赖Hidumper能力..'), - new QuestionItem('2.SP工具支持RAM采集吗?', 'ram采集目前是 读取进程节点内存信息中的PSS值...'), - new QuestionItem('3.FPS采集不到?', '可能是视频应用,需要联系开发添加对应的图层,做采集适配'), - new QuestionItem('4.SP采集原理?', '目前除fps外,其他采集均是通过cat 系统节点获取'), - new QuestionItem('5.报告页的值是怎么算的?', '最终以一场测试结果的平均值为准'), - new QuestionItem('6.SP后续规划?', '集成更多采集能力,如trace采集,counter采集,网络采集等等;优化数据展示方式,报告上传网站端,在线分析性能功耗问题') + new QuestionItem('1.SP工具怎么使用', '如何使用可以查看以下地址:https://gitee.com/openharmony/developtools_profiler/blob/master/host/smartperf/client/client_ui/README_zh.md'), + new QuestionItem('2.SP工具支持FPS采集吗?', '可以,fps依赖Hidumper能力..'), + new QuestionItem('3.SP工具支持RAM采集吗?', 'ram采集目前是 读取进程节点内存信息中的PSS值...'), + new QuestionItem('4.FPS采集不到?', '可能是视频应用,需要联系开发添加对应的图层,做采集适配'), + new QuestionItem('5.SP采集原理?', '目前除fps外,其他采集均是通过cat 系统节点获取'), + new QuestionItem('6.报告页的值是怎么算的?', '最终以一场测试结果的平均值为准'), + new QuestionItem('7.SP后续规划?', '集成更多采集能力,如trace采集,counter采集,网络采集等等;优化数据展示方式,报告上传网站端,在线分析性能功耗问题') ) export class SummaryItem { public icon: Resource public content: string public value: string + public backColor: string - constructor(icon: Resource, content: string, value: string) { + constructor(icon: Resource, content: string, value: string, backColor: string) { this.icon = icon this.content = content this.value = value + this.backColor = backColor } } diff --git a/device/device_ui/entry/src/main/ets/common/profiler/ProfilerTask.ets b/device/device_ui/entry/src/main/ets/common/profiler/ProfilerTask.ets index 6aaa9a7..1adc521 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/ProfilerTask.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/ProfilerTask.ets @@ -14,13 +14,12 @@ * limitations under the License. */ import deviceInfo from '@ohos.deviceInfo' -import CommonEvent from '@ohos.commonevent' -import database from '../database/databaseutils' -import { getPidOfAbility } from '../utils/SystemUtils'; +import CommonEvent from '@ohos.commonEvent' +import database from '../database/DatabaseUtils' import { CPU } from './item/CPU'; -import { csvGeneralInfo, csvTIndexInfo } from '../utils/csvutils' -import { createFilePath } from '../utils/ioutils' -import { GPData, TIndexInfo, TGeneralInfo } from '../entity/databaseentity' +import { csvGeneralInfo, csvTIndexInfo } from '../utils/CSVUtils' +import { createFilePath } from '../utils/IOUtils' +import { GPData, TIndexInfo, TGeneralInfo } from '../entity/DatabaseEntity' import { ProfilerFactory } from './base/ProfilerFactory' import SPLogger from '../../common/utils/SPLogger' @@ -49,20 +48,14 @@ export class ProfilerTask { } } } - //默认不使用daemon - globalThis.useDaemon = false - let status = globalThis.CheckDaemon() - if (status == "Running") { - globalThis.useDaemon = true - } - SPLogger.DEBUG(ProfilerTask.name,'ProfilerTask initModule:Daemon status ' + status) } getSupports(keys:string[]){ - let supports = new Map + let supports : Map = new Map() for (var key of keys) { let typeCollect = ProfilerFactory.getProfilerByConfig(key) - supports.set(key.toString(),typeCollect.isSupport()) + let isSupport = typeCollect.isSupport() + supports.set(key.toString(),isSupport) } return supports } @@ -88,10 +81,7 @@ export class ProfilerTask { globalThis.fpsArr = new Array() // fpsJitter队列 globalThis.fpsJitterArr = new Array() - //初始化采集目标进程pid - getPidOfAbility(globalThis.collectPkg).then(pid=>{ - globalThis.processPid = pid - }) + //初始化数据库 2022-02-23 dbName改为时间戳 database.createTable(globalThis.dbTime) SPLogger.DEBUG(ProfilerTask.name,'ProfilerUtils taskInit called'); @@ -106,7 +96,7 @@ export class ProfilerTask { if (typeCollect != null) { let gpData:GPData =typeCollect.readData() if(typeCollect instanceof CPU){ - typeCollect.readCPULoad() + gpDataArr.push(typeCollect.readCPULoad()) } gpDataArr.push(gpData) SPLogger.DEBUG(ProfilerTask.name,"profiler ProfilerTask:curData:"+gpData); @@ -119,7 +109,7 @@ export class ProfilerTask { let tTndex = database.gpArray2Index(gpDataArr) globalThis.tTndexs.push(tTndex) globalThis.tTndex = tTndex; - CommonEvent.publish("event", { code: 0, data: tTndex.fps + "", }, (err)=>{}); + CommonEvent.publish("event", { code: 0, data: JSON.stringify(tTndex), }, (err)=>{}); } globalThis.collectIntervalNum++ SPLogger.DEBUG(ProfilerTask.name,"profiler globalThis.collectIntervalNum " + globalThis.collectIntervalNum) diff --git a/device/device_ui/entry/src/main/ets/common/profiler/WorkerHandler.ts b/device/device_ui/entry/src/main/ets/common/profiler/WorkerHandler.ts new file mode 100644 index 0000000..96aa9a8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/profiler/WorkerHandler.ts @@ -0,0 +1,45 @@ +import { CatchTraceStatus } from './base/ProfilerConstant' +export default class WorkerHandler{ + + static socketHandler(result){ + let arr = result.data.split("$") + switch (arr[0]) { + case "RAM": + globalThis.ramArr.push(arr[1]) + break; + case "FPS": + globalThis.fpsArr.push(arr[1]) + globalThis.fpsJitterArr.push(arr[2]) + globalThis.timerFps = arr[1] + if ( + globalThis.catchTraceState == CatchTraceStatus.catch_trace_start || + globalThis.catchTraceState == CatchTraceStatus.catch_trace_finish || + globalThis.catchTraceState == CatchTraceStatus.catch_trace_first_running) { + if (globalThis.fpsJitterArr != undefined && globalThis.fpsJitterArr != null && globalThis.fpsJitterArr != "") { + let tempQueue: Array = globalThis.fpsJitterArr + let curJitter = tempQueue.pop() + let tempJitterArr = curJitter.split("==") + for (var i = 0; i < tempJitterArr.length; i++) { + let tmp = tempJitterArr[i] + let jitter = parseInt(tmp) / 1e6 + if (jitter > 100) { + globalThis.jitterTrace = true + return + } + } + } + } + break; + case "UdpStatus": + let isSocketConnect = Number(arr[1]).valueOf() + if (isSocketConnect > 0) { + globalThis.useDaemon = true + }else{ + globalThis.useDaemon = false + } + default: + break; + } + } + +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfiler.ets b/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfiler.ets index 78fcd09..e3a0096 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfiler.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfiler.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { GPData } from '../../entity/databaseentity'; +import { GPData } from '../../entity/DatabaseEntity'; import { CollectorType } from './ProfilerConstant' export abstract class BaseProfiler { diff --git a/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfilerUtils.ets b/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfilerUtils.ets index 8077764..f802438 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfilerUtils.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/base/BaseProfilerUtils.ets @@ -14,8 +14,8 @@ */ import fileio from '@ohos.fileio'; import util from '@ohos.util'; -import { GPData } from '../../entity/databaseentity'; -import SPLogger from '../../../common/utils/SPLogger' +import { GPData } from '../../entity/DatabaseEntity'; +import SPLogger from '../../utils/SPLogger'; const BUFF_SIZE = 1024 const TAG="BaseProfilerUtils" @@ -29,10 +29,12 @@ export function createGPData(moduleType: string, values: Map): G export function isAccess(path: string):boolean{ + SPLogger.DEBUG("BaseProfilerUtils","ets check path is start..."+ "path:" + path); var isAccess = false try { fileio.accessSync(path); isAccess = true + SPLogger.DEBUG("BaseProfilerUtils","ets check path is finish..."+ "path:" + path); } catch(err) { SPLogger.DEBUG(TAG,"accessSync failed with error:"+ err + "path:" + path); } diff --git a/device/device_ui/entry/src/main/ets/common/profiler/base/ProfilerFactory.ets b/device/device_ui/entry/src/main/ets/common/profiler/base/ProfilerFactory.ets index dbe7d78..3745865 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/base/ProfilerFactory.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/base/ProfilerFactory.ets @@ -27,28 +27,28 @@ import SPLogger from '../../../common/utils/SPLogger' export class ProfilerFactory { static getProfilerByConfig(moduleName: string): BaseProfiler{ if (moduleName == CollectorType.TYPE_FPS) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_FPS" ); + return FPS.getInstance() } else if (moduleName == CollectorType.TYPE_CPU) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_CPU"); + return CPU.getInstance() } else if (moduleName == CollectorType.TYPE_GPU) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_GPU" ); + return GPU.getInstance() } else if (moduleName == CollectorType.TYPE_POWER) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_POWER" ); + return Power.getInstance() } else if (moduleName == CollectorType.TYPE_RAM) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_RAM" ); + return RAM.getInstance() } else if (moduleName == CollectorType.TYPE_TEMPERATURE) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_TEMPERATURE" ); + return Thermal.getInstance() } else if (moduleName == CollectorType.TYPE_DDR) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_DDR"); + return DDR.getInstance() }else if (moduleName == CollectorType.TYPE_NET) { - SPLogger.DEBUG(ProfilerFactory.name,"getProfilerByConfig:TYPE_NET"); + return NetWork.getInstance() } return null diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/CPU.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/CPU.ets index e475360..46397a9 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/item/CPU.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/item/CPU.ets @@ -16,8 +16,8 @@ import fileio from '@ohos.fileio'; import { fileOpen, travelFile, createGPData, isAccess} from '../base/BaseProfilerUtils'; import { BaseProfiler } from '../base/BaseProfiler' import { CollectorType } from '../base/ProfilerConstant' -import { GPData } from '../../entity/databaseentity'; -import SPLogger from '../../../common/utils/SPLogger' +import { GPData } from '../../entity/DatabaseEntity' +import SPLogger from '../../utils/SPLogger' enum CPU_CONFIG { CPU_BASE = "/sys/devices/system/cpu", CPU_CUR_FREQ = "/cpufreq/scaling_cur_freq", @@ -44,10 +44,11 @@ export class CPU extends BaseProfiler { } isSupport(){ - if(isAccess(CPU_CONFIG.CPU_BASE)&&isAccess(CPU_CONFIG.CPU_LOAD)){ - if (globalThis.checkAccess(CPU_CONFIG.CPU_LOAD) == "PermissionAccessed") { - return true - } +// if(isAccess(CPU_CONFIG.CPU_BASE)&&isAccess(CPU_CONFIG.CPU_LOAD)){ +// return true +// } + if (globalThis.useDaemon) { + return true } return false } @@ -57,7 +58,7 @@ export class CPU extends BaseProfiler { const path = CPU_CONFIG.CPU_BASE + "/cpu" + i + CPU_CONFIG.CPU_CUR_FREQ var cpu = fileOpen(path) this.cpuMap.set("cpu" + i + "Freq", cpu) - SPLogger.DEBUG(CPU.name,"profile_readCpu cpu" + i + "freq:" + cpu); + } return createGPData("CPU", this.cpuMap) } @@ -93,7 +94,7 @@ export class CPU extends BaseProfiler { this.prebufferArr[index] = buffer } } catch (err) { - SPLogger.ERROR(CPU.name,"readCPULoad path:" + path + " error:" + err); + } finally { fileio.closeSync(fd) } diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/FPS.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/FPS.ets index 1fa4443..4ea5230 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/item/FPS.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/item/FPS.ets @@ -12,12 +12,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createGPData, extractNumber , isAccess} from '../base/BaseProfilerUtils'; +import { createGPData, extractNumber } from '../base/BaseProfilerUtils'; import { BaseProfiler } from '../base/BaseProfiler' import { CollectorType } from '../base/ProfilerConstant' import { SocketProfiler } from '../base/SocketProfiler' import SPLogger from '../../../common/utils/SPLogger' +import worker from '@ohos.worker'; +import WorkerHandler from '../WorkerHandler'; +let MainWorker = globalThis.MainWorker + +MainWorker.onmessage = function (result) { + WorkerHandler.socketHandler(result) +} + export class FPS extends BaseProfiler implements SocketProfiler { private fpsMap: Map = new Map @@ -35,33 +43,27 @@ export class FPS extends BaseProfiler implements SocketProfiler { } isSupport(){ - return true + if (globalThis.useDaemon) { + return true + } + return false } readData() { if (globalThis.useDaemon) { this.readMessageQueue() - } else { - let fpsStr: string = globalThis.CreateNativeFps(globalThis.collectPkg) - if (fpsStr.length > 0) { - let fpsArr = fpsStr.split("|") - this.fpsMap.set("fps", extractNumber(fpsArr[0])) - let fpsJitters = "\"" + fpsArr[1].split("==").join(",") + "\"" - this.fpsMap.set("fpsJitters", fpsJitters) - globalThis.fpsArr.push(fpsArr[0]) - globalThis.fpsJitterArr.push(fpsArr[1]) - globalThis.timerFps = fpsArr[0] - } } return createGPData("FPS", this.fpsMap) } readMessageQueue() { + MainWorker.postMessage({ "fps": true, "pkg": globalThis.collectPkg }) SPLogger.DEBUG(FPS.name, "messageQueue for fps" + globalThis.fpsArr.length) if (globalThis.fpsArr.length > 0) { let fpsQueue: Array = globalThis.fpsArr let fpsJitterQueue: Array = globalThis.fpsJitterArr let curFPS = fpsQueue.pop() + globalThis.timerFps = curFPS let curFPSJitter = fpsJitterQueue.pop() let fpsJitters = "\"" + curFPSJitter.split("==").join(",") + "\"" this.fpsMap.set("fpsJitters", fpsJitters) diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/GPU.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/GPU.ets index 1ebd21f..4336bc3 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/item/GPU.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/item/GPU.ets @@ -12,20 +12,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { fileOpen, createGPData, isAccess} from '../base/BaseProfilerUtils'; +import { fileOpen, createGPData, isAccess } from '../base/BaseProfilerUtils'; import { BaseProfiler } from '../base/BaseProfiler' import { CollectorType } from '../base/ProfilerConstant' -enum GPU_CONFIG { - GPU_FREQ_PATH = "/sys/class/devfreq/fde60000.gpu/cur_freq", //RK - GPU_LOAD_PATH = "/sys/class/devfreq/fde60000.gpu/load", //RK -// GPU_FREQ_PATH = "/sys/class/devfreq/gpufreq/cur_freq", // wgr -// GPU_LOAD_PATH = "/sys/class/devfreq/gpufreq/gpu_scene_aware/utilisation", // wgr +let GPU_CONFIG = { + GPU_FREQ_PATH: [ + "/sys/class/devfreq/fde60000.gpu/cur_freq", //RK + "/sys/class/devfreq/gpufreq/cur_freq" + ], + GPU_LOAD_PATH: [ + "/sys/class/devfreq/fde60000.gpu/load", //RK + "/sys/class/devfreq/gpufreq/gpu_scene_aware/utilisation" + ] } export class GPU extends BaseProfiler { private gpuMap: Map = new Map - public static instance: GPU = null public static getInstance() { if (this.instance == null) { @@ -38,27 +41,48 @@ export class GPU extends BaseProfiler { //初始化GPU return CollectorType.TYPE_GPU } - isSupport(){ - if(isAccess(GPU_CONFIG.GPU_FREQ_PATH)&&isAccess(GPU_CONFIG.GPU_LOAD_PATH)){ + + isSupport() { + let curFreqPath = undefined + let curLoadPath = undefined + GPU_CONFIG.GPU_FREQ_PATH.forEach(path => { + if (isAccess(path)) { + curFreqPath = path + } + }) + GPU_CONFIG.GPU_LOAD_PATH.forEach(path => { + if (isAccess(path)) { + curLoadPath = path + } + }) + if (curFreqPath != undefined && curLoadPath != undefined) { return true } return false } + readData() { - console.log("GPU:BaseProfiler called") - let gpuFreq = fileOpen(GPU_CONFIG.GPU_FREQ_PATH) - let gpuLoad = fileOpen(GPU_CONFIG.GPU_LOAD_PATH) - console.log("GPU:BaseProfiler called1111") + let gpuFreq:String = "" + let gpuLoad:String = "" + GPU_CONFIG.GPU_FREQ_PATH.forEach(path => { + if (isAccess(path)) { + gpuFreq = fileOpen(path) + } + }) + GPU_CONFIG.GPU_LOAD_PATH.forEach(path => { + if (isAccess(path)) { + gpuLoad = fileOpen(path) + } + }) let loadStr: string[] = gpuLoad.split("@") let load = "-1" if (loadStr.length > 0) { load = loadStr[0].toString() - }else{ + } else { load = gpuLoad.toString() } this.gpuMap.set("gpuFreq", gpuFreq) this.gpuMap.set("gpuLoad", load) - console.log("GPU:BaseProfiler called2222") return createGPData("GPU", this.gpuMap) } } diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/NetWork.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/NetWork.ts similarity index 100% rename from device/device_ui/entry/src/main/ets/common/profiler/item/NetWork.ets rename to device/device_ui/entry/src/main/ets/common/profiler/item/NetWork.ts diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/Power.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/Power.ets index 35d69f7..7ede63d 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/item/Power.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/item/Power.ets @@ -17,9 +17,8 @@ import { BaseProfiler } from '../base/BaseProfiler' import { CollectorType } from '../base/ProfilerConstant' import SPLogger from '../../../common/utils/SPLogger' enum POWER_CONFIG { - //此目录 fileio 无法读取 需自行适配 -// POWER_PATH = "/data/local/tmp/battery" - POWER_PATH = "/sys/class/power_supply/Battery" // wgr + //设备电源驱动默认节点 + POWER_PATH = "/sys/class/power_supply/Battery" } export class Power extends BaseProfiler { diff --git a/device/device_ui/entry/src/main/ets/common/profiler/item/RAM.ets b/device/device_ui/entry/src/main/ets/common/profiler/item/RAM.ets index a7f9a31..d2031b0 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/item/RAM.ets +++ b/device/device_ui/entry/src/main/ets/common/profiler/item/RAM.ets @@ -16,10 +16,16 @@ import { createGPData, extractNumber } from '../base/BaseProfilerUtils'; import { BaseProfiler } from '../base/BaseProfiler' import { CollectorType } from '../base/ProfilerConstant' import { SocketProfiler} from '../base/SocketProfiler' +import worker from '@ohos.worker'; +import WorkerHandler from '../WorkerHandler'; +let MainWorker = globalThis.MainWorker + +MainWorker.onmessage = function (result) { + WorkerHandler.socketHandler(result) +} export class RAM extends BaseProfiler implements SocketProfiler{ private ramMap: Map = new Map - public static instance: RAM = null public static getInstance() { if (this.instance == null) { @@ -34,25 +40,25 @@ export class RAM extends BaseProfiler implements SocketProfiler{ } isSupport(){ - return true + if (globalThis.useDaemon) { + return true + } + return false } readData() { if (globalThis.useDaemon) { this.readMessageQueue() - }else{ - let ramStr:string = globalThis.CreateNativeRam(globalThis.collectPkg) - if (ramStr.length > 0) { - this.ramMap.set("pss", ramStr) - } } return createGPData("RAM", this.ramMap) } readMessageQueue(){ + MainWorker.postMessage({ "ram": true, "pkg": globalThis.collectPkg }) if (globalThis.ramArr.length > 0) { let ramQueue: Array = globalThis.ramArr let curRAM = ramQueue.pop() this.ramMap.set("pss", extractNumber(curRAM)) } + MainWorker.terminate() } } diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/Load.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/Load.ets new file mode 100644 index 0000000..69c125e --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/Load.ets @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import LineChart from './chart/charts/LineChart' +import { LineChartModel } from './chart/charts/LineChart' +import { XAxis, XAxisPosition } from './chart/components/XAxis'; +import YAxis, { AxisDependency, YAxisLabelPosition } from './chart/components/YAxis' +import LineData from './chart/data/LineData'; +import { LineDataSet, ColorStop, Mode } from './chart/data/LineDataSet'; +import EntryOhos from './chart/data/EntryOhos'; +import { JArrayList } from './chart/utils/JArrayList'; +import ILineDataSet from './chart/interfaces/datasets/ILineDataSet' +import { TIndexInfo } from '../../entity/DatabaseEntity'; +import window from '@ohos.window'; +import CheckEmptyUtils from'../../utils/CheckEmptyUtils' + +/** + * 负载页面 + */ +@Entry +@Component +export struct Load { + @State private gpData: Array = new Array() + atWidth: number = globalThis.screenWith > 800 ? 700 : 350 ; //表的宽度 + atHeight: number = globalThis.screenWith > 800 ? 200 : 300; //表的高度 + minOffset: number = 15; //X轴线偏移量 + + //cpuLoad0 cpuLoad1 cpuLoad2 cpuLoad3 + topAxis: XAxis = new XAxis(); //顶部X轴 + bottomAxis: XAxis = new XAxis(); //底部X轴 + leftAxis: YAxis = null; + rightAxis: YAxis = null; + lineData: LineData = null; + + //CPUFre0 CPUFre1 CPUFre2 CPUFre3 + topAxis1: XAxis = new XAxis(); //顶部X轴 + bottomAxis1: XAxis = new XAxis(); //底部X轴 + leftAxis1: YAxis = null; + rightAxis1: YAxis = null; + lineData1: LineData = null; + + //CPULoad4 CPULoad5 CPULoad6 + topAxis2: XAxis = new XAxis(); //顶部X轴 + bottomAxis2: XAxis = new XAxis(); //底部X轴 + leftAxis2: YAxis = null; + rightAxis2: YAxis = null; + lineData2: LineData = null; + + //CPUFre4 CPUFre5 CPUFre6 + topAxis3: XAxis = new XAxis(); //顶部X轴 + bottomAxis3: XAxis = new XAxis(); //底部X轴 + leftAxis3: YAxis = null; + rightAxis3: YAxis = null; + lineData3: LineData = null; + + //CPULoad7 + topAxis4: XAxis = new XAxis(); //顶部X轴 + bottomAxis4: XAxis = new XAxis(); //底部X轴 + leftAxis4: YAxis = null; + rightAxis4: YAxis = null; + lineData4: LineData = null; + + //CPUFre7 + topAxis5: XAxis = new XAxis(); //顶部X轴 + bottomAxis5: XAxis = new XAxis(); //底部X轴 + leftAxis5: YAxis = null; + rightAxis5: YAxis = null; + lineData5: LineData = null; + + //GPULoad + topAxis6: XAxis = new XAxis(); //顶部X轴 + bottomAxis6: XAxis = new XAxis(); //底部X轴 + leftAxis6: YAxis = null; + rightAxis6: YAxis = null; + lineData6: LineData = null; + + //GPUFre + topAxis7: XAxis = new XAxis(); //顶部X轴 + bottomAxis7: XAxis = new XAxis(); //底部X轴 + leftAxis7: YAxis = null; + rightAxis7: YAxis = null; + lineData7: LineData = null; + + //DDRFre + topAxis8: XAxis = new XAxis(); //顶部X轴 + bottomAxis8: XAxis = new XAxis(); //底部X轴 + leftAxis8: YAxis = null; + rightAxis8: YAxis = null; + lineData8: LineData = null; + + lineChartModel : LineChartModel = new LineChartModel(); + lineChartModel1 : LineChartModel = new LineChartModel(); + lineChartModel2 : LineChartModel = new LineChartModel(); + lineChartModel3 : LineChartModel = new LineChartModel(); + lineChartModel4 : LineChartModel = new LineChartModel(); + lineChartModel5 : LineChartModel = new LineChartModel(); + lineChartModel6 : LineChartModel = new LineChartModel(); + lineChartModel7 : LineChartModel = new LineChartModel(); + lineChartModel8 : LineChartModel = new LineChartModel(); + + aboutToAppear() { + //CPULoad 0-3 + this.lineData = this.initCpuLoad0to3Data(); + + if (this.gpData.length < 10) { + this.topAxis.setLabelCount(this.gpData.length, false); + } else { + this.topAxis.setLabelCount(6, false); + } + + this.topAxis.setPosition(XAxisPosition.TOP); + this.topAxis.setAxisMinimum(0); + this.topAxis.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis.setLabelCount(6, false); + } + this.bottomAxis.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis.setAxisMinimum(0); + this.bottomAxis.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis = new YAxis(AxisDependency.LEFT); + this.leftAxis.setLabelCount(11, false); + this.leftAxis.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis.setSpaceTop(15); + this.leftAxis.setAxisMinimum(this.lineData.getYMin() - 10); + this.leftAxis.setAxisMaximum(this.lineData.getYMax() + 10); + this.leftAxis.enableGridDashedLine(10, 10, 0) + this.rightAxis = new YAxis(AxisDependency.RIGHT); + this.rightAxis.setDrawGridLines(false); + this.rightAxis.setLabelCount(11, false); + this.rightAxis.setSpaceTop(15); + this.rightAxis.setAxisMinimum(this.lineData.getYMin() - 10); // this replaces setStartAtZero(true) + this.rightAxis.setAxisMaximum(this.lineData.getYMax() + 10); + + + //CPUFre0 + this.lineData1 = this.initCpu0FreData(); + if (this.gpData.length < 10) { + this.topAxis1.setLabelCount(this.gpData.length, false); + } else { + this.topAxis1.setLabelCount(6, false); + } + + this.topAxis1.setPosition(XAxisPosition.TOP); + this.topAxis1.setAxisMinimum(0); + this.topAxis1.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis1.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis1.setLabelCount(6, false); + } + this.bottomAxis1.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis1.setAxisMinimum(0); + this.bottomAxis1.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis1 = new YAxis(AxisDependency.LEFT); + this.leftAxis1.setLabelCount(11, false); + this.leftAxis1.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis1.setSpaceTop(15); + this.leftAxis1.setAxisMinimum(this.lineData1.getYMin() - 500); + this.leftAxis1.setAxisMaximum(this.lineData1.getYMax() + 500); + this.leftAxis1.enableGridDashedLine(10, 10, 0) + this.rightAxis1 = new YAxis(AxisDependency.RIGHT); + this.rightAxis1.setDrawGridLines(false); + this.rightAxis1.setLabelCount(11, false); + this.rightAxis1.setSpaceTop(15); + this.rightAxis1.setAxisMinimum(this.lineData1.getYMin() - 500); // this replaces setStartAtZero(true) + this.rightAxis1.setAxisMaximum(this.lineData1.getYMax() + 500); + + //CPULoad4 CPULoad5 CPULoad6 + this.lineData2 = this.initCpuLoad4to6Data(); + if (this.gpData.length < 10) { + this.topAxis2.setLabelCount(this.gpData.length, false); + } else { + this.topAxis2.setLabelCount(6, false); + } + + this.topAxis2.setPosition(XAxisPosition.TOP); + this.topAxis2.setAxisMinimum(0); + this.topAxis2.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis2.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis2.setLabelCount(6, false); + } + this.bottomAxis2.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis2.setAxisMinimum(0); + this.bottomAxis2.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis2 = new YAxis(AxisDependency.LEFT); + this.leftAxis2.setLabelCount(11, false); + this.leftAxis2.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis2.setSpaceTop(15); + this.leftAxis2.setAxisMinimum(this.lineData2.getYMin() - 10); + this.leftAxis2.setAxisMaximum(this.lineData2.getYMax() + 10); + this.leftAxis2.enableGridDashedLine(10, 10, 0) + this.rightAxis2 = new YAxis(AxisDependency.RIGHT); + this.rightAxis2.setDrawGridLines(false); + this.rightAxis2.setLabelCount(11, false); + this.rightAxis2.setSpaceTop(15); + this.rightAxis2.setAxisMinimum(this.lineData2.getYMin() - 10); // this replaces setStartAtZero(true) + this.rightAxis2.setAxisMaximum(this.lineData2.getYMax() + 10); + + //CPUFre4 + this.lineData3 = this.initCpuFre4Data(); + if (this.gpData.length < 10) { + this.topAxis3.setLabelCount(this.gpData.length, false); + } else { + this.topAxis3.setLabelCount(6, false); + } + + this.topAxis3.setPosition(XAxisPosition.TOP); + this.topAxis3.setAxisMinimum(0); + this.topAxis3.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis3.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis3.setLabelCount(6, false); + } + this.bottomAxis3.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis3.setAxisMinimum(0); + this.bottomAxis3.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis3 = new YAxis(AxisDependency.LEFT); + this.leftAxis3.setLabelCount(11, false); + this.leftAxis3.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis3.setSpaceTop(15); + this.leftAxis3.setAxisMinimum(this.lineData3.getYMin() - 500); + this.leftAxis3.setAxisMaximum(this.lineData3.getYMax() + 500); + this.leftAxis3.enableGridDashedLine(10, 10, 0) + this.rightAxis3 = new YAxis(AxisDependency.RIGHT); + this.rightAxis3.setDrawGridLines(false); + this.rightAxis3.setLabelCount(11, false); + this.rightAxis3.setSpaceTop(15); + this.rightAxis3.setAxisMinimum(this.lineData3.getYMin() - 500); // this replaces setStartAtZero(true) + this.rightAxis3.setAxisMaximum(this.lineData3.getYMax() + 500); + + //CPULoad7 + this.lineData4 = this.initCpuLoad7Data(); + if (this.gpData.length < 10) { + this.topAxis4.setLabelCount(this.gpData.length, false); + } else { + this.topAxis4.setLabelCount(6, false); + } + + this.topAxis4.setPosition(XAxisPosition.TOP); + this.topAxis4.setAxisMinimum(0); + this.topAxis4.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis4.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis4.setLabelCount(6, false); + } + this.bottomAxis4.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis4.setAxisMinimum(0); + this.bottomAxis4.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis4 = new YAxis(AxisDependency.LEFT); + this.leftAxis4.setLabelCount(8, false); + this.leftAxis4.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis4.setSpaceTop(15); + this.leftAxis4.setAxisMinimum(this.lineData2.getYMin() - 10); + this.leftAxis4.setAxisMaximum(this.lineData4.getYMax() + 10); + this.leftAxis4.enableGridDashedLine(10, 10, 0) + this.rightAxis4 = new YAxis(AxisDependency.RIGHT); + this.rightAxis4.setDrawGridLines(false); + this.rightAxis4.setLabelCount(8, false); + this.rightAxis4.setSpaceTop(15); + this.rightAxis4.setAxisMinimum(this.lineData2.getYMin() - 10); // this replaces setStartAtZero(true) + this.rightAxis4.setAxisMaximum(this.lineData4.getYMax() + 10); + + //CPUFre7 + this.lineData5 = this.initCpuFre7Data(); + if (this.gpData.length < 10) { + this.topAxis5.setLabelCount(this.gpData.length, false); + } else { + this.topAxis5.setLabelCount(6, false); + } + + this.topAxis5.setPosition(XAxisPosition.TOP); + this.topAxis5.setAxisMinimum(0); + this.topAxis5.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis5.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis5.setLabelCount(6, false); + } + this.bottomAxis5.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis5.setAxisMinimum(0); + this.bottomAxis5.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis5 = new YAxis(AxisDependency.LEFT); + this.leftAxis5.setLabelCount(8, false); + this.leftAxis5.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis5.setSpaceTop(15); + this.leftAxis5.setAxisMinimum(this.lineData5.getYMin() - 500); + this.leftAxis5.setAxisMaximum(this.lineData5.getYMax() + 500); + this.leftAxis5.enableGridDashedLine(10, 10, 0) + this.rightAxis5 = new YAxis(AxisDependency.RIGHT); + this.rightAxis5.setDrawGridLines(false); + this.rightAxis5.setLabelCount(8, false); + this.rightAxis5.setSpaceTop(15); + this.rightAxis5.setAxisMinimum(this.lineData5.getYMin() - 500); // this replaces setStartAtZero(true) + this.rightAxis5.setAxisMaximum(this.lineData5.getYMax() + 500); + + //GPULoad + this.lineData6 = this.initGpuLoadData(); + if (this.gpData.length < 10) { + this.topAxis6.setLabelCount(this.gpData.length, false); + } else { + this.topAxis6.setLabelCount(6, false); + } + + this.topAxis6.setPosition(XAxisPosition.TOP); + this.topAxis6.setAxisMinimum(0); + this.topAxis6.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis6.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis6.setLabelCount(6, false); + } + this.bottomAxis6.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis6.setAxisMinimum(0); + this.bottomAxis6.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis6 = new YAxis(AxisDependency.LEFT); + this.leftAxis6.setLabelCount(8, false); + this.leftAxis6.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis6.setSpaceTop(15); + this.leftAxis6.setAxisMinimum(this.lineData6.getYMin() - 10); + this.leftAxis6.setAxisMaximum(this.lineData6.getYMax() + 10); + this.leftAxis6.enableGridDashedLine(10, 10, 0) + this.rightAxis6 = new YAxis(AxisDependency.RIGHT); + this.rightAxis6.setDrawGridLines(false); + this.rightAxis6.setLabelCount(8, false); + this.rightAxis6.setSpaceTop(15); + this.rightAxis6.setAxisMinimum(this.lineData6.getYMin() - 10); // this replaces setStartAtZero(true) + this.rightAxis6.setAxisMaximum(this.lineData6.getYMax() + 10); + + //GPUFre + this.lineData7 = this.initGPUFreData(); + if (this.gpData.length < 10) { + this.topAxis7.setLabelCount(this.gpData.length, false); + } else { + this.topAxis7.setLabelCount(6, false); + } + + this.topAxis7.setPosition(XAxisPosition.TOP); + this.topAxis7.setAxisMinimum(0); + this.topAxis7.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis7.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis7.setLabelCount(6, false); + } + this.bottomAxis7.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis7.setAxisMinimum(0); + this.bottomAxis7.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis7 = new YAxis(AxisDependency.LEFT); + this.leftAxis7.setLabelCount(8, false); + this.leftAxis7.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis7.setSpaceTop(15); + this.leftAxis7.setAxisMinimum(this.lineData7.getYMin() - 500); + this.leftAxis7.setAxisMaximum(this.lineData7.getYMax() + 500); + this.leftAxis7.enableGridDashedLine(10, 10, 0) + this.rightAxis7 = new YAxis(AxisDependency.RIGHT); + this.rightAxis7.setDrawGridLines(false); + this.rightAxis7.setLabelCount(8, false); + this.rightAxis7.setSpaceTop(15); + this.rightAxis7.setAxisMinimum(this.lineData7.getYMin() - 500); // this replaces setStartAtZero(true) + this.rightAxis7.setAxisMaximum(this.lineData7.getYMax() + 500); + + //DDRFre + this.lineData8 = this.initDDRFreData(); + if (this.gpData.length < 10) { + this.topAxis8.setLabelCount(this.gpData.length, false); + } else { + this.topAxis8.setLabelCount(6, false); + } + + this.topAxis8.setPosition(XAxisPosition.TOP); + this.topAxis8.setAxisMinimum(0); + this.topAxis8.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis8.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis8.setLabelCount(6, false); + } + this.bottomAxis8.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis8.setAxisMinimum(0); + this.bottomAxis8.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis8 = new YAxis(AxisDependency.LEFT); + this.leftAxis8.setLabelCount(8, false); + this.leftAxis8.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis8.setSpaceTop(15); + this.leftAxis8.setAxisMinimum(this.lineData8.getYMin() - 500); + this.leftAxis8.setAxisMaximum(this.lineData8.getYMax() + 500); + this.leftAxis8.enableGridDashedLine(10, 10, 0) + this.rightAxis8 = new YAxis(AxisDependency.RIGHT); + this.rightAxis8.setDrawGridLines(false); + this.rightAxis8.setLabelCount(8, false); + this.rightAxis8.setSpaceTop(15); + this.rightAxis8.setAxisMinimum(this.lineData8.getYMin() - 500); // this replaces setStartAtZero(true) + this.rightAxis8.setAxisMaximum(this.lineData8.getYMax() + 500); + + this.lineChartModel.setTopAxis(this.topAxis); + this.lineChartModel.setBottomAxis(this.bottomAxis); + this.lineChartModel.setWidth(this.atWidth); + this.lineChartModel.setHeight(this.atHeight); + this.lineChartModel.setMinOffset(this.minOffset); + this.lineChartModel.setLeftAxis(this.leftAxis); + this.lineChartModel.setRightAxis(this.rightAxis); + this.lineChartModel.setLineData(this.lineData); + this.lineChartModel.setIsShowLegend(false); + this.lineChartModel.init(); + + this.lineChartModel1.setTopAxis(this.topAxis1); + this.lineChartModel1.setBottomAxis(this.bottomAxis1); + this.lineChartModel1.setWidth(this.atWidth); + this.lineChartModel1.setHeight(this.atHeight); + this.lineChartModel1.setMinOffset(this.minOffset); + this.lineChartModel1.setLeftAxis(this.leftAxis1); + this.lineChartModel1.setRightAxis(this.rightAxis1); + this.lineChartModel1.setLineData(this.lineData1); + this.lineChartModel1.setIsShowLegend(false); + this.lineChartModel1.init(); + + this.lineChartModel2.setTopAxis(this.topAxis2); + this.lineChartModel2.setBottomAxis(this.bottomAxis2); + this.lineChartModel2.setWidth(this.atWidth); + this.lineChartModel2.setHeight(this.atHeight); + this.lineChartModel2.setMinOffset(this.minOffset); + this.lineChartModel2.setLeftAxis(this.leftAxis2); + this.lineChartModel2.setRightAxis(this.rightAxis2); + this.lineChartModel2.setLineData(this.lineData2); + this.lineChartModel2.setIsShowLegend(false); + this.lineChartModel2.init(); + + this.lineChartModel3.setTopAxis(this.topAxis3); + this.lineChartModel3.setBottomAxis(this.bottomAxis3); + this.lineChartModel3.setWidth(this.atWidth); + this.lineChartModel3.setHeight(this.atHeight); + this.lineChartModel3.setMinOffset(this.minOffset); + this.lineChartModel3.setLeftAxis(this.leftAxis3); + this.lineChartModel3.setRightAxis(this.rightAxis3); + this.lineChartModel3.setLineData(this.lineData3); + this.lineChartModel3.setIsShowLegend(false); + this.lineChartModel3.init(); + + this.lineChartModel4.setTopAxis(this.topAxis4); + this.lineChartModel4.setBottomAxis(this.bottomAxis4); + this.lineChartModel4.setWidth(this.atWidth); + this.lineChartModel4.setHeight(this.atHeight); + this.lineChartModel4.setMinOffset(this.minOffset); + this.lineChartModel4.setLeftAxis(this.leftAxis4); + this.lineChartModel4.setRightAxis(this.rightAxis4); + this.lineChartModel4.setLineData(this.lineData4); + this.lineChartModel4.setIsShowLegend(false); + this.lineChartModel4.init(); + + this.lineChartModel5.setTopAxis(this.topAxis5); + this.lineChartModel5.setBottomAxis(this.bottomAxis5); + this.lineChartModel5.setWidth(this.atWidth); + this.lineChartModel5.setHeight(this.atHeight); + this.lineChartModel5.setMinOffset(this.minOffset); + this.lineChartModel5.setLeftAxis(this.leftAxis5); + this.lineChartModel5.setRightAxis(this.rightAxis5); + this.lineChartModel5.setLineData(this.lineData5); + this.lineChartModel5.setIsShowLegend(false); + this.lineChartModel5.init(); + + this.lineChartModel6.setTopAxis(this.topAxis6); + this.lineChartModel6.setBottomAxis(this.bottomAxis6); + this.lineChartModel6.setWidth(this.atWidth); + this.lineChartModel6.setHeight(this.atHeight); + this.lineChartModel6.setMinOffset(this.minOffset); + this.lineChartModel6.setLeftAxis(this.leftAxis6); + this.lineChartModel6.setRightAxis(this.rightAxis6); + this.lineChartModel6.setLineData(this.lineData6); + this.lineChartModel6.setIsShowLegend(false); + this.lineChartModel6.init(); + + this.lineChartModel7.setTopAxis(this.topAxis7); + this.lineChartModel7.setBottomAxis(this.bottomAxis7); + this.lineChartModel7.setWidth(this.atWidth); + this.lineChartModel7.setHeight(this.atHeight); + this.lineChartModel7.setMinOffset(this.minOffset); + this.lineChartModel7.setLeftAxis(this.leftAxis7); + this.lineChartModel7.setRightAxis(this.rightAxis7); + this.lineChartModel7.setLineData(this.lineData7); + this.lineChartModel7.setIsShowLegend(false); + this.lineChartModel7.init(); + + this.lineChartModel8.setTopAxis(this.topAxis8); + this.lineChartModel8.setBottomAxis(this.bottomAxis8); + this.lineChartModel8.setWidth(this.atWidth); + this.lineChartModel8.setHeight(this.atHeight); + this.lineChartModel8.setMinOffset(this.minOffset); + this.lineChartModel8.setLeftAxis(this.leftAxis8); + this.lineChartModel8.setRightAxis(this.rightAxis8); + this.lineChartModel8.setLineData(this.lineData8); + this.lineChartModel8.setIsShowLegend(false); + this.lineChartModel8.init(); + + } + + /** + * 初始化数据 cpu0Load、cpu1Load、cpu2Load、cpu3Load + * @param count 曲线图点的个数 + * @param range y轴范围 + */ + private initCpuLoad0to3Data(): LineData { + + let dataSet = new JArrayList(); + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu0Load == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu0Load).valueOf())); + } + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + let set1 = new LineDataSet(values, "cpu0Load(%)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Blue); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Blue) + set1.setDrawCircleHole(false) + dataSet.add(set1); + + let values2 = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu1Load == "") { + continue + } + values2.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu1Load).valueOf())); + } + let gradientFillColor2 = new Array(); + gradientFillColor2.push([0x0C0099CC, 0.2]) + gradientFillColor2.push([0x7F0099CC, 0.4]) + gradientFillColor2.push([0x0099CC, 1.0]) + let set2 = new LineDataSet(values2, "cpu1Load(%)"); + set2.setDrawFilled(false); + set2.setMode(Mode.CUBIC_BEZIER); + set2.setDrawValues(false); + set2.setGradientFillColor(gradientFillColor2) + set2.setColorByColor(Color.Green); + set2.setLineWidth(3) + set2.setDrawCircles(false); + set2.setCircleColor(Color.Green); + set2.setCircleRadius(8); + set2.setCircleHoleRadius(4) + set2.setCircleHoleColor(Color.Green) + set2.setDrawCircleHole(false) + dataSet.add(set2); + + let values3 = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu2Load == "") { + continue + } + values3.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu2Load).valueOf())); + } + let gradientFillColor3 = new Array(); + gradientFillColor3.push([0x0C0099CC, 0.2]) + gradientFillColor3.push([0x7F0099CC, 0.4]) + gradientFillColor3.push([0x0099CC, 1.0]) + let set3 = new LineDataSet(values3, "cpu2Load(%)"); + set3.setDrawFilled(false); + set3.setMode(Mode.CUBIC_BEZIER); + set3.setGradientFillColor(gradientFillColor3) + set3.setColorByColor(Color.Red); + set3.setLineWidth(3) + set3.setDrawValues(false); + set3.setDrawCircles(false); + set3.setCircleColor(Color.Red); + set3.setCircleRadius(8); + set3.setCircleHoleRadius(4) + set3.setCircleHoleColor(Color.Red) + set3.setDrawCircleHole(false) + dataSet.add(set3); + + let values4 = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu3Load == "") { + continue + } + values4.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu3Load).valueOf())); + } + let gradientFillColor4 = new Array(); + gradientFillColor4.push([0x0C0099CC, 0.2]) + gradientFillColor4.push([0x7F0099CC, 0.4]) + gradientFillColor4.push([0x0099CC, 1.0]) + let set4 = new LineDataSet(values4, "cpu3Load(%)"); + set4.setDrawFilled(false); + set1.setDrawValues(false); + set4.setMode(Mode.CUBIC_BEZIER); + set4.setGradientFillColor(gradientFillColor4) + set4.setColorByColor(Color.Orange); + set4.setLineWidth(3) + set4.setDrawCircles(false); + set4.setCircleColor(Color.Orange); + set4.setCircleRadius(8); + set4.setCircleHoleRadius(4) + set4.setCircleHoleColor(Color.Orange) + set4.setDrawCircleHole(false) + dataSet.add(set4); + + return new LineData(dataSet) + } + + private initCpu0FreData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu0Frequency == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu0Frequency).valueOf() / 1e3)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "cpu0Frequency(MHZ)"); + set1.setDrawFilled(false); + set1.setDrawValues(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Red); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Red) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCpuLoad4to6Data(): LineData { + + let dataSet = new JArrayList(); + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu4Load == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu4Load).valueOf())); + } + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + let set1 = new LineDataSet(values, "cpu4Load(%)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Red); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Red); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Red) + set1.setDrawCircleHole(false) + dataSet.add(set1); + + let values2 = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu5Load == "") { + continue + } + values2.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu5Load).valueOf())); + } + let gradientFillColor2 = new Array(); + gradientFillColor2.push([0x0C0099CC, 0.2]) + gradientFillColor2.push([0x7F0099CC, 0.4]) + gradientFillColor2.push([0x0099CC, 1.0]) + let set2 = new LineDataSet(values2, "cpu5Load(%)"); + set2.setDrawFilled(false); + set2.setMode(Mode.CUBIC_BEZIER); + set2.setDrawValues(false); + set2.setGradientFillColor(gradientFillColor2) + set2.setColorByColor(Color.Blue); + set2.setLineWidth(3) + set2.setDrawCircles(false); + set2.setCircleColor(Color.Blue); + set2.setCircleRadius(8); + set2.setCircleHoleRadius(4) + set2.setCircleHoleColor(Color.Blue) + set2.setDrawCircleHole(false) + dataSet.add(set2); + + let values3 = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu6Load == "") { + continue + } + values3.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu6Load).valueOf())); + } + let gradientFillColor3 = new Array(); + gradientFillColor3.push([0x0C0099CC, 0.2]) + gradientFillColor3.push([0x7F0099CC, 0.4]) + gradientFillColor3.push([0x0099CC, 1.0]) + + let set3 = new LineDataSet(values3, "cpu6Load(%)"); + set3.setDrawFilled(false); + set3.setDrawValues(false); + set3.setMode(Mode.CUBIC_BEZIER); + set3.setGradientFillColor(gradientFillColor3) + set3.setColorByColor(Color.Green); + set3.setLineWidth(3) + set3.setDrawCircles(false); + set3.setCircleColor(Color.Green); + set3.setCircleRadius(8); + set3.setCircleHoleRadius(4) + set3.setCircleHoleColor(Color.Green) + set3.setDrawCircleHole(false) + dataSet.add(set3); + + return new LineData(dataSet) + } + + private initCpuFre4Data(): LineData { + + let dataSet = new JArrayList(); + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu4Frequency == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu4Frequency).valueOf() / 1e3)); + } + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let set1 = new LineDataSet(values, "cpu4Frequency(MHZ)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + + return new LineData(dataSet) + } + + private initCpuLoad7Data(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu7Load == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu7Load).valueOf())); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "cpu7Load(%)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + //CPUFre7 + private initCpuFre7Data(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.cpu7Frequency == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.cpu7Frequency).valueOf() / 1e3)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "cpu7Frequency(MHZ)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + //GPULoad + private initGpuLoadData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.gpuLoad == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.gpuLoad).valueOf())); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "GPULoad(%)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initGPUFreData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.gpuFrequency == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.gpuFrequency).valueOf() / 1e6)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "GPUFrequency(MHZ)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initDDRFreData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.ddrFrequency == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.ddrFrequency).valueOf() / 1e6)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "DDR Frequency(MHZ)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + build() { + + Stack({ alignContent: Alignment.TopStart }) { + Scroll() { + Column({ space: 20 }) { + LineChart({lineChartModel: this.lineChartModel}) + Row({ space: 20 }) { + Text("cpu0Load(%),") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + Text('cpu1Load(%),').fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + Text('cpu2Load(%),').fontWeight(FontWeight.Bold).fontColor(Color.Red).fontSize('15fp').textAlign(TextAlign.Center) + Text('cpu3Load(%)').fontWeight(FontWeight.Bold).fontColor(Color.Orange).fontSize('15fp').textAlign(TextAlign.Center) + } + LineChart({lineChartModel: this.lineChartModel1}) + Text("cpu0Frequency(MHZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Red).fontSize('15fp').textAlign(TextAlign.Center) + LineChart({lineChartModel: this.lineChartModel2}) + Row({ space: 20 }) { + Text('cpu4Load(%),').fontWeight(FontWeight.Bold).fontColor(Color.Red).fontSize('15fp').textAlign(TextAlign.Center) + Text("cpu5Load(%),") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + Text('cpu6Load(%),').fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + } + LineChart({lineChartModel: this.lineChartModel3}) + Text("cpu4Frequency(MHZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel4}) + Text("cpu7Load(%)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel5}) + Text("cpu7Frequency(MHZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + + LineChart({lineChartModel: this.lineChartModel6}) + Text("GPULoad(%)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel7}) + Text("GPUFrequency(MHZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + Stack() { + Column() { + LineChart({lineChartModel: this.lineChartModel8}) + Text("DDR Frequency(MHZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center).margin({ top: "50%" }) + } + } + }.width('100%').alignItems(HorizontalAlign.Center) + }.width('100%') + }.width('100%').height('100%') + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/Performance.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/Performance.ets new file mode 100644 index 0000000..883f5ab --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/Performance.ets @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import LineChart from './chart/charts/LineChart' +import { LineChartModel } from './chart/charts/LineChart' +import { XAxis, XAxisPosition } from './chart/components/XAxis'; +import YAxis, { AxisDependency, YAxisLabelPosition } from './chart/components/YAxis' +import LineData from './chart/data/LineData'; +import { LineDataSet, ColorStop, Mode } from './chart/data/LineDataSet'; +import EntryOhos from './chart/data/EntryOhos'; +import { JArrayList } from './chart/utils/JArrayList'; +import ILineDataSet from './chart/interfaces/datasets/ILineDataSet' +import { TIndexInfo } from '../../../common/entity/DatabaseEntity'; +import HandleLostFrame from './utils/HandleLostFrame'; +import CalculationUtils from '../../utils/CalculationUtils'; +import { FpsLostFrame } from './data/DetailCommon'; +import SPLogger from '../../utils/SPLogger' + +const TAG: string = "Performance" +/** + * 性能页面 + */ +@Entry +@Component +export struct Performance { + @State private gpData: Array = new Array() + @State lostList: Array = new Array() + lostLine: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] + fpsValues: Array = new Array() + fpsJitterValues: Array = new Array() + topAxis: XAxis = new XAxis(); //顶部X轴 + bottomAxis: XAxis = new XAxis(); //底部X轴 + atWidth: number = globalThis.screenWith > 800 ? 700 : 350 ; //表的宽度 + atHeight: number = globalThis.screenWith > 800 ? 200 : 300; //表的高度 + minOffset: number = 15; //X轴线偏移量 + leftAxis: YAxis = null; + rightAxis: YAxis = null; + lineData: LineData = null; + lineChartModel : LineChartModel = new LineChartModel(); + + aboutToAppear() { + SPLogger.DEBUG(TAG, "this.gpData.length" + this.gpData.length); + this.lineData = this.initCurveData(); + if (this.gpData.length < 10) { + this.topAxis.setLabelCount(this.gpData.length, false); + } else { + this.topAxis.setLabelCount(6, false); + } + + this.topAxis.setPosition(XAxisPosition.TOP); + this.topAxis.setAxisMinimum(0); + this.topAxis.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis.setLabelCount(6, false); + } + this.bottomAxis.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis.setAxisMinimum(0); + this.bottomAxis.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis = new YAxis(AxisDependency.LEFT); + this.leftAxis.setLabelCount(11, false); + this.leftAxis.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis.setSpaceTop(15); + this.leftAxis.setAxisMinimum(-10); + this.leftAxis.setAxisMaximum(this.lineData.getYMax() * 2); + this.leftAxis.enableGridDashedLine(10, 10, 0) + + this.rightAxis = new YAxis(AxisDependency.RIGHT); + this.rightAxis.setDrawGridLines(false); + this.rightAxis.setLabelCount(7, false); + this.rightAxis.setSpaceTop(11); + this.rightAxis.setAxisMinimum(-10); // this replaces setStartAtZero(true) + this.rightAxis.setAxisMaximum(this.lineData.getYMax() * 2); + + this.lineChartModel.setTopAxis(this.topAxis); + this.lineChartModel.setBottomAxis(this.bottomAxis); + this.lineChartModel.setWidth(this.atWidth); + this.lineChartModel.setHeight(this.atHeight); + this.lineChartModel.setMinOffset(this.minOffset); + this.lineChartModel.setLeftAxis(this.leftAxis); + this.lineChartModel.setRightAxis(this.rightAxis); + this.lineChartModel.setLineData(this.lineData); + this.lineChartModel.setIsShowLegend(false); + this.lineChartModel.init(); + + } + + /** + * 处理丢帧 + */ + private handleLostFrame() { + let handleLostFrame = new HandleLostFrame(CalculationUtils.calculateFPSNew(this.fpsValues)) + let lostFrameMap = new Map() + for (let jitter of this.fpsJitterValues) { + let jankMap = handleLostFrame.getJankMap(jitter) + for (let key of jankMap.keys()) { + if (lostFrameMap.get(key) != null && lostFrameMap.get(key) !== undefined) { + lostFrameMap.set(key, lostFrameMap.get(key) + jankMap.get(key)) + } else { + lostFrameMap.set(key, jankMap.get(key)) + } + } + } + let sumLostFrame: number = 0 + for (let key of lostFrameMap.keys()) { + sumLostFrame += parseInt(lostFrameMap.get(key).toString()) + } + SPLogger.DEBUG(TAG, + " sumLostFrame" + sumLostFrame); + for (let key of lostFrameMap.keys()) { + SPLogger.DEBUG(TAG, "value start key: " + key); + SPLogger.DEBUG(TAG, "value start : " + lostFrameMap.get(key).toString() + "total: " + sumLostFrame); + let fpsLostFrame = new FpsLostFrame((parseInt(key.toString()) - 1).toString(), lostFrameMap.get(key).toString(), + this.getPercent(parseInt(lostFrameMap.get(key).toString()), sumLostFrame).toString(), $r("app.color.color_fff")) + this.lostList.push(fpsLostFrame) + } + let list = this.lostList.sort((a, b) => parseInt(a.key) - parseInt(b.key)) + this.lostList.unshift(new FpsLostFrame("丢帧", "丢帧次数", "占比", $r("app.color.colorPrimary"))) + SPLogger.DEBUG(TAG, "this.lostList" + this.lostList.length) + SPLogger.DEBUG(TAG, "this.lostList JSON" + JSON.stringify(this.lostList)) + SPLogger.DEBUG(TAG, "this.list JSON" + JSON.stringify(list)) + } + + /** + * 获取百分比 + */ + private getPercent(value: number, total: number): String { + SPLogger.DEBUG(TAG, "value end : " + value + "total: " + total); + if (isNaN(value) || isNaN(total)) { + return "0.00%"; + } + let result: String = total <= 0 ? "0.00%" : (Math.round(value / total * 10000) / 100.00).toFixed(2) + "%"; + SPLogger.DEBUG(TAG, "value value / total * 10000 : " + value / total * 10000); + SPLogger.DEBUG(TAG, "value Math.round(value / total * 10000) : " + Math.round(value / total * 10000)); + SPLogger.DEBUG(TAG, "value Math.round(value / total * 10000) : " + (Math.round(value / total * 10000) / 100.00)); + SPLogger.DEBUG(TAG, "value result : " + result); + return result; + } + + /** + * 初始化数据 + * @param count 曲线图点的个数 + * @param range y轴范围 + */ + private initCurveData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + + if (gpDataCur.fps == "" || gpDataCur.fps === undefined) { + continue + } + this.fpsValues.push(parseInt(gpDataCur.fps.toString())) + SPLogger.DEBUG(TAG, "gpDataCur.fpsJitters" + gpDataCur.fpsJitters); + this.fpsJitterValues.push(gpDataCur.fpsJitters.toString().replace("\"", "")) + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.fps).valueOf())); + } + SPLogger.DEBUG(TAG, "this.fpsJitterValues" + JSON.stringify(this.fpsJitterValues)); + + //处理丢帧 + this.handleLostFrame(); + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "fps(HZ)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setDrawValues(false); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + build() { + + Scroll() { + Column({ space: '3vp' }) { + LineChart({lineChartModel: this.lineChartModel}) + Text("fps(HZ)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + Text("丢帧统计表\n(注:该表用于统计每一秒的丢帧数, 帧率基线取决于当前数据的最高帧率,\n其中丢帧的指的是丢几帧, 丢帧次数为整场数据的左侧丢帧值得发生次数)") { + }.fontWeight(FontWeight.Bold).fontColor($r("app.color.color_333")).fontSize('12fp').textAlign(TextAlign.Center) + + List() { + ForEach(this.lostList, (lostFrame) => { + ListItem() { + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { + Text(lostFrame.key) + .fontSize('12fp') + .fontColor($r("app.color.color_333")) + .height('30vp') + .width('20%') + .border({ width: '1vp', color: $r("app.color.color_999") }) + .backgroundColor(lostFrame.color) + .textAlign(TextAlign.Center) + + Text(lostFrame.value) + .fontSize('12fp') + .fontColor($r("app.color.color_333")) + .height('30vp') + .width('20%') + .border({ width: '1vp', color: $r("app.color.color_999") }) + .backgroundColor(lostFrame.color) + .textAlign(TextAlign.Center) + + Text(lostFrame.percent) + .fontSize('12fp') + .fontColor($r("app.color.color_333")) + .height('30vp') + .width('20%') + .border({ width: '1vp', color: $r("app.color.color_999") }) + .backgroundColor(lostFrame.color) + .textAlign(TextAlign.Center) + }.width('100%') + } + }, lostFrame => lostFrame.key) + }.width('100%').height("100%").margin({ bottom: "50%" }) + }.alignItems(HorizontalAlign.Center) + }.scrollable(ScrollDirection.Vertical).scrollBar(BarState.Auto).height("100%") + + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/Power.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/Power.ets new file mode 100644 index 0000000..54f2895 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/Power.ets @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import LineChart from './chart/charts/LineChart' +import { LineChartModel } from './chart/charts/LineChart' +import { XAxis, XAxisPosition } from './chart/components/XAxis'; +import YAxis, { AxisDependency, YAxisLabelPosition } from './chart/components/YAxis' +import LineData from './chart/data/LineData'; +import { LineDataSet, ColorStop, Mode } from './chart/data/LineDataSet'; +import EntryOhos from './chart/data/EntryOhos'; +import { JArrayList } from './chart/utils/JArrayList'; +import ILineDataSet from './chart/interfaces/datasets/ILineDataSet' +import { TIndexInfo } from '../../entity/DatabaseEntity'; + + +@Entry +@Component +export struct Power { + @State private gpData: Array = new Array() + topAxis: XAxis = new XAxis(); //顶部X轴 + bottomAxis: XAxis = new XAxis(); //底部X轴 + atWidth: number = globalThis.screenWith > 800 ? 700 : 350 ; //表的宽度 + atHeight: number = globalThis.screenWith > 800 ? 200 : 300; //表的高度 + minOffset: number = 15; //X轴线偏移量 + leftAxis: YAxis = null; + rightAxis: YAxis = null; + lineData: LineData = null; + lineChartModel : LineChartModel = new LineChartModel(); + + aboutToAppear() { + + this.lineData = this.initCurveData(); + + if (this.gpData.length < 10) { + this.topAxis.setLabelCount(this.gpData.length, false); + } else { + this.topAxis.setLabelCount(6, false); + } + + this.topAxis.setPosition(XAxisPosition.TOP); + this.topAxis.setAxisMinimum(0); + this.topAxis.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis.setLabelCount(6, false); + } + this.bottomAxis.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis.setAxisMinimum(0); + this.bottomAxis.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis = new YAxis(AxisDependency.LEFT); + this.leftAxis.setLabelCount(10, false); + this.leftAxis.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis.setSpaceTop(15); + this.leftAxis.setAxisMinimum(this.lineData.getYMin() - 50); // this replaces setStartAtZero(true) + this.leftAxis.setAxisMaximum(this.lineData.getYMax() + 50); + this.leftAxis.enableGridDashedLine(10, 10, 0) + + this.rightAxis = new YAxis(AxisDependency.RIGHT); + this.rightAxis.setDrawGridLines(false); + this.rightAxis.setLabelCount(10, false); + this.rightAxis.setSpaceTop(15); + this.rightAxis.setAxisMinimum(this.lineData.getYMin() - 50); // this replaces setStartAtZero(true) + this.rightAxis.setAxisMaximum(this.lineData.getYMax() + 50); + + this.lineChartModel.setTopAxis(this.topAxis); + this.lineChartModel.setBottomAxis(this.bottomAxis); + this.lineChartModel.setWidth(this.atWidth); + this.lineChartModel.setHeight(this.atHeight); + this.lineChartModel.setMinOffset(this.minOffset); + this.lineChartModel.setLeftAxis(this.leftAxis); + this.lineChartModel.setRightAxis(this.rightAxis); + this.lineChartModel.setLineData(this.lineData); + this.lineChartModel.setIsShowLegend(false); + this.lineChartModel.init(); + } + + /** + * 初始化数据 + * @param count 曲线图点的个数 + * @param range y轴范围 + */ + private initCurveData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.currentNow == "") { + parseInt + continue + } + values.add(new EntryOhos(Number(index).valueOf(), + Math.round((Number(gpDataCur.currentNow).valueOf() * (Number(gpDataCur.voltageNow).valueOf() / 1e6)) / 3.8) + )); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "Power Info(归一化电流 MA)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Blue); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setDrawValues(false); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Blue) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + build() { + Stack({ alignContent: Alignment.TopStart }) { + Scroll() { + Column({ space: 20 }) { + LineChart({lineChartModel: this.lineChartModel}) + Text("Power Info(归一化电流 MA)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + + }.width('100%').alignItems(HorizontalAlign.Center) + }.width('100%') + }.width('100%').height('100%') + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/Summary.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/Summary.ets index fee3ba3..d8423d6 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/detail/Summary.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/Summary.ets @@ -29,9 +29,9 @@ export struct Summary { @State private summaryItems: Array = new Array() aboutToAppear() { - SPLogger.DEBUG(TAG,"Summary: aboutToAppear"); + var cpuCoreArr = getCpuCoreInfo() - SPLogger.DEBUG(TAG,"Summary:aboutToAppear: cpuCoreArr:" + cpuCoreArr); + var cpu0FreqSum: number = 0 var cpu1FreqSum: number = 0 @@ -47,14 +47,14 @@ export struct Summary { var cpuMid: number = 0 var cpuMax: number = 0 - var currentNowSum: number = 0 - var curVoltageSum: number = 0 var normalCurrentNow: number = 0 var socThermalTemp: number = 0 var gpuLoadSum: number = 0 + var gpuFreqSum: number = 0 var ddrFreqSum: number = 0 + var shellFrameTempSum: number = 0 // fps和ram 为空时 过滤掉脏数据 0和空 var fpsNullSum = 0 @@ -68,16 +68,13 @@ export struct Summary { for (var index = 0; index < this.gpData.length; index++) { const gpDataCur = this.gpData[index]; - SPLogger.DEBUG(TAG,"Summary s:" + JSON.stringify(gpDataCur)); let currentNow = Number(gpDataCur.currentNow).valueOf() - currentNowSum += Math.abs(currentNow) - let curVoltage = Number(gpDataCur.voltageNow).valueOf() / 1e6 - curVoltageSum += Math.abs(curVoltage) normalCurrentNow += Math.abs(currentNow) * Math.abs(curVoltage) / 3.8 - socThermalTemp += Number(gpDataCur.socThermalTemp).valueOf() + shellFrameTempSum += Number(gpDataCur.shellFrameTemp).valueOf() gpuLoadSum += Number(gpDataCur.gpuLoad).valueOf() + gpuFreqSum += Number(gpDataCur.gpuFrequency).valueOf() ddrFreqSum += Number(gpDataCur.ddrFrequency).valueOf() cpu0FreqSum += Number(gpDataCur.cpu0Frequency).valueOf() cpu1FreqSum += Number(gpDataCur.cpu1Frequency).valueOf() @@ -114,63 +111,72 @@ export struct Summary { cpuFreqMap.set("cpu6FreqSum", cpu6FreqSum) cpuFreqMap.set("cpu7FreqSum", cpu7FreqSum) - SPLogger.DEBUG(TAG,"Summary cpu0FreqSum" + cpu0FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu1FreqSum" + cpu1FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu2FreqSum" + cpu2FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu3FreqSum" + cpu3FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu4FreqSum" + cpu4FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu5FreqSum" + cpu5FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu6FreqSum" + cpu6FreqSum); - SPLogger.DEBUG(TAG,"Summary cpu7FreqSum" + cpu7FreqSum); - SPLogger.DEBUG(TAG,"Summary FpsJitters" + JSON.stringify(fpsJitters)); - - SPLogger.DEBUG(TAG,"Summary:aboutToAppear currentNowSum:" + currentNowSum); - SPLogger.DEBUG(TAG,"Summary:aboutToAppear socThermalTemp:" + socThermalTemp); - SPLogger.DEBUG(TAG,"Summary:aboutToAppear gpuLoadSum:" + gpuLoadSum); - SPLogger.DEBUG(TAG,"Summary:aboutToAppear ddrFreqSum:" + ddrFreqSum); - SPLogger.DEBUG(TAG,"Summary:aboutToAppear cpuFreqMap:" + JSON.stringify(cpuFreqMap)); - - cpuMin = cpuFreqMap.get("cpu" + 0 + "FreqSum") / 1e3 - cpuMid = cpuFreqMap.get("cpu" +1 + "FreqSum") / 1e3 + cpuMid = cpuFreqMap.get("cpu" + 1 + "FreqSum") / 1e3 cpuMax = cpuFreqMap.get("cpu" + 2 + "FreqSum") / 1e3 + if (cpu4FreqSum > 0 && cpu7FreqSum > 0) { + cpuMid = cpuFreqMap.get("cpu" + 4 + "FreqSum") / 1e3 + cpuMax = cpuFreqMap.get("cpu" + 7 + "FreqSum") / 1e3 + } + let calculationTest = new CalculationUtils(fpsList, CalculationUtils.calculateFPSNew(fpsList)) + + if (normalCurrentNow > 0) { + this.summaryItems.push( + new SummaryItem($r("app.media.icon_normalized_current"), "归一化电流", (normalCurrentNow / this.gpData.length).toFixed(0) + "mA", ""), + ) + } + if (socThermalTemp > 0) { + this.summaryItems.push( + new SummaryItem($r("app.media.icon_max_temperature"), "soc温度", (socThermalTemp / this.gpData.length / 1000 ).toFixed(0) + "℃", "#fff8f8"), + ) + } + + if (shellFrameTempSum > 0) { + this.summaryItems.push( + new SummaryItem($r("app.media.icon_max_temperature"), "壳温", (shellFrameTempSum / this.gpData.length / 1000 ).toFixed(0) + "℃", "#fff8f8"), + ) + } + this.summaryItems.push( - new SummaryItem($r("app.media.icon_normalized_current"), "归一化电流", (normalCurrentNow / this.gpData.length).toFixed(0) + "mA"), - new SummaryItem($r("app.media.icon_max_temperature"), "soc温度", (socThermalTemp / this.gpData.length / 1000 ).toFixed(0) + "℃"), - new SummaryItem($r("app.media.icon_jank_score"), "平均帧率/最高帧率", (fpsSum / (this.gpData.length - fpsNullSum)).toFixed(0) + "Hz" + "/" + (fpsMax).toFixed(0) + "HZ"), - new SummaryItem($r("app.media.icon_jank_score"), "低帧率", (calculationTest.Low_Frame_Rate()).toFixed(2) + "%"), - new SummaryItem($r("app.media.icon_jank_score"), "抖动率", (calculationTest.Jitter_rate()).toFixed(2) + "%"), - new SummaryItem($r("app.media.icon_jank_score"), "卡顿次数", (calculationTest.calculateCaton(fpsJitters)).toFixed(0) + "次"), - new SummaryItem($r("app.media.icon_frame_score"), "GPU负载", (gpuLoadSum / this.gpData.length).toFixed(0) + "%"), - new SummaryItem($r("app.media.icon_frame_score"), "DDR频率", (ddrFreqSum / this.gpData.length / 1e6).toFixed(0) + "MHZ"), - new SummaryItem($r("app.media.icon_average_frame_b"), "CPU0频率", (cpuMin / this.gpData.length).toFixed(0) + "MHZ"), - new SummaryItem($r("app.media.icon_average_frame_b"), "CPU1频率", (cpuMid / this.gpData.length).toFixed(0) + "MHZ"), - new SummaryItem($r("app.media.icon_average_frame_b"), "CPU2频率", (cpuMax / this.gpData.length).toFixed(0) + "MHZ"), - new SummaryItem($r("app.media.icon_jank_each_hour"), "RAM", (pssSum / (this.gpData.length - ramNullSum)).toFixed(0) + "KB") + new SummaryItem($r("app.media.icon_jank_score"), "平均帧率", (fpsSum / (this.gpData.length - fpsNullSum)).toFixed(0) + "Hz", "#fcf4ee"), + new SummaryItem($r("app.media.icon_jank_score"), "最高帧率", (fpsMax).toFixed(0) + "HZ", "#fcf4ee"), + new SummaryItem($r("app.media.icon_jank_score"), "低帧率", (calculationTest.Low_Frame_Rate()).toFixed(2) + "%", "#fcf4ee"), + new SummaryItem($r("app.media.icon_jank_score"), "抖动率", (calculationTest.Jitter_rate()).toFixed(2) + "%", "#fcf4ee"), + new SummaryItem($r("app.media.icon_jank_score"), "卡顿次数", (calculationTest.calculateCaton(fpsJitters)).toFixed(0) + "次", "#fcf4ee"), + new SummaryItem( + $r("app.media.icon_frame_score"), "GPU负载", + (gpuFreqSum / this.gpData.length / 1e6).toFixed(0) + "MHZ" + " " + + (gpuLoadSum / this.gpData.length).toFixed(0) + "%", "#fcf9f2"), + new SummaryItem($r("app.media.icon_frame_score"), "DDR频率", (ddrFreqSum / this.gpData.length / 1e6).toFixed(0) + "MHZ", "#fcf9f2"), + new SummaryItem($r("app.media.icon_average_frame_b"), "CPU0频率", (cpuMin / this.gpData.length).toFixed(0) + "MHZ", "#fcf9f2"), + new SummaryItem($r("app.media.icon_average_frame_b"), "CPU1频率", (cpuMid / this.gpData.length).toFixed(0) + "MHZ", "#fcf9f2"), + new SummaryItem($r("app.media.icon_average_frame_b"), "CPU2频率", (cpuMax / this.gpData.length).toFixed(0) + "MHZ", "#fcf9f2"), + new SummaryItem($r("app.media.icon_jank_each_hour"), "RAM", (pssSum / (this.gpData.length - ramNullSum)).toFixed(0) + "KB", "#f0faff") ) } build() { - Scroll() { - Column() { - Grid() { - ForEach(this.summaryItems, item => { - GridItem() { - Flex({ alignItems: ItemAlign.Center, alignContent: FlexAlign.Start }) { - Row({ space: "8vp" }) { - Image(item.icon).width('50vp').height('50vp') - Text(item.content).fontSize('14fp').textAlign(TextAlign.Start) - Text(item.value).fontSize('11fp').textAlign(TextAlign.Start) - }.alignItems(VerticalAlign.Center) - }.margin({ left: '10' }).borderStyle(BorderStyle.Solid).borderStyle(1).borderColor(Color.Gray) - }.margin({ left: "6%" }) - }, item => item.content) - }.margin({ bottom: "20%" }) - .columnsTemplate('1fr 1fr') - .rowsGap(20) - } - }.height('100%').width('100%') + Column() { + Grid() { + ForEach(this.summaryItems, item => { + GridItem() { + Row({ space: "8vp" }) { + Image(item.icon).width('30vp').height('30vp') + Text(item.content).fontSize('14fp').textAlign(TextAlign.Start) + Text(item.value).fontSize('11fp').textAlign(TextAlign.Start) + }.alignItems(VerticalAlign.Center).width('100%') + } + .width("90%") + .align(Alignment.Center) + .backgroundColor(item.backColor) + .border({ radius: '5vp', color: '#ffffff' }).shadow({radius : 5}) + .margin({top: '30vp'}) + .padding("10vp") + }, item => item.content) + }.margin({ bottom: "30%", left: "15%", right: "15%" }).width('90%') + .columnsTemplate('1fr 1fr') + }.height('90%').width('100%') } } \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/Temperature.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/Temperature.ets new file mode 100644 index 0000000..b25633f --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/Temperature.ets @@ -0,0 +1,708 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import LineChart from './chart/charts/LineChart' +import { LineChartModel } from './chart/charts/LineChart' +import { XAxis, XAxisPosition } from './chart/components/XAxis'; +import YAxis, { AxisDependency, YAxisLabelPosition } from './chart/components/YAxis' +import LineData from './chart/data/LineData'; +import { LineDataSet, ColorStop, Mode } from './chart/data/LineDataSet'; +import EntryOhos from './chart/data/EntryOhos'; +import { JArrayList } from './chart/utils/JArrayList'; +import ILineDataSet from './chart/interfaces/datasets/ILineDataSet' +import { TIndexInfo } from '../../entity/DatabaseEntity'; + + +@Entry +@Component +export struct Temperature { + @State private gpData: Array = new Array() + topAxis: XAxis = new XAxis(); //顶部X轴 + bottomAxis: XAxis = new XAxis(); //底部X轴 + atWidth: number = globalThis.screenWith > 800 ? 700 : 350 ; //表的宽度 + atHeight: number = globalThis.screenWith > 800 ? 200 : 300; //表的高度 + minOffset: number = 15; //X轴线偏移量 + leftAxis: YAxis = null; + rightAxis: YAxis = null; + lineData: LineData = null; + topAxis1: XAxis = new XAxis(); //顶部X轴 + bottomAxis1: XAxis = new XAxis(); //底部X轴 + leftAxis1: YAxis = null; + rightAxis1: YAxis = null; + lineData1: LineData = null; + topAxis2: XAxis = new XAxis(); //顶部X轴 + bottomAxis2: XAxis = new XAxis(); //底部X轴 + leftAxis2: YAxis = null; + rightAxis2: YAxis = null; + lineData2: LineData = null; + topAxis3: XAxis = new XAxis(); //顶部X轴 + bottomAxis3: XAxis = new XAxis(); //底部X轴 + leftAxis3: YAxis = null; + rightAxis3: YAxis = null; + lineData3: LineData = null; + topAxis4: XAxis = new XAxis(); //顶部X轴 + bottomAxis4: XAxis = new XAxis(); //底部X轴 + leftAxis4: YAxis = null; + rightAxis4: YAxis = null; + lineData4: LineData = null; + topAxis5: XAxis = new XAxis(); //顶部X轴 + bottomAxis5: XAxis = new XAxis(); //底部X轴 + leftAxis5: YAxis = null; + rightAxis5: YAxis = null; + lineData5: LineData = null; + topAxis6: XAxis = new XAxis(); //顶部X轴 + bottomAxis6: XAxis = new XAxis(); //底部X轴 + leftAxis6: YAxis = null; + rightAxis6: YAxis = null; + lineData6: LineData = null; + + + lineChartModel : LineChartModel = new LineChartModel(); + lineChartModel1 : LineChartModel = new LineChartModel(); + lineChartModel2 : LineChartModel = new LineChartModel(); + lineChartModel3 : LineChartModel = new LineChartModel(); + lineChartModel4 : LineChartModel = new LineChartModel(); + lineChartModel5 : LineChartModel = new LineChartModel(); + lineChartModel6 : LineChartModel = new LineChartModel(); + + aboutToAppear() { + + this.lineData = this.initCurveData(); + + if (this.gpData.length < 10) { + this.topAxis.setLabelCount(this.gpData.length, false); + } else { + this.topAxis.setLabelCount(6, false); + } + + this.topAxis.setPosition(XAxisPosition.TOP); + this.topAxis.setAxisMinimum(0); + this.topAxis.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis.setLabelCount(6, false); + } + this.bottomAxis.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis.setAxisMinimum(0); + this.bottomAxis.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis = new YAxis(AxisDependency.LEFT); + this.leftAxis.setLabelCount(11, false); + this.leftAxis.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis.setSpaceTop(15); + this.leftAxis.setAxisMinimum(this.lineData.getYMin() - 5); + this.leftAxis.setAxisMaximum(this.lineData.getYMax() + 5); + this.leftAxis.enableGridDashedLine(10, 10, 0) + this.rightAxis = new YAxis(AxisDependency.RIGHT); + this.rightAxis.setDrawGridLines(false); + this.rightAxis.setLabelCount(11, false); + this.rightAxis.setSpaceTop(15); + this.rightAxis.setAxisMinimum(this.lineData.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis.setAxisMaximum(this.lineData.getYMax() + 5); + + this.lineData1 = this.initCurveData1(); + + if (this.gpData.length < 10) { + this.topAxis1.setLabelCount(this.gpData.length, false); + } else { + this.topAxis1.setLabelCount(6, false); + } + + this.topAxis1.setPosition(XAxisPosition.TOP); + this.topAxis1.setAxisMinimum(0); + this.topAxis1.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis1.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis1.setLabelCount(6, false); + } + this.bottomAxis1.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis1.setAxisMinimum(0); + this.bottomAxis1.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis1 = new YAxis(AxisDependency.LEFT); + this.leftAxis1.setLabelCount(11, false); + this.leftAxis1.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis1.setSpaceTop(15); + this.leftAxis1.setAxisMinimum(this.lineData1.getYMin() - 5); + this.leftAxis1.setAxisMaximum(this.lineData1.getYMax() + 5); + this.leftAxis1.enableGridDashedLine(10, 10, 0) + this.rightAxis1 = new YAxis(AxisDependency.RIGHT); + this.rightAxis1.setDrawGridLines(false); + this.rightAxis1.setLabelCount(11, false); + this.rightAxis1.setSpaceTop(15); + this.rightAxis1.setAxisMinimum(this.lineData1.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis1.setAxisMaximum(this.lineData1.getYMax() + 5); + + + this.lineData2 = this.initCurveData2(); + + if (this.gpData.length < 10) { + this.topAxis2.setLabelCount(this.gpData.length, false); + } else { + this.topAxis2.setLabelCount(6, false); + } + + this.topAxis2.setPosition(XAxisPosition.TOP); + this.topAxis2.setAxisMinimum(0); + this.topAxis2.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis2.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis2.setLabelCount(6, false); + } + this.bottomAxis2.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis2.setAxisMinimum(0); + this.bottomAxis2.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis2 = new YAxis(AxisDependency.LEFT); + this.leftAxis2.setLabelCount(11, false); + this.leftAxis2.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis2.setSpaceTop(15); + this.leftAxis2.setAxisMinimum(this.lineData2.getYMin() - 5); + this.leftAxis2.setAxisMaximum(this.lineData2.getYMax() + 5); + this.leftAxis2.enableGridDashedLine(10, 10, 0) + this.rightAxis2 = new YAxis(AxisDependency.RIGHT); + this.rightAxis2.setDrawGridLines(false); + this.rightAxis2.setLabelCount(11, false); + this.rightAxis2.setSpaceTop(15); + this.rightAxis2.setAxisMinimum(this.lineData2.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis2.setAxisMaximum(this.lineData2.getYMax() + 5); + + + this.lineData3 = this.initCurveData3(); + + if (this.gpData.length < 10) { + this.topAxis3.setLabelCount(this.gpData.length, false); + } else { + this.topAxis3.setLabelCount(6, false); + } + + this.topAxis3.setPosition(XAxisPosition.TOP); + this.topAxis3.setAxisMinimum(0); + this.topAxis3.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis3.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis3.setLabelCount(6, false); + } + this.bottomAxis3.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis3.setAxisMinimum(0); + this.bottomAxis3.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis3 = new YAxis(AxisDependency.LEFT); + this.leftAxis3.setLabelCount(11, false); + this.leftAxis3.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis3.setSpaceTop(15); + this.leftAxis3.setAxisMinimum(this.lineData3.getYMin() - 5); + this.leftAxis3.setAxisMaximum(this.lineData3.getYMax() + 5); + this.leftAxis3.enableGridDashedLine(10, 10, 0) + this.rightAxis3 = new YAxis(AxisDependency.RIGHT); + this.rightAxis3.setDrawGridLines(false); + this.rightAxis3.setLabelCount(11, false); + this.rightAxis3.setSpaceTop(15); + this.rightAxis3.setAxisMinimum(this.lineData3.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis3.setAxisMaximum(this.lineData3.getYMax() + 5); + + + this.lineData4 = this.initCurveData4(); + + if (this.gpData.length < 10) { + this.topAxis4.setLabelCount(this.gpData.length, false); + } else { + this.topAxis4.setLabelCount(6, false); + } + + this.topAxis4.setPosition(XAxisPosition.TOP); + this.topAxis4.setAxisMinimum(0); + this.topAxis4.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis4.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis4.setLabelCount(6, false); + } + this.bottomAxis4.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis4.setAxisMinimum(0); + this.bottomAxis4.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis4 = new YAxis(AxisDependency.LEFT); + this.leftAxis4.setLabelCount(11, false); + this.leftAxis4.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis4.setSpaceTop(15); + this.leftAxis4.setAxisMinimum(this.lineData4.getYMin() - 5); + this.leftAxis4.setAxisMaximum(this.lineData4.getYMax() + 5); + this.leftAxis4.enableGridDashedLine(10, 10, 0) + this.rightAxis4 = new YAxis(AxisDependency.RIGHT); + this.rightAxis4.setDrawGridLines(false); + this.rightAxis4.setLabelCount(11, false); + this.rightAxis4.setSpaceTop(15); + this.rightAxis4.setAxisMinimum(this.lineData4.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis4.setAxisMaximum(this.lineData4.getYMax() + 5); + + + this.lineData5 = this.initCurveData5(); + + if (this.gpData.length < 10) { + this.topAxis5.setLabelCount(this.gpData.length, false); + } else { + this.topAxis5.setLabelCount(6, false); + } + + this.topAxis5.setPosition(XAxisPosition.TOP); + this.topAxis5.setAxisMinimum(0); + this.topAxis5.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis5.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis5.setLabelCount(6, false); + } + this.bottomAxis5.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis5.setAxisMinimum(0); + this.bottomAxis5.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis5 = new YAxis(AxisDependency.LEFT); + this.leftAxis5.setLabelCount(11, false); + this.leftAxis5.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis5.setSpaceTop(15); + this.leftAxis5.setAxisMinimum(this.lineData5.getYMin() - 5); + this.leftAxis5.setAxisMaximum(this.lineData5.getYMax() + 5); + this.leftAxis5.enableGridDashedLine(10, 10, 0) + this.rightAxis5 = new YAxis(AxisDependency.RIGHT); + this.rightAxis5.setDrawGridLines(false); + this.rightAxis5.setLabelCount(11, false); + this.rightAxis5.setSpaceTop(15); + this.rightAxis5.setAxisMinimum(this.lineData5.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis5.setAxisMaximum(this.lineData5.getYMax() + 5); + + + this.lineData6 = this.initCurveData6(); + + if (this.gpData.length < 10) { + this.topAxis6.setLabelCount(this.gpData.length, false); + } else { + this.topAxis6.setLabelCount(6, false); + } + + this.topAxis6.setPosition(XAxisPosition.TOP); + this.topAxis6.setAxisMinimum(0); + this.topAxis6.setAxisMaximum(this.gpData.length - 1); + + if (this.gpData.length < 10) { + this.bottomAxis6.setLabelCount(this.gpData.length, false); + } else { + this.bottomAxis6.setLabelCount(6, false); + } + this.bottomAxis6.setPosition(XAxisPosition.BOTTOM); + this.bottomAxis6.setAxisMinimum(0); + this.bottomAxis6.setAxisMaximum(this.gpData.length - 1); + + this.leftAxis6 = new YAxis(AxisDependency.LEFT); + this.leftAxis6.setLabelCount(11, false); + this.leftAxis6.setPosition(YAxisLabelPosition.OUTSIDE_CHART); + this.leftAxis6.setSpaceTop(15); + this.leftAxis6.setAxisMinimum(this.lineData6.getYMin() - 5); + this.leftAxis6.setAxisMaximum(this.lineData6.getYMax() + 5); + this.leftAxis6.enableGridDashedLine(10, 10, 0) + this.rightAxis6 = new YAxis(AxisDependency.RIGHT); + this.rightAxis6.setDrawGridLines(false); + this.rightAxis6.setLabelCount(11, false); + this.rightAxis6.setSpaceTop(15); + this.rightAxis6.setAxisMinimum(this.lineData6.getYMin() - 5); // this replaces setStartAtZero(true) + this.rightAxis6.setAxisMaximum(this.lineData6.getYMax() + 5); + + + + + this.lineChartModel.setTopAxis(this.topAxis); + this.lineChartModel.setBottomAxis(this.bottomAxis); + this.lineChartModel.setWidth(this.atWidth); + this.lineChartModel.setHeight(this.atHeight); + this.lineChartModel.setMinOffset(this.minOffset); + this.lineChartModel.setLeftAxis(this.leftAxis); + this.lineChartModel.setRightAxis(this.rightAxis); + this.lineChartModel.setLineData(this.lineData); + this.lineChartModel.setIsShowLegend(false); + this.lineChartModel.init(); + + this.lineChartModel1.setTopAxis(this.topAxis1); + this.lineChartModel1.setBottomAxis(this.bottomAxis1); + this.lineChartModel1.setWidth(this.atWidth); + this.lineChartModel1.setHeight(this.atHeight); + this.lineChartModel1.setMinOffset(this.minOffset); + this.lineChartModel1.setLeftAxis(this.leftAxis1); + this.lineChartModel1.setRightAxis(this.rightAxis1); + this.lineChartModel1.setLineData(this.lineData1); + this.lineChartModel1.setIsShowLegend(false); + this.lineChartModel1.init(); + + this.lineChartModel2.setTopAxis(this.topAxis2); + this.lineChartModel2.setBottomAxis(this.bottomAxis2); + this.lineChartModel2.setWidth(this.atWidth); + this.lineChartModel2.setHeight(this.atHeight); + this.lineChartModel2.setMinOffset(this.minOffset); + this.lineChartModel2.setLeftAxis(this.leftAxis2); + this.lineChartModel2.setRightAxis(this.rightAxis2); + this.lineChartModel2.setLineData(this.lineData2); + this.lineChartModel2.setIsShowLegend(false); + this.lineChartModel2.init(); + + this.lineChartModel3.setTopAxis(this.topAxis3); + this.lineChartModel3.setBottomAxis(this.bottomAxis3); + this.lineChartModel3.setWidth(this.atWidth); + this.lineChartModel3.setHeight(this.atHeight); + this.lineChartModel3.setMinOffset(this.minOffset); + this.lineChartModel3.setLeftAxis(this.leftAxis3); + this.lineChartModel3.setRightAxis(this.rightAxis3); + this.lineChartModel3.setLineData(this.lineData3); + this.lineChartModel3.setIsShowLegend(false); + this.lineChartModel3.init(); + + this.lineChartModel4.setTopAxis(this.topAxis4); + this.lineChartModel4.setBottomAxis(this.bottomAxis4); + this.lineChartModel4.setWidth(this.atWidth); + this.lineChartModel4.setHeight(this.atHeight); + this.lineChartModel4.setMinOffset(this.minOffset); + this.lineChartModel4.setLeftAxis(this.leftAxis4); + this.lineChartModel4.setRightAxis(this.rightAxis4); + this.lineChartModel4.setLineData(this.lineData4); + this.lineChartModel4.setIsShowLegend(false); + this.lineChartModel4.init(); + + this.lineChartModel5.setTopAxis(this.topAxis5); + this.lineChartModel5.setBottomAxis(this.bottomAxis5); + this.lineChartModel5.setWidth(this.atWidth); + this.lineChartModel5.setHeight(this.atHeight); + this.lineChartModel5.setMinOffset(this.minOffset); + this.lineChartModel5.setLeftAxis(this.leftAxis5); + this.lineChartModel5.setRightAxis(this.rightAxis5); + this.lineChartModel5.setLineData(this.lineData5); + this.lineChartModel5.setIsShowLegend(false); + this.lineChartModel5.init(); + + this.lineChartModel6.setTopAxis(this.topAxis6); + this.lineChartModel6.setBottomAxis(this.bottomAxis6); + this.lineChartModel6.setWidth(this.atWidth); + this.lineChartModel6.setHeight(this.atHeight); + this.lineChartModel6.setMinOffset(this.minOffset); + this.lineChartModel6.setLeftAxis(this.leftAxis6); + this.lineChartModel6.setRightAxis(this.rightAxis6); + this.lineChartModel6.setLineData(this.lineData6); + this.lineChartModel6.setIsShowLegend(false); + this.lineChartModel6.init(); + } + + /** + * 初始化数据 + * @param count 曲线图点的个数 + * @param range y轴范围 + */ + private initCurveData(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.systemHTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.systemHTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "SystemHTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData1(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.batteryTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.batteryTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "BatteryTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Red); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setDrawValues(false); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Red) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData2(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.shellFrontTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.shellFrontTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "ShellFrontTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Blue); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Blue) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData3(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.shellFrameTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.shellFrameTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "ShellFrameTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData4(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.socThermalTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.socThermalTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "SocThermalTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Red); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Red) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData5(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.shellBackTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.shellBackTemp).valueOf() / 1000)); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "ShellBackTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Blue); + set1.setLineWidth(3) + set1.setDrawValues(false); + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Blue) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + private initCurveData6(): LineData { + + let values = new JArrayList(); + for (let index: number = 0; index < this.gpData.length; index++) { + const gpDataCur = this.gpData[index]; + if (gpDataCur.gpuTemp == "") { + continue + } + values.add(new EntryOhos(Number(index).valueOf(), Number(gpDataCur.gpuTemp).valueOf())); + } + + + let gradientFillColor = new Array(); + gradientFillColor.push([0x0C0099CC, 0.2]) + gradientFillColor.push([0x7F0099CC, 0.4]) + gradientFillColor.push([0x0099CC, 1.0]) + + let dataSet = new JArrayList(); + + let set1 = new LineDataSet(values, "GPUTemp(℃)"); + set1.setDrawFilled(false); + set1.setMode(Mode.CUBIC_BEZIER); + set1.setGradientFillColor(gradientFillColor) + set1.setColorByColor(Color.Green); + set1.setLineWidth(3) + set1.setDrawCircles(false); + set1.setCircleColor(Color.Blue); + set1.setCircleRadius(8); + set1.setDrawValues(false); + set1.setCircleHoleRadius(4) + set1.setCircleHoleColor(Color.Green) + set1.setDrawCircleHole(false) + dataSet.add(set1); + return new LineData(dataSet) + } + + + build() { + Stack({ alignContent: Alignment.TopStart }) { + Scroll() { + Column({ space: 20 }) { + LineChart({lineChartModel: this.lineChartModel2}) + Text("ShellFrontTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel4}) + Text("socThermalTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel1}) + Text("BatteryTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel3}) + Text("ShellFrameTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel5}) + Text("ShellBackTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + + LineChart({lineChartModel: this.lineChartModel}) + Text("SystemHTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Green).fontSize('15fp').textAlign(TextAlign.Center) + Stack() { + Column() { + LineChart({lineChartModel: this.lineChartModel6}) + Text("gpuTemp(℃)") { + }.fontWeight(FontWeight.Bold).fontColor(Color.Blue).fontSize('15fp').textAlign(TextAlign.Center) + } + } + }.width('100%').alignItems(HorizontalAlign.Center) + }.width('100%') + }.width('100%').height('100%') + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/animation/ChartAnimator.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/animation/ChartAnimator.ets new file mode 100644 index 0000000..4756553 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/animation/ChartAnimator.ets @@ -0,0 +1,203 @@ +// @ts-nocheck +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class ChartAnimator { + + /** object that is updated upon animation update */ + // private AnimatorUpdateListener mListener; + + /** The phase of drawn values on the y-axis. 0 - 1 */ + protected mPhaseY:number = 1; + + /** The phase of drawn values on the x-axis. 0 - 1 */ + protected mPhaseX:number = 1; + + constructor() { } + + // constructor(listener:AnimatorUpdateListener) { + // this.mListener = listener; + // } + + private xAnimator(duration:number, easing:EasingFunction):ObjectAnimator { + + let animatorX = ObjectAnimator.ofFloat(this, "phaseX", 0, 1); + animatorX.setInterpolator(easing); + animatorX.setDuration(duration); + + return animatorX; + } + + private yAnimator(duration:number, easing:EasingFunction):ObjectAnimator { + + let animatorY = ObjectAnimator.ofFloat(this, "phaseY", 0, 1); + animatorY.setInterpolator(easing); + animatorY.setDuration(duration); + + return animatorY; + } + + /** + * Animates values along the X axis, in a linear fashion. + * + * @param durationMillis animation duration + */ + public animateX(durationMillis:number) { + animateX(durationMillis, Easing.Linear); + } + + /** + * Animates values along the X axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + + public animateX(durationMillis:number, easing:EasingFunction) { + + let animatorX = this.xAnimator(durationMillis, easing); + animatorX.addUpdateListener(mListener); + animatorX.start(); + } + + /** + * Animates values along both the X and Y axes, in a linear fashion. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + */ + + public animateXY(durationMillisX:number,durationMillisY:number) { + animateXY(durationMillisX, durationMillisY, Easing.Linear, Easing.Linear); + } + + /** + * Animates values along both the X and Y axes. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + * @param easing EasingFunction for both axes + */ + + public animateXY(durationMillisX:number, durationMillisY:number, easing:EasingFunction) { + + let xAnimator = xAnimator(durationMillisX, easing); + let yAnimator = yAnimator(durationMillisY, easing); + + if (durationMillisX > durationMillisY) { + xAnimator.addUpdateListener(mListener); + } else { + yAnimator.addUpdateListener(mListener); + } + + xAnimator.start(); + yAnimator.start(); + } + + /** + * Animates values along both the X and Y axes. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + * @param easingX EasingFunction for the X axis + * @param easingY EasingFunction for the Y axis + */ + + public animateXY(durationMillisX:number, durationMillisY:number, easingX:EasingFunction, + easingY:EasingFunction) { + + let xAnimator = xAnimator(durationMillisX, easingX); + let yAnimator = yAnimator(durationMillisY, easingY); + + if (durationMillisX > durationMillisY) { + xAnimator.addUpdateListener(mListener); + } else { + yAnimator.addUpdateListener(mListener); + } + + xAnimator.start(); + yAnimator.start(); + } + + /** + * Animates values along the Y axis, in a linear fashion. + * + * @param durationMillis animation duration + */ + + public animateY(durationMillis:number) { + this.animateY(durationMillis, Easing.Linear); + } + + /** + * Animates values along the Y axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + + public animateY(durationMillis:number, easing:EasingFunction) { + + let animatorY:ObjectAnimator = yAnimator(durationMillis, easing); + animatorY.addUpdateListener(mListener); + animatorY.start(); + } + + /** + * Gets the Y axis phase of the animation. + * + * @return float value of {@link #mPhaseY} + */ + public getPhaseY():number { + return this.mPhaseY; + } + + /** + * Sets the Y axis phase of the animation. + * + * @param phase float value between 0 - 1 + */ + public setPhaseY(phase:number) { + if (phase > 1) { + phase = 1; + } else if (phase < 0) { + phase = 0; + } + this.mPhaseY = phase; + } + + /** + * Gets the X axis phase of the animation. + * + * @return float value of {@link #mPhaseX} + */ + public getPhaseX():number { + return this.mPhaseX; + } + + /** + * Sets the X axis phase of the animation. + * + * @param phase float value between 0 - 1 + */ + public setPhaseX(phase:number) { + if (phase > 1) { + phase = 1; + } else if (phase < 0) { + phase = 0; + } + this.mPhaseX = phase; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/Chart.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/Chart.ets new file mode 100644 index 0000000..b1a2b1d --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/Chart.ets @@ -0,0 +1,1687 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Runnable from '../data/Runnable'; +import {JArrayList} from '../utils/JArrayList'; +import ChartHighlighter from '../highlight/ChartHighlighter'; +import MyRect from '../data/Rect'; +import Highlight from '../highlight/Highlight'; +import ChartAnimator from '../animation/ChartAnimator'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import LegendRenderer from '../renderer/LegendRenderer'; +import Legend from '../components/Legend'; +import Description from '../components/Description'; +import {XAxis} from '../components/XAxis'; +import Paint, {TextPaint} from '../data/Paint'; +import DefaultValueFormatter from '../formatter/DefaultValueFormatter'; + +import IDataSet from '../interfaces/datasets/IDataSet' +import EntryOhos from '../data/EntryOhos' + +import OnChartValueSelectedListener from '../listener/OnChartValueSelectedListener' +import ChartTouchListener from '../listener/ChartTouchListener' +import OnChartGestureListener from '../listener/OnChartGestureListener' +import DataRenderer from '../renderer/DataRenderer' +import IHighlighter from '../highlight/IHighlighter' +import Utils from '../utils/Utils' +import {Color} from '../utils/ColorTemplate' +import MPPointF from '../utils/MPPointF' +import IMarker from '../components/IMarker' +import IValueFormatter from '../formatter/IValueFormatter' +import worker from '@ohos.worker'; + +/** + * Baseclass of all Chart-Views. + * + * @author Philipp Jahoda + */ +export default abstract class Chart { +// >> implements ChartInterface + + public static LOG_TAG: string = "ohos-MPChart"; + +/** + * flag that indicates if logging is enabled or not + */ + protected mLogEnabled: boolean = false; + +/** + * object that holds all data that was originally set for the chart, before + * it was modified or any filtering algorithms had been applied + */ + protected mData: T = null; + +/** + * Flag that indicates if highlighting per tap (touch) is enabled + */ + protected mHighLightPerTapEnabled: boolean = true; + +/** + * If set to true, chart continues to scroll after touch up + */ + private mDragDecelerationEnabled: boolean = true; + +/** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + */ + private mDragDecelerationFrictionCoef: number = 0.9; + +/** + * default value-formatter, number of digits depends on provided chart-data + */ + protected mDefaultValueFormatter: DefaultValueFormatter = new DefaultValueFormatter(0); + +/** + * paint object used for drawing the description text in the bottom right + * corner of the chart + */ + protected mDescPaint: Paint; + +/** + * paint object for drawing the information text when there are no values in + * the chart + */ + protected mInfoPaint: Paint; + +/** + * the object representing the labels on the x-axis + */ + protected mXAxis: XAxis; + +/** + * if true, touch gestures are enabled on the chart + */ + protected mTouchEnabled: boolean = true; + +/** + * the object responsible for representing the description text + */ + protected mDescription: Description; + +/** + * the legend object containing all data associated with the legend + */ + protected mLegend: Legend; + +/** + * listener that is called when a value on the chart is selected + */ + protected mSelectionListener: OnChartValueSelectedListener; +// @ts-ignore 泛型 + protected mChartTouchListener: ChartTouchListener; + +/** + * text that is displayed when the chart is empty + */ + private mNoDataText: string = "No chart data available."; + +/** + * Gesture listener for custom callbacks when making gestures on the chart. + */ + private mGestureListener: OnChartGestureListener; + protected mLegendRenderer: LegendRenderer; + +/** + * object responsible for rendering the data + */ + protected mRenderer: DataRenderer; + protected mHighlighter: IHighlighter; + +/** + * object that manages the bounds and drawing constraints of the chart + */ + protected mViewPortHandler: ViewPortHandler = new ViewPortHandler(); + +/** + * object responsible for animations + */ + protected mAnimator: ChartAnimator; + +/** + * Extra offsets to be appended to the viewport + */ + private mExtraTopOffset: number = 0 + private mExtraRightOffset: number = 0 + private mExtraBottomOffset: number = 0 + private mExtraLeftOffset: number = 0 + protected width: number = Utils.convertDpToPixel(50); + protected height: number = Utils.convertDpToPixel(50); + private background: Color | string | number | Resource; + private workerInstance = new worker.Worker("workers/worker.js", { name: "chart worker" }); + +/** + * default constructor for initialization in code + */ + constructor() { + this.init(); + } + +/** + * initialize all paints and stuff + */ + protected init() { + + this.mAnimator = new ChartAnimator(); + + // initialize the utils + Utils.init(); + this.mMaxHighlightDistance = Utils.convertDpToPixel(500); + + this.mDescription = new Description(); + this.mLegend = new Legend(); + + this.mLegendRenderer = new LegendRenderer(this.mViewPortHandler, this.mLegend); + + this.mXAxis = new XAxis(); + + this.mDescPaint = new TextPaint(); + + this.mInfoPaint = new Paint(); + this.mInfoPaint.setColor(Color.rgb(247, 189, 51)); // orange + this.mInfoPaint.setTextAlign(TextAlign.Center); + this.mInfoPaint.setTextSize(Utils.convertDpToPixel(12)); + + if (this.mLogEnabled) + console.info("Chart.init()") + } + +/** + * Sets a new data object for the chart. The data object contains all values + * and information needed for displaying. + * + * @param data + */ + public setData(data: T) { + + this.mData = data; + this.mOffsetsCalculated = false; + + if (data == null) { + return; + } + + // calculate how many digits are needed + // @ts-ignore 缺失方法 + this.setupDefaultFormatter(data.getYMin(), data.getYMax()); + + // @ts-ignore 缺失方法 + for (let item of this.mData.getDataSets()) { + if (item.needsFormatter() || item.getValueFormatter() == this.mDefaultValueFormatter) + item.setValueFormatter(this.mDefaultValueFormatter); + } + + // let the chart know there is new data + this.notifyDataSetChanged(); + + if (this.mLogEnabled) + console.info(Chart.LOG_TAG + ":Data is set.") + } + +/** + * Clears the chart from all data (sets it to null) and refreshes it (by + * calling invalidate()). + */ + public clear() { + this.mData = null; + this.mOffsetsCalculated = false; + this.mIndicesToHighlight = null; + this.mChartTouchListener.setLastHighlighted(null); + this.invalidate(); + } + +/** + * Removes all DataSets (and thereby Entries) from the chart. Does not set the data object to null. Also refreshes the + * chart by calling invalidate(). + */ + public clearValues() { + // @ts-ignore 缺失方法 + this.mData.clearValues(); + this.invalidate(); + } + +/** + * Returns true if the chart is empty (meaning it's data object is either + * null or contains no entries). + * + * @return + */ + public isEmpty(): boolean { + + if (this.mData == null) + return true; + else { + + // @ts-ignore 缺失方法 + if (this.mData.getEntryCount() <= 0) + return true; + else + return false; + } + } + +/** + * Lets the chart know its underlying data has changed and performs all + * necessary recalculations. It is crucial that this method is called + * everytime data is changed dynamically. Not calling this method can lead + * to crashes or unexpected behaviour. + */ + public abstract notifyDataSetChanged(); + +/** + * Calculates the offsets of the chart to the border depending on the + * position of an eventual legend or depending on the length of the y-axis + * and x-axis labels and their position + */ + protected abstract calculateOffsets(); + +/** + * Calculates the y-min and y-max value and the y-delta and x-delta value + */ + protected abstract calcMinMax(); + +/** + * Calculates the required number of digits for the values that might be + * drawn in the chart (if enabled), and creates the default-value-formatter + */ + protected setupDefaultFormatter(min: number, max: number) { + + let reference: number = 0; + + + // @ts-ignore 缺失方法 + if (this.mData == null || this.mData.getEntryCount() < 2) { + + reference = Math.max(Math.abs(min), Math.abs(max)); + } else { + reference = Math.abs(max - min); + } + + var digits: number = Utils.getDecimals(reference); + + // setup the formatter with a new number of digits + this.mDefaultValueFormatter.setup(digits); + } + +/** + * flag that indicates if offsets calculation has already been done or not + */ + private mOffsetsCalculated: boolean = false; + + protected onDraw(): Paint[] { + + if (this.mData == null) { + + let hasText: boolean = !this.strIsEmpty(this.mNoDataText); + + if (hasText) { + let pt: MPPointF = this.getCenter(); + + let textPaint: TextPaint = new TextPaint(); + textPaint.set(this.mInfoPaint); + + switch (this.mInfoPaint.getTextAlign()) { + case TextAlign.Start: + pt.x = 0; + break; + + case TextAlign.End: + pt.x *= 2.0; + break; + + default: + break; + } + textPaint.setText(this.mNoDataText) + textPaint.setX(pt.x) + textPaint.setY(pt.y) + return [textPaint] + } + + return []; + } + + if (!this.mOffsetsCalculated) { + + this.calculateOffsets(); + this.mOffsetsCalculated = true; + } + } + + public strIsEmpty(str: string): boolean { + return str == null || str.length == 0; + } + +/** + * Draws the description text in the bottom right corner of the chart (per default) + */ + protected drawDescription(): Paint[] { + + // check if description should be drawn + if (this.mDescription != null && this.mDescription.isEnabled()) { + + let position: MPPointF = this.mDescription.getPosition(); + + this.mDescPaint.setTypeface(this.mDescription.getTypeface()); + this.mDescPaint.setTextSize(this.mDescription.getTextSize()); + this.mDescPaint.setColor(this.mDescription.getTextColor()); + this.mDescPaint.setTextAlign(this.mDescription.getTextAlign()); + + let x, y: number = 0; + + // if no position specified, draw on default position + if (position == null) { + x = this.width - this.mViewPortHandler.offsetRight() - this.mDescription.getXOffset(); + y = this.height - this.mViewPortHandler.offsetBottom() - this.mDescription.getYOffset(); + } else { + x = position.x; + y = position.y; + } + + (this.mDescPaint as TextPaint).setText(this.mDescription.getText()); + this.mDescPaint.setX(x); + this.mDescPaint.setY(y); + return [this.mDescPaint]; + } + + return []; + } + +/** + * ################ ################ ################ ################ + */ +/** BELOW THIS CODE FOR HIGHLIGHTING */ + +/** + * array of Highlight objects that reference the highlighted slices in the + * chart + */ + protected mIndicesToHighlight: Highlight[]; + +/** + * The maximum distance in dp away from an entry causing it to highlight. + */ + protected mMaxHighlightDistance: number = 0; + + public getMaxHighlightDistance(): number { + return this.mMaxHighlightDistance; + } + +/** + * Sets the maximum distance in screen dp a touch can be away from an entry to cause it to get highlighted. + * Default: 500dp + * + * @param distDp + */ + public setMaxHighlightDistance(distDp: number) { + this.mMaxHighlightDistance = Utils.convertDpToPixel(distDp); + } + +/** + * Returns the array of currently highlighted values. This might a null or + * empty array if nothing is highlighted. + * + * @return + */ + public getHighlighted(): Highlight[] { + return this.mIndicesToHighlight; + } + +/** + * Returns true if values can be highlighted via tap gesture, false if not. + * + * @return + */ + public isHighlightPerTapEnabled(): boolean { + return this.mHighLightPerTapEnabled; + } + +/** + * Set this to false to prevent values from being highlighted by tap gesture. + * Values can still be highlighted via drag or programmatically. Default: true + * + * @param enabled + */ + public setHighlightPerTapEnabled(enabled: boolean) { + this.mHighLightPerTapEnabled = enabled; + } + +/** + * Returns true if there are values to highlight, false if there are no + * values to highlight. Checks if the highlight array is null, has a length + * of zero or if the first object is null. + * + * @return + */ + public valuesToHighlight(): boolean { + return this.mIndicesToHighlight == null || this.mIndicesToHighlight.length <= 0 + || this.mIndicesToHighlight[0] == null ? false + : true; + } + +/** + * Sets the last highlighted value for the touchlistener. + * + * @param highs + */ + protected setLastHighlighted(highs: Highlight[]) { + + if (highs == null || highs.length <= 0 || highs[0] == null) { + this.mChartTouchListener.setLastHighlighted(null); + } else { + this.mChartTouchListener.setLastHighlighted(highs[0]); + } + } + +/** + * Highlights the values at the given indices in the given DataSets. Provide + * null or an empty array to undo all highlighting. This should be used to + * programmatically highlight values. + * This method *will not* call the listener. + * + * @param highs + */ + public highlightValuesForArray(highs: Highlight[]) { + + // set the indices to highlight + this.mIndicesToHighlight = highs; + + this.setLastHighlighted(highs); + + // redraw the chart + this.invalidate(); + } + +/** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + * @param callListener Should the listener be called for this change + */ + public highlightValue(x: number, y?: number, dataSetIndex?: number, dataIndex?: number, callListener?: boolean) { + if (y == null || y == undefined) { + y = NaN; + } + if (dataIndex == null || dataIndex == undefined) { + dataIndex = -1; + } + if (callListener == null || callListener == undefined) { + callListener = true; + } + // @ts-ignore 缺失方法 + if (dataSetIndex < 0 || dataSetIndex >= this.mData.getDataSetCount()) { + this.highlightValueForObject(null, callListener); + } else { + this.highlightValueForObject(new Highlight(x, y, dataSetIndex, dataIndex), callListener); + } + } + +/** + * Highlights the value selected by touch gesture. Unlike + * highlightValues(...), this generates a callback to the + * OnChartValueSelectedListener. + * + * @param high - the highlight object + * @param callListener - call the listener + */ + public highlightValueForObject(high: Highlight, callListener?: boolean) { + + if (callListener == null || callListener == undefined) { + callListener = true; + } + let e: EntryOhos = null; + + if (high == null) + this.mIndicesToHighlight = null; + else { + + if (this.mLogEnabled) + console.info(Chart.LOG_TAG + ":Highlighted: " + high.toString()) + + // @ts-ignore 缺失方法 + e = this.mData.getEntryForHighlight(high); + if (e == null) { + this.mIndicesToHighlight = null; + high = null; + } else { + + // set the indices to highlight + this.mIndicesToHighlight = [high]; + } + } + this.setLastHighlighted(this.mIndicesToHighlight); + + if (callListener && this.mSelectionListener != null) { + + if (!this.valuesToHighlight()) + this.mSelectionListener.onNothingSelected(); + else { + // notify the listener + this.mSelectionListener.onValueSelected(e, high); + } + } + + // redraw the chart + this.invalidate(); + } + + public abstract invalidate(); + +/** + * Returns the Highlight object (contains x-index and DataSet index) of the + * selected value at the given touch point inside the Line-, Scatter-, or + * CandleStick-Chart. + * + * @param x + * @param y + * @return + */ + public getHighlightByTouchPoint(x: number, y: number): Highlight { + + if (this.mData == null) { + console.error(Chart.LOG_TAG + ":Can't select by touch. No data set.") + return null; + } else + return this.getHighlighter().getHighlight(x, y); + } + +/** + * Set a new (e.g. custom) ChartTouchListener NOTE: make sure to + * setTouchEnabled(true); if you need touch gestures on the chart + * + * @param l + */ +// @ts-ignore 泛型 + public setOnTouchListener(l: ChartTouchListener) { + this.mChartTouchListener = l; + } + +/** + * Returns an instance of the currently active touch listener. + * + * @return + */ +// @ts-ignore 泛型 + public getOnTouchListener(): ChartTouchListener { + return this.mChartTouchListener; + } + +/** + * ################ ################ ################ ################ + */ +/** BELOW CODE IS FOR THE MARKER VIEW */ + +/** + * if set to true, the marker view is drawn when a value is clicked + */ + protected mDrawMarkers: boolean = true; + +/** + * the view that represents the marker + */ + protected mMarker: IMarker; + +/** + * draws all MarkerViews on the highlighted positions + */ + protected drawMarkers(): Paint[]{ + + let paints: Paint[] = []; + + // if there is no marker view or drawing marker is disabled + if (this.mMarker == null || !this.isDrawMarkersEnabled() || !this.valuesToHighlight()) + return; + + for (let i = 0; i < this.mIndicesToHighlight.length; i++) { + + let highlight: Highlight = this.mIndicesToHighlight[i]; + + // @ts-ignore 缺失方法 泛型问题 + let dataSet: IDataSet = this.mData.getDataSetByIndex(highlight.getDataSetIndex()); + + // @ts-ignore 缺失方法 + let e: EntryOhos = this.mData.getEntryForHighlight(this.mIndicesToHighlight[i]); + let entryIndex: number = dataSet.getEntryIndex(e); + + // make sure entry not null + if (e == null || entryIndex > dataSet.getEntryCount() * this.mAnimator.getPhaseX()) + continue; + + let pos: number[] = this.getMarkerPosition(highlight); + + // check bounds + if (!this.mViewPortHandler.isInBounds(pos[0], pos[1])) + continue; + + // callbacks to update the content + this.mMarker.refreshContent(e, highlight); + + // draw the marker + paints.concat(this.mMarker.draw(pos[0], pos[1])); + } + return paints; + } + +/** + * Returns the actual position in pixels of the MarkerView for the given + * Highlight object. + * + * @param high + * @return + */ + protected getMarkerPosition(high: Highlight): number[] { + return [high.getDrawX(), high.getDrawY()]; + } + + +/** CODE BELOW THIS RELATED TO ANIMATION */ + +/** + * Returns the animator responsible for animating chart values. + * + * @return + */ + public getAnimator(): ChartAnimator { + return this.mAnimator; + } + +/** + * If set to true, chart continues to scroll after touch up default: true + */ + public isDragDecelerationEnabled(): boolean { + return this.mDragDecelerationEnabled; + } + +/** + * If set to true, chart continues to scroll after touch up. Default: true. + * + * @param enabled + */ + public setDragDecelerationEnabled(enabled: boolean) { + this.mDragDecelerationEnabled = enabled; + } + +/** + * Returns drag deceleration friction coefficient + * + * @return + */ + public getDragDecelerationFrictionCoef(): number { + return this.mDragDecelerationFrictionCoef; + } + +/** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + * + * @param newValue + */ + public setDragDecelerationFrictionCoef(newValue: number) { + + if (newValue < 0) + newValue = 0; + + if (newValue >= 1) + newValue = 0.999; + + this.mDragDecelerationFrictionCoef = newValue; + } + + +/** CODE BELOW FOR PROVIDING EASING FUNCTIONS */ +// +// /** +// * Animates the drawing / rendering of the chart on both x- and y-axis with +// * the specified animation time. If animate(...) is called, no further +// * calling of invalidate() is necessary to refresh the chart. ANIMATIONS +// * +// * @param durationMillisX +// * @param durationMillisY +// * @param easingX a custom easing function to be used on the animation phase +// * @param easingY a custom easing function to be used on the animation phase +// */ +// @RequiresApi(11) +// public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX, +// EasingFunction easingY) { +// mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY); +// } +// +// /** +// * Animates the drawing / rendering of the chart on both x- and y-axis with +// * the specified animation time. If animate(...) is called, no further +// * calling of invalidate() is necessary to refresh the chart. ANIMATIONS +// * +// * @param durationMillisX +// * @param durationMillisY +// * @param easing a custom easing function to be used on the animation phase +// */ +// @RequiresApi(11) +// public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) { +// mAnimator.animateXY(durationMillisX, durationMillisY, easing); +// } + +// /** +// * Animates the rendering of the chart on the x-axis with the specified +// * animation time. If animate(...) is called, no further calling of +// * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR +// * +// * @param durationMillis +// * @param easing a custom easing function to be used on the animation phase +// */ +// @RequiresApi(11) +// public void animateX(int durationMillis, EasingFunction easing) { +// mAnimator.animateX(durationMillis, easing); +// } + +// /** +// * Animates the rendering of the chart on the y-axis with the specified +// * animation time. If animate(...) is called, no further calling of +// * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR +// * +// * @param durationMillis +// * @param easing a custom easing function to be used on the animation phase +// */ +// @RequiresApi(11) +// public void animateY(int durationMillis, EasingFunction easing) { +// mAnimator.animateY(durationMillis, easing); +// } +// +// /** +// * ################ ################ ################ ################ +// */ +// /** CODE BELOW FOR PREDEFINED EASING OPTIONS */ +// +// /** +// * ################ ################ ################ ################ +// */ +// /** CODE BELOW FOR ANIMATIONS WITHOUT EASING */ +// +// /** +// * Animates the rendering of the chart on the x-axis with the specified +// * animation time. If animate(...) is called, no further calling of +// * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR +// * +// * @param durationMillis +// */ +// @RequiresApi(11) +// public void animateX(int durationMillis) { +// mAnimator.animateX(durationMillis); +// } +// +// /** +// * Animates the rendering of the chart on the y-axis with the specified +// * animation time. If animate(...) is called, no further calling of +// * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR +// * +// * @param durationMillis +// */ +// @RequiresApi(11) +// public void animateY(int durationMillis) { +// mAnimator.animateY(durationMillis); +// } +// +// /** +// * Animates the drawing / rendering of the chart on both x- and y-axis with +// * the specified animation time. If animate(...) is called, no further +// * calling of invalidate() is necessary to refresh the chart. ANIMATIONS +// * +// * @param durationMillisX +// * @param durationMillisY +// */ +// @RequiresApi(11) +// public void animateXY(int durationMillisX, int durationMillisY) { +// mAnimator.animateXY(durationMillisX, durationMillisY); +// } +// +// /** +// * ################ ################ ################ ################ +// */ +// /** BELOW THIS ONLY GETTERS AND SETTERS */ +// +// +/** + * Returns the object representing all x-labels, this method can be used to + * acquire the XAxis object and modify it (e.g. change the position of the + * labels, styling, etc.) + * + * @return + */ + public getXAxis(): XAxis { + return this.mXAxis; + } + +/** + * Returns the default IValueFormatter that has been determined by the chart + * considering the provided minimum and maximum values. + * + * @return + */ + public getDefaultValueFormatter(): IValueFormatter { + return this.mDefaultValueFormatter; + } + +/** + * set a selection listener for the chart + * + * @param l + */ + public setOnChartValueSelectedListener(l: OnChartValueSelectedListener) { + this.mSelectionListener = l; + } + +/** + * Sets a gesture-listener for the chart for custom callbacks when executing + * gestures on the chart surface. + * + * @param l + */ + public setOnChartGestureListener(l: OnChartGestureListener) { + this.mGestureListener = l; + } + +/** + * Returns the custom gesture listener. + * + * @return + */ + public getOnChartGestureListener(): OnChartGestureListener { + return this.mGestureListener; + } + +/** + * returns the current y-max value across all DataSets + * + * @return + */ + public getYMax(): number { + // @ts-ignore 缺失方法 + return this.mData.getYMax(); + } + +/** + * returns the current y-min value across all DataSets + * + * @return + */ + public getYMin(): number { + // @ts-ignore 缺失方法 + return this.mData.getYMin(); + } + + public getXChartMax(): number { + return this.mXAxis.mAxisMaximum; + } + + public getXChartMin(): number { + return this.mXAxis.mAxisMinimum; + } + + public getXRange(): number { + return this.mXAxis.mAxisRange; + } + +/** + * Returns a recyclable MPPointF instance. + * Returns the center point of the chart (the whole View) in pixels. + * + * @return + */ + public getCenter(): MPPointF { + return MPPointF.getInstance(this.width / 2, this.height / 2); + } + + public getWidth(){ + return this.width + } + + public getHeight(){ + return this.height + } + +/** + * Returns a recyclable MPPointF instance. + * Returns the center of the chart taking offsets under consideration. + * (returns the center of the content rectangle) + * + * @return + */ + public getCenterOffsets(): MPPointF { + return this.mViewPortHandler.getContentCenter(); + } + +/** + * Sets extra offsets (around the chart view) to be appended to the + * auto-calculated offsets. + * + * @param left + * @param top + * @param right + * @param bottom + */ + public setExtraOffsets(left: number, top: number, right: number, bottom: number) { + this.setExtraLeftOffset(left); + this.setExtraTopOffset(top); + this.setExtraRightOffset(right); + this.setExtraBottomOffset(bottom); + } + +/** + * Set an extra offset to be appended to the viewport's top + */ + public setExtraTopOffset(offset: number) { + this.mExtraTopOffset = Utils.convertDpToPixel(offset); + } + +/** + * @return the extra offset to be appended to the viewport's top + */ + public getExtraTopOffset(): number { + return this.mExtraTopOffset; + } + +/** + * Set an extra offset to be appended to the viewport's right + */ + public setExtraRightOffset(offset: number) { + this.mExtraRightOffset = Utils.convertDpToPixel(offset); + } + +/** + * @return the extra offset to be appended to the viewport's right + */ + public getExtraRightOffset(): number { + return this.mExtraRightOffset; + } + +/** + * Set an extra offset to be appended to the viewport's bottom + */ + public setExtraBottomOffset(offset: number) { + this.mExtraBottomOffset = Utils.convertDpToPixel(offset); + } + +/** + * @return the extra offset to be appended to the viewport's bottom + */ + public getExtraBottomOffset(): number { + return this.mExtraBottomOffset; + } + +/** + * Set an extra offset to be appended to the viewport's left + */ + public setExtraLeftOffset(offset: number) { + this.mExtraLeftOffset = Utils.convertDpToPixel(offset); + } + +/** + * @return the extra offset to be appended to the viewport's left + */ + public getExtraLeftOffset(): number { + return this.mExtraLeftOffset; + } + +/** + * Set this to true to enable logcat outputs for the chart. Beware that + * logcat output decreases rendering performance. Default: disabled. + * + * @param enabled + */ + public setLogEnabled(enabled: boolean) { + this.mLogEnabled = enabled; + } + +/** + * Returns true if log-output is enabled for the chart, fals if not. + * + * @return + */ + public isLogEnabled(): boolean { + return this.mLogEnabled; + } + +/** + * Sets the text that informs the user that there is no data available with + * which to draw the chart. + * + * @param text + */ + public setNoDataText(text: string) { + this.mNoDataText = text; + } + +/** + * Sets the color of the no data text. + * + * @param color + */ + public setNoDataTextColor(color: number | string) { + this.mInfoPaint.setColor(color); + } + +/** + * Sets the typeface to be used for the no data text. + * + * @param tf + */ + public setNoDataTextTypeface(tf: FontWeight) { + this.mInfoPaint.setTypeface(tf); + } + +/** + * alignment of the no data text + * + * @param align + */ + public setNoDataTextAlignment(align: TextAlign) { + this.mInfoPaint.setTextAlign(align); + } + +/** + * Set this to false to disable all gestures and touches on the chart, + * default: true + * + * @param enabled + */ + public setTouchEnabled(enabled: boolean) { + this.mTouchEnabled = enabled; + } + +/** + * sets the marker that is displayed when a value is clicked on the chart + * + * @param marker + */ + public setMarker(marker: IMarker) { + this.mMarker = marker; + } + +/** + * returns the marker that is set as a marker view for the chart + * + * @return + */ + public getMarker(): IMarker { + return this.mMarker; + } + + public setMarkerView(v: IMarker) { + this.setMarker(v); + } + + public getMarkerView(): IMarker { + return this.getMarker(); + } + +/** + * Sets a new Description object for the chart. + * + * @param desc + */ + public setDescription(desc: Description) { + this.mDescription = desc; + } + +/** + * Returns the Description object of the chart that is responsible for holding all information related + * to the description text that is displayed in the bottom right corner of the chart (by default). + * + * @return + */ + public getDescription(): Description { + return this.mDescription; + } + +/** + * Returns the Legend object of the chart. This method can be used to get an + * instance of the legend in order to customize the automatically generated + * Legend. + * + * @return + */ + public getLegend(): Legend { + return this.mLegend; + } + +/** + * Returns the renderer object responsible for rendering / drawing the + * Legend. + * + * @return + */ + public getLegendRenderer(): LegendRenderer { + return this.mLegendRenderer; + } + +/** + * Returns the rectangle that defines the borders of the chart-value surface + * (into which the actual values are drawn). + * + * @return + */ + public getContentRect(): MyRect { + return this.mViewPortHandler.getContentRect(); + } + +/** + * disables intercept touchevents + */ +// public disableScroll() { +// ViewParent parent = getParent(); +// if (parent != null) +// parent.requestDisallowInterceptTouchEvent(true); +// } + +/** + * enables intercept touchevents + */ +// public enableScroll() { +// ViewParent parent = getParent(); +// if (parent != null) +// parent.requestDisallowInterceptTouchEvent(false); +// } + +/** + * paint for the grid background (only line and barchart) + */ + public static PAINT_GRID_BACKGROUND: number = 4; + +/** + * paint for the info text that is displayed when there are no values in the + * chart + */ + public static PAINT_INFO: number = 7; + +/** + * paint for the description text in the bottom right corner + */ + public static PAINT_DESCRIPTION: number = 11; + +/** + * paint for the hole in the middle of the pie chart + */ + public static PAINT_HOLE: number = 13; + +/** + * paint for the text in the middle of the pie chart + */ + public static PAINT_CENTER_TEXT: number = 14; + +/** + * paint used for the legend + */ + public static PAINT_LEGEND_LABEL: number = 18; + +/** + * set a new paint object for the specified parameter in the chart e.g. + * Chart.PAINT_VALUES + * + * @param p the new paint object + * @param which Chart.PAINT_VALUES, Chart.PAINT_GRID, Chart.PAINT_VALUES, + * ... + */ + public setPaint(p: Paint, which: number) { + + switch (which) { + case Chart.PAINT_INFO: + this.mInfoPaint = p; + break; + case Chart.PAINT_DESCRIPTION: + this.mDescPaint = p; + break; + } + } + +/** + * Returns the paint object associated with the provided constant. + * + * @param which e.g. Chart.PAINT_LEGEND_LABEL + * @return + */ + public getPaint(which: number): Paint { + switch (which) { + case Chart.PAINT_INFO: + return this.mInfoPaint; + case Chart.PAINT_DESCRIPTION: + return this.mDescPaint; + } + + return null; + } + + public isDrawMarkerViewsEnabled(): boolean { + return this.isDrawMarkersEnabled(); + } + + public setDrawMarkerViews(enabled: boolean) { + this.setDrawMarkers(enabled); + } + +/** + * returns true if drawing the marker is enabled when tapping on values + * (use the setMarker(IMarker marker) method to specify a marker) + * + * @return + */ + public isDrawMarkersEnabled(): boolean { + return this.mDrawMarkers; + } + +/** + * Set this to true to draw a user specified marker when tapping on + * chart values (use the setMarker(IMarker marker) method to specify a + * marker). Default: true + * + * @param enabled + */ + public setDrawMarkers(enabled: boolean) { + this.mDrawMarkers = enabled; + } + +/** + * Returns the ChartData object that has been set for the chart. + * + * @return + */ + public getData(): T { + return this.mData; + } + +/** + * Returns the ViewPortHandler of the chart that is responsible for the + * content area of the chart and its offsets and dimensions. + * + * @return + */ + public getViewPortHandler(): ViewPortHandler { + return this.mViewPortHandler; + } + +/** + * Returns the Renderer object the chart uses for drawing data. + * + * @return + */ + public getRenderer(): DataRenderer { + return this.mRenderer; + } + +/** + * Sets a new DataRenderer object for the chart. + * + * @param renderer + */ + public setRenderer(renderer: DataRenderer) { + + if (renderer != null) + this.mRenderer = renderer; + } + + public getHighlighter(): IHighlighter { + return this.mHighlighter; + } + +/** + * Sets a custom highligher object for the chart that handles / processes + * all highlight touch events performed on the chart-view. + * + * @param highlighter + */ +// @ts-ignore 泛型问题 + public setHighlighter(highlighter: ChartHighlighter) { + this.mHighlighter = highlighter; + } + +/** + * Returns a recyclable MPPointF instance. + * + * @return + */ + public getCenterOfView(): MPPointF { + return this.getCenter(); + } + +/** + * Returns the bitmap that represents the chart. + * + * @return + */ + public getChartBitmap(): Paint[] { + // Define a bitmap with the same size as the view + var returnedBitmap: Paint[] = []; + // Get the view's background + var background = this.getBackground(); + if (background == null) + // has background drawable, then draw it on the canvas + background = "#ffffff" + // draw the view on the canvas + // this.draw(); + // return the bitmap + return returnedBitmap; + } + + setBackground(background: Color | string | number | Resource) { + this.background = background; + } + + getBackground(): Color | string | number | Resource{ + return this.background; + } + +// /** +// * Saves the current chart state with the given name to the given path on +// * the sdcard leaving the path empty "" will put the saved file directly on +// * the SD card chart is saved as a PNG image, example: +// * saveToPath("myfilename", "foldername1/foldername2"); +// * +// * @param title +// * @param pathOnSD e.g. "folder1/folder2/folder3" +// * @return returns true on success, false on error +// */ +// public boolean saveToPath(String title, String pathOnSD) { +// +// +// +// Bitmap b = getChartBitmap(); +// +// OutputStream stream = null; +// try { +// stream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() +// + pathOnSD + "/" + title +// + ".png"); +// +// /* +// * Write bitmap to file using JPEG or PNG and 40% quality hint for +// * JPEG. +// */ +// b.compress(CompressFormat.PNG, 40, stream); +// +// stream.close(); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// +// return true; +// } + +// /** +// * Saves the current state of the chart to the gallery as an image type. The +// * compression must be set for JPEG only. 0 == maximum compression, 100 = low +// * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE +// * +// * @param fileName e.g. "my_image" +// * @param subFolderPath e.g. "ChartPics" +// * @param fileDescription e.g. "Chart details" +// * @param format e.g. Bitmap.CompressFormat.PNG +// * @param quality e.g. 50, min = 0, max = 100 +// * @return returns true if saving was successful, false if not +// */ +// public boolean saveToGallery(String fileName, String subFolderPath, String fileDescription, Bitmap.CompressFormat +// format, int quality) { +// // restrain quality +// if (quality < 0 || quality > 100) +// quality = 50; +// +// long currentTime = System.currentTimeMillis(); +// +// File extBaseDir = Environment.getExternalStorageDirectory(); +// File file = new File(extBaseDir.getAbsolutePath() + "/DCIM/" + subFolderPath); +// if (!file.exists()) { +// if (!file.mkdirs()) { +// return false; +// } +// } +// +// String mimeType = ""; +// switch (format) { +// case PNG: +// mimeType = "image/png"; +// if (!fileName.endsWith(".png")) +// fileName += ".png"; +// break; +// case WEBP: +// mimeType = "image/webp"; +// if (!fileName.endsWith(".webp")) +// fileName += ".webp"; +// break; +// case JPEG: +// default: +// mimeType = "image/jpeg"; +// if (!(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg"))) +// fileName += ".jpg"; +// break; +// } +// +// String filePath = file.getAbsolutePath() + "/" + fileName; +// FileOutputStream out = null; +// try { +// out = new FileOutputStream(filePath); +// +// Bitmap b = getChartBitmap(); +// b.compress(format, quality, out); +// +// out.flush(); +// out.close(); +// +// } catch (IOException e) { +// e.printStackTrace(); +// +// return false; +// } +// +// long size = new File(filePath).length(); +// +// ContentValues values = new ContentValues(8); +// +// // store the details +// values.put(Images.Media.TITLE, fileName); +// values.put(Images.Media.DISPLAY_NAME, fileName); +// values.put(Images.Media.DATE_ADDED, currentTime); +// values.put(Images.Media.MIME_TYPE, mimeType); +// values.put(Images.Media.DESCRIPTION, fileDescription); +// values.put(Images.Media.ORIENTATION, 0); +// values.put(Images.Media.DATA, filePath); +// values.put(Images.Media.SIZE, size); +// +// return getContext().getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values) != null; +// } +// +// /** +// * Saves the current state of the chart to the gallery as a JPEG image. The +// * filename and compression can be set. 0 == maximum compression, 100 = low +// * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE +// * +// * @param fileName e.g. "my_image" +// * @param quality e.g. 50, min = 0, max = 100 +// * @return returns true if saving was successful, false if not +// */ +// public boolean saveToGallery(String fileName, int quality) { +// return saveToGallery(fileName, "", "ohos-MPChart-Library Save", Bitmap.CompressFormat.PNG, quality); +// } +// +// /** +// * Saves the current state of the chart to the gallery as a PNG image. +// * NOTE: Needs permission WRITE_EXTERNAL_STORAGE +// * +// * @param fileName e.g. "my_image" +// * @return returns true if saving was successful, false if not +// */ +// public boolean saveToGallery(String fileName) { +// return saveToGallery(fileName, "", "ohos-MPChart-Library Save", Bitmap.CompressFormat.PNG, 40); +// } +// +/** + * tasks to be done after the view is setup + */ + protected mJobs: JArrayList = new JArrayList(); + + public removeViewportJob(job: Runnable) { + // @ts-ignore + this.workerInstance.removeEventListener(job._type) + this.mJobs.remove(job); + } + + public clearAllViewportJobs() { + // @ts-ignore EventListener + this.workerInstance.removeAllListener(); + this.mJobs.clear(); + } + +/** + * Either posts a job immediately if the chart has already setup it's + * dimensions or adds the job to the execution queue. + * + * @param job + */ + public addViewportJob(job: Runnable) { + + if (this.mViewPortHandler.hasChartDimens()) { + this.post(job); + } else { + this.mJobs.add(job); + } + } + + protected post(job: Runnable) { + // @ts-ignore + this.workerInstance.dispatchEvent({ type: job._type }); + } + +/** + * Returns all jobs that are scheduled to be executed after + * onSizeChanged(...). + * + * @return + */ + public getJobs(): JArrayList { + return this.mJobs; + } + +// @Override +// protected void onLayout(boolean changed, int left, int top, int right, int bottom) { +// +// for (int i = 0; i < getChildCount(); i++) { +// getChildAt(i).layout(left, top, right, bottom); +// } +// } +// +// @Override +// protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { +// super.onMeasure(widthMeasureSpec, heightMeasureSpec); +// int size = (int) Utils.convertDpToPixel(50f); +// setMeasuredDimension( +// Math.max(getSuggestedMinimumWidth(), +// resolveSize(size, +// widthMeasureSpec)), +// Math.max(getSuggestedMinimumHeight(), +// resolveSize(size, +// heightMeasureSpec))); +// } +// +// @Override +// protected void onSizeChanged(int w, int h, int oldw, int oldh) { +// if (mLogEnabled) +// Log.i(LOG_TAG, "OnSizeChanged()"); +// +// if (w > 0 && h > 0 && w < 10000 && h < 10000) { +// if (mLogEnabled) +// Log.i(LOG_TAG, "Setting chart dimens, width: " + w + ", height: " + h); +// mViewPortHandler.setChartDimens(w, h); +// } else { +// if (mLogEnabled) +// Log.w(LOG_TAG, "*Avoiding* setting chart dimens! width: " + w + ", height: " + h); +// } +// +// // This may cause the chart view to mutate properties affecting the view port -- +// // lets do this before we try to run any pending jobs on the view port itself +// notifyDataSetChanged(); +// +// for (Runnable r : mJobs) { +// post(r); +// } +// +// mJobs.clear(); +// +// super.onSizeChanged(w, h, oldw, oldh); +// } +// +/** + * Setting this to true will set the layer-type HARDWARE for the view, false + * will set layer-type SOFTWARE. + * + * @param enabled + */ + public setHardwareAccelerationEnabled(enabled: boolean) { + + // if (enabled) + // setLayerType(View.LAYER_TYPE_HARDWARE, null); + // else + // setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + +// @Override +// protected void onDetachedFromWindow() { +// super.onDetachedFromWindow(); +// +// //Log.i(LOG_TAG, "Detaching..."); +// +// if (mUnbind) +// unbindDrawables(this); +// } + +/** + * unbind flag + */ + private mUnbind: boolean = false; + +// /** +// * Unbind all drawables to avoid memory leaks. +// * Link: http://stackoverflow.com/a/6779164/1590502 +// * +// * @param view +// */ +// private void unbindDrawables(View view) { +// +// if (view.getBackground() != null) { +// view.getBackground().setCallback(null); +// } +// if (view instanceof ViewGroup) { +// for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { +// unbindDrawables(((ViewGroup) view).getChildAt(i)); +// } +// ((ViewGroup) view).removeAllViews(); +// } +// } +// +/** + * Set this to true to enable "unbinding" of drawables. When a View is detached + * from a window. This helps avoid memory leaks. + * Default: false + * Link: http://stackoverflow.com/a/6779164/1590502 + * + * @param enabled + */ + public setUnbindEnabled(enabled: boolean) { + this.mUnbind = enabled; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/LineChart.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/LineChart.ets new file mode 100644 index 0000000..884fe83 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/charts/LineChart.ets @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../data/EntryOhos'; +import { LineDataSet } from '../data/LineDataSet'; +import Runnable from '../data/Runnable'; +import LegendEntry from '../components/LegendEntry'; +import { JArrayList } from '../utils/JArrayList'; +import LegendView from '../components/LegendView'; +import Legend, {LegendForm, LegendVerticalAlignment} from '../components/Legend'; +import MyRect from '../data/Rect'; +import { TextPaint } from '../data/Paint'; +import LineData from '../data/LineData'; +import {XAxis} from '../components/XAxis'; +import XAxisView from '../components/renderer/XAxisView'; +import YAxisView from '../components/renderer/YAxisView' +import YAxis,{AxisDependency} from '../components/YAxis' +import PathView,{ PathViewModel } from '../components/PathView' +import Utils from '../utils/Utils' +import YAxisRenderer from '../renderer/YAxisRenderer' +import Transformer from '../utils/Transformer' +import ViewPortHandler from '../utils/ViewPortHandler' + +@Component +@Preview +export default struct LineChart { + + @State + xStartPoint:number[] = []; + @State + xEndPoint:number[] = []; + @State + yStartPoint:number[] = []; + @State + yEndPoint:number[] = []; + + @State + pathViewModel: PathViewModel = new PathViewModel(); + @State + lineChartModel: LineChartModel = new LineChartModel(); + + + + build() { + Column() { + Stack({ alignContent: Alignment.TopStart }) { + Stack({ alignContent: Alignment.TopStart }) { + XAxisView({ + scaleMode: this.lineChartModel.pathViewModel + }); + } + .clip(new Path().commands(this.lineChartModel.clipPath)) + .visibility(this.lineChartModel.isShowXAxis ? Visibility.Visible: Visibility.Hidden) + + Stack({ alignContent: Alignment.TopStart }) { + YAxisView({ model:this.lineChartModel.pathViewModel.leftAxisModel }) + YAxisView({ model: this.lineChartModel.pathViewModel.rightAxisModel }) + } + .visibility(this.lineChartModel.isShowXAxis ? Visibility.Visible: Visibility.Hidden) + + PathView({ model: this.lineChartModel.pathViewModel }) + }.backgroundColor(this.lineChartModel.rootViewBgColor) + if (this.lineChartModel.isShowLegend) { + LegendView({ + model: this.lineChartModel.legendModel + }) + } + } + .width(this.lineChartModel.width) + .height(this.lineChartModel.height) + } + + public aboutToAppear() { + + this.initPathViewModel(); + + // 数据设置 setLegend + let entries : JArrayList = new JArrayList(); + for(let i=0; i; + +/** + * flag indicating the limit lines layer depth + */ + protected mDrawLimitLineBehindData: boolean = false; + +/** + * flag indicating the grid lines layer depth + */ + protected mDrawGridLinesBehindData: boolean = true; + +/** + * Extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + protected mSpaceMin: number= 0; + +/** + * Extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + protected mSpaceMax: number= 0; + +/** + * flag indicating that the axis-min value has been customized + */ + protected mCustomAxisMin: boolean= false; + +/** + * flag indicating that the axis-max value has been customized + */ + protected mCustomAxisMax: boolean = false; + +/** + * don't touch this direclty, use setter + */ + public mAxisMaximum: number= 0; + +/** + * don't touch this directly, use setter + */ + public mAxisMinimum: number = 0; + +/** + * the total range of values this axis covers + */ + public mAxisRange: number = 0; + private mAxisMinLabels: number = 2; + private mAxisMaxLabels: number = 25; + +/** + * The minumum number of labels on the axis + */ + public getAxisMinLabels(): number{ + return this.mAxisMinLabels; + } + +/** + * The minumum number of labels on the axis + */ + public setAxisMinLabels(labels: number): void { + if (labels > 0) + this.mAxisMinLabels = labels; + } + +/** + * The maximum number of labels on the axis + */ + public getAxisMaxLabels(): number { + return this.mAxisMaxLabels; + } + +/** + * The maximum number of labels on the axis + */ + public setAxisMaxLabels(labels: number): void { + if (labels > 0) + this.mAxisMaxLabels = labels; + } + +/** + * default constructor + */ + constructor() { + super(); + this.mTextSize = 10; + this.mXOffset = Utils.convertDpToPixel(5); + this.mYOffset = Utils.convertDpToPixel(5); + this.mLimitLines = new JArrayList(); + } + +/** + * Set this to true to enable drawing the grid lines for this axis. + * + * @param enabled + */ + public setDrawGridLines(enabled: boolean): void { + this.mDrawGridLines = enabled; + } + +/** + * Returns true if drawing grid lines is enabled for this axis. + * + * @return + */ + public isDrawGridLinesEnabled(): boolean { + return this.mDrawGridLines; + } + +/** + * Set this to true if the line alongside the axis should be drawn or not. + * + * @param enabled + */ + public setDrawAxisLine(enabled: boolean): void { + this.mDrawAxisLine = enabled; + } + +/** + * Returns true if the line alongside the axis should be drawn. + * + * @return + */ + public isDrawAxisLineEnabled(): boolean{ + return this.mDrawAxisLine; + } + +/** + * Centers the axis labels instead of drawing them at their original position. + * This is useful especially for grouped BarChart. + * + * @param enabled + */ + public setCenterAxisLabels(enabled: boolean): void { + this.mCenterAxisLabels = enabled; + } + + public isCenterAxisLabelsEnabled(): boolean { + return this.mCenterAxisLabels && this.mEntryCount > 0; + } + +/** + * Sets the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @param color + */ + public setGridColor(color: number): void { + this.mGridColor = color; + } + +/** + * Returns the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @return + */ + public getGridColor(): number { + return this.mGridColor; + } + +/** + * Sets the width of the border surrounding the chart in dp. + * + * @param width + */ + public setAxisLineWidth(width: number): void { + this.mAxisLineWidth = Utils.convertDpToPixel(width); + } + +/** + * Returns the width of the axis line (line alongside the axis). + * + * @return + */ + public getAxisLineWidth(): number{ + return this.mAxisLineWidth; + } + +/** + * Sets the width of the grid lines that are drawn away from each axis + * label. + * + * @param width + */ + public setGridLineWidth(width: number): void { + this.mGridLineWidth = Utils.convertDpToPixel(width); + } + +/** + * Returns the width of the grid lines that are drawn away from each axis + * label. + * + * @return + */ + public getGridLineWidth(): number { + return this.mGridLineWidth; + } + +/** + * Sets the color of the border surrounding the chart. + * + * @param color + */ + public setAxisLineColor(color: number): void { + this.mAxisLineColor = color; + } + +/** + * Returns the color of the axis line (line alongside the axis). + * + * @return + */ + public getAxisLineColor(): number { + return this.mAxisLineColor; + } + +/** + * Set this to true to enable drawing the labels of this axis (this will not + * affect drawing the grid lines or axis lines). + * + * @param enabled + */ + public setDrawLabels(enabled: boolean): void { + this.mDrawLabels = enabled; + } + +/** + * Returns true if drawing the labels is enabled for this axis. + * + * @return + */ + public isDrawLabelsEnabled(): boolean{ + return this.mDrawLabels; + } + +/** + * sets the number of label entries for the y-axis max = 25, min = 2, default: 6, be aware + * that this number is not + * fixed (if force == false) and can only be approximated. + * + * @param count the number of y-axis labels that should be displayed + * @param force if enabled, the set label count will be forced, meaning that the exact + * specified count of labels will + * be drawn and evenly distributed alongside the axis - this might cause labels + * to have uneven values + */ + public setLabelCount(count: number, force?: boolean): void { + if (count > this.getAxisMaxLabels()) + count = this.getAxisMaxLabels(); + if (count < this.getAxisMinLabels()) + count = this.getAxisMinLabels(); + this.mLabelCount = count; + this.mForceLabels = false; + if (force != null || force != undefined) { + this.mForceLabels = force; + } + } + +/** + * Returns true if focing the y-label count is enabled. Default: false + * + * @return + */ + public isForceLabelsEnabled(): boolean { + return this.mForceLabels; + } + +/** + * Returns the number of label entries the y-axis should have + * + * @return + */ + public getLabelCount(): number { + return this.mLabelCount; + } + +/** + * @return true if granularity is enabled + */ + public isGranularityEnabled(): boolean { + return this.mGranularityEnabled; + } + +/** + * Enabled/disable granularity control on axis value intervals. If enabled, the axis + * interval is not allowed to go below a certain granularity. Default: false + * + * @param enabled + */ + public setGranularityEnabled(enabled: boolean): void { + this.mGranularityEnabled = enabled; + } + +/** + * @return the minimum interval between axis values + */ + public getGranularity(): number{ + return this.mGranularity; + } + +/** + * Set a minimum interval for the axis when zooming in. The axis is not allowed to go below + * that limit. This can be used to avoid label duplicating when zooming in. + * + * @param granularity + */ + public setGranularity(granularity: number): void{ + this.mGranularity = granularity; + // set this to true if it was disabled, as it makes no sense to call this method with granularity disabled + this.mGranularityEnabled = true; + } + +/** + * Adds a new LimitLine to this axis. + * + * @param l + */ + public addLimitLine(l: LimitLine): void { + this.mLimitLines.add(l); + + if (this.mLimitLines.size() > 6) { + console.log("MPAndroiChart", + "Warning! You have more than 6 LimitLines on your axis, do you really want " + + "that?"); + } + } + +/** + * Removes the specified LimitLine from the axis. + * + * @param l + */ + public removeLimitLine(l: LimitLine): void { + this.mLimitLines.remove(l); + } + +/** + * Removes all LimitLines from the axis. + */ + public removeAllLimitLines(): void { + this.mLimitLines.clear(); + } + +/** + * Returns the LimitLines of this axis. + * + * @return + */ + public getLimitLines(): JArrayList { + return this.mLimitLines; + } + +/** + * If this is set to true, the LimitLines are drawn behind the actual data, + * otherwise on top. Default: false + * + * @param enabled + */ + public setDrawLimitLinesBehindData(enabled: boolean): void{ + this.mDrawLimitLineBehindData = enabled; + } + + public isDrawLimitLinesBehindDataEnabled(): boolean { + return this.mDrawLimitLineBehindData; + } + +/** + * If this is set to false, the grid lines are draw on top of the actual data, + * otherwise behind. Default: true + * + * @param enabled + */ + public setDrawGridLinesBehindData(enabled: boolean): void { + this.mDrawGridLinesBehindData = enabled; + } + + public isDrawGridLinesBehindDataEnabled(): boolean { + return this.mDrawGridLinesBehindData; + } + +/** + * Returns the longest formatted label (in terms of characters), this axis + * contains. + * + * @return + */ + public getLongestLabel(): string { + var longest: string = ""; + for (var i = 0; i < this.mEntries.length; i++) { + var text: string = this.getFormattedLabel(i); + if (text && text != null && longest.length < text.length) + longest = text; + } + + return longest; + } + + public getFormattedLabel(index: number): string{ + + if (index < 0 || index >= this.mEntries.length) + return ""; + else + return this.getValueFormatter().getFormattedValue(this.mEntries[index], this); + } + +/** + * Sets the formatter to be used for formatting the axis labels. If no formatter is set, the + * chart will + * automatically determine a reasonable formatting (concerning decimals) for all the values + * that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter calculated by the chart. + * + * @param f + */ + public setValueFormatter(f: IAxisValueFormatter): void { + + if (!f || f == null) + this.mAxisValueFormatter = new DefaultAxisValueFormatter(this.mDecimals); + else + this.mAxisValueFormatter = f; + } + +/** + * Returns the formatter used for formatting the axis labels. + * + * @return + */ + public getValueFormatter(): IAxisValueFormatter { + + if (!this.mAxisValueFormatter || this.mAxisValueFormatter == null || + (this.mAxisValueFormatter instanceof DefaultAxisValueFormatter && + (this.mAxisValueFormatter as DefaultAxisValueFormatter).getDecimalDigits() != this.mDecimals)) + this.mAxisValueFormatter = new DefaultAxisValueFormatter(this.mDecimals); + + return this.mAxisValueFormatter; + } + +/** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public enableGridDashedLine(lineLength: number, spaceLength: number, phase: number): void { + this.mGridDashPathEffect = new DashPathEffect([lineLength, spaceLength], phase); + } + +/** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public setGridDashedLine(effect: DashPathEffect + ): void{ + this.mGridDashPathEffect = effect; + } + +/** + * Disables the grid line to be drawn in dashed mode. + */ + public disableGridDashedLine(): void { + this.mGridDashPathEffect = null; + } + +/** + * Returns true if the grid dashed-line effect is enabled, false if not. + * + * @return + */ + public isGridDashedLineEnabled(): boolean { + return this.mGridDashPathEffect == null ? false : true; + } + +/** + * returns the DashPathEffect that is set for grid line + * + * @return + */ + public getGridDashPathEffect(): DashPathEffect + { + return this.mGridDashPathEffect; + } + + +/** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public enableAxisLineDashedLine(lineLength: number, spaceLength: number, phase: number): void { + this.mAxisLineDashPathEffect = new DashPathEffect([lineLength, spaceLength],phase); + } + +/** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public setAxisLineDashedLine(effect: DashPathEffect ): void{ + this.mAxisLineDashPathEffect = effect; + } + +/** + * Disables the axis line to be drawn in dashed mode. + */ + public disableAxisLineDashedLine(): void { + this.mAxisLineDashPathEffect = null; + } + +/** + * Returns true if the axis dashed-line effect is enabled, false if not. + * + * @return + */ + public isAxisLineDashedLineEnabled(): boolean{ + return this.mAxisLineDashPathEffect == null ? false : true; + } + +/** + * returns the DashPathEffect that is set for axis line + * + * @return + */ + public getAxisLineDashPathEffect(): DashPathEffect + { + return this.mAxisLineDashPathEffect; + } + +/** + * ###### BELOW CODE RELATED TO CUSTOM AXIS VALUES ###### + */ + + public getAxisMaximum(): number { + return this.mAxisMaximum; + } + + public getAxisMinimum(): number { + return this.mAxisMinimum; + } + +/** + * By calling this method, any custom maximum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public resetAxisMaximum(): void { + this.mCustomAxisMax = false; + } + +/** + * Returns true if the axis max value has been customized (and is not calculated automatically) + * + * @return + */ + public isAxisMaxCustom(): boolean { + return this.mCustomAxisMax; + } + +/** + * By calling this method, any custom minimum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public resetAxisMinimum(): void { + this.mCustomAxisMin = false; + } + +/** + * Returns true if the axis min value has been customized (and is not calculated automatically) + * + * @return + */ + public isAxisMinCustom(): boolean{ + return this.mCustomAxisMin; + } + +/** + * Set a custom minimum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMinValue() to undo this. Do not forget to call + * setStartAtZero(false) if you use + * this method. Otherwise, the axis-minimum value will still be forced to 0. + * + * @param min + */ + public setAxisMinimum(min: number): void { + this.mCustomAxisMin = true; + this.mAxisMinimum = min; + this.mAxisRange = Math.abs(this.mAxisMaximum - min); + } + +/** + * Use setAxisMinimum(...) instead. + * + * @param min + */ + public setAxisMinValue(min: number): void { + this.setAxisMinimum(min); + } + +/** + * Set a custom maximum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMaxValue() to undo this. + * + * @param max + */ + public setAxisMaximum(max: number): void{ + this.mCustomAxisMax = true; + this.mAxisMaximum = max; + this.mAxisRange = Math.abs(max - this.mAxisMinimum); + } + +/** + * Use setAxisMaximum(...) instead. + * + * @param max + */ + public setAxisMaxValue(max: number): void { + this.setAxisMaximum(max); + } + +/** + * Calculates the minimum / maximum and range values of the axis with the given + * minimum and maximum values from the chart data. + * + * @param dataMin the min value according to chart data + * @param dataMax the max value according to chart data + */ + public calculate(dataMin: number, dataMax: number): void { + + // if custom, use value as is, else use data value + var min: number = this.mCustomAxisMin ? this.mAxisMinimum : (dataMin - this.mSpaceMin); + var max: number = this.mCustomAxisMax ? this.mAxisMaximum : (dataMax + this.mSpaceMax); + + // temporary range (before calculations) + var range: number = Math.abs(max - min); + + // in case all values are equal + if (range == 0) { + max = max + 1; + min = min - 1; + } + + this.mAxisMinimum = min; + this.mAxisMaximum = max; + + // actual range + this.mAxisRange = Math.abs(max - min); + } + +/** + * Gets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public getSpaceMin(): number{ + return this.mSpaceMin; + } + +/** + * Sets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public setSpaceMin(mSpaceMin: number): void{ + this.mSpaceMin = mSpaceMin; + } + +/** + * Gets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public getSpaceMax(): number{ + return this.mSpaceMax; + } + +/** + * Sets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public setSpaceMax(mSpaceMax: number): void { + this.mSpaceMax = mSpaceMax; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/ComponentBase.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/ComponentBase.ets new file mode 100644 index 0000000..5047924 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/ComponentBase.ets @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Utils from '../utils/Utils'; + +/** + * This class encapsulates everything both Axis, Legend and LimitLines have in common. + * + */ +export default abstract class ComponentBase { + /** + * flag that indicates if this axis / legend is enabled or not + */ + protected mEnabled:boolean = true; + + /** + * the offset in pixels this component has on the x-axis + */ + protected mXOffset:number = 5; + + /** + * the offset in pixels this component has on the Y-axis + */ + protected mYOffset:number = 5; + + /** + * the typeface used for the labels + */ + protected mTypeface:FontWeight/*Typeface*/ = FontWeight.Normal; + + /** + * the text size of the labels + */ + protected mTextSize:number = 10; + + /** + * the text color to use for the labels + */ + protected mTextColor:number = Color.Black; + + + constructor(){ + + } + + /** + * Returns the used offset on the x-axis for drawing the axis or legend + * labels. This offset is applied before and after the label. + * + * @return + */ + public getXOffset():number{ + return this.mXOffset; + } + + /** + * Sets the used x-axis offset for the labels on this axis. + * + * @param xOffset + */ + public setXOffset(xOffset:number):void{ + this.mXOffset = Utils.convertDpToPixel(xOffset); + } + + /** + * Returns the used offset on the x-axis for drawing the axis labels. This + * offset is applied before and after the label. + * + * @return + */ + public getYOffset():number { + return this.mYOffset; + } + + /** + * Sets the used y-axis offset for the labels on this axis. For the legend, + * higher offset means the legend as a whole will be placed further away + * from the top. + * + * @param yOffset + */ + public setYOffset(yOffset:number):void { + this.mYOffset = Utils.convertDpToPixel(yOffset); + } + + /** + * returns the Typeface used for the labels, returns null if none is set + * + * @return + */ + public getTypeface():FontWeight/*Typeface*/ { + return this.mTypeface; + } + + /** + * sets a specific Typeface for the labels + * + * @param tf + */ + public setTypeface( tf:FontWeight/*Typeface*/):void{ + this.mTypeface = tf; + } + + /** + * sets the size of the label text in density pixels min = 6f, max = 24f, default + * 10f + * + * @param size the text size, in DP + */ + public setTextSize(size:number):void { + + if (size > 24) + size = 24; + if (size < 6) + size = 6; + + this.mTextSize = size; + } + + /** + * returns the text size that is currently set for the labels, in pixels + * + * @return + */ + public getTextSize():number { + return this.mTextSize; + } + + + /** + * Sets the text color to use for the labels. Make sure to use + * getResources().getColor(...) when using a color from the resources. + * + * @param color + */ + public setTextColor(color:number):void { + this.mTextColor = color; + } + + /** + * Returns the text color that is set for the labels. + * + * @return + */ + public getTextColor():Color|string|number|Resource { + return this.mTextColor; + } + + /** + * Set this to true if this component should be enabled (should be drawn), + * false if not. If disabled, nothing of this component will be drawn. + * Default: true + * + * @param enabled + */ + public setEnabled(enabled:boolean):void { + this.mEnabled = enabled; + } + + /** + * Returns true if this comonent is enabled (should be drawn), false if not. + * + * @return + */ + public isEnabled():boolean { + return this.mEnabled; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Description.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Description.ets new file mode 100644 index 0000000..6fc4d27 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Description.ets @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MPPointF from '../utils/MPPointF'; +import Utils from '../utils/Utils'; +import ComponentBase from './ComponentBase'; + +export default class Description extends ComponentBase { + + /** + * the text used in the description + */ + private text:string = "Description Label"; + + /** + * the custom position of the description text + */ + private mPosition:MPPointF; + + /** + * the alignment of the description text + */ + private mTextAlign:TextAlign = TextAlign.End + + constructor(){ + super(); + // default size + this.mTextSize = Utils.convertDpToPixel(8); + } + + /** + * Sets the text to be shown as the description. + * + * @param text + */ + public setText( text:string):void { + this.text = text; + } + + /** + * Returns the description text. + * + * @return + */ + public getText():string{ + return this.text; + } + + /** + * Sets a custom position for the description text in pixels on the screen. + * + * @param x - xcoordinate + * @param y - ycoordinate + */ + public setPosition(x:number, y:number):void { + if (this.mPosition == null) { + this.mPosition = MPPointF.getInstance(x, y); + } else { + this.mPosition.x = x; + this.mPosition.y = y; + } + } + + /** + * Returns the customized position of the description, or null if none set. + * + * @return + */ + public getPosition():MPPointF { + return this.mPosition; + } + + /** + * Sets the text alignment of the description text. Default RIGHT. + * + * @param align + */ + public setTextAlign(align:TextAlign) { + this.mTextAlign = align; + } + + /** + * Returns the text alignment of the description. + * + * @return + */ + public getTextAlign():TextAlign { + return this.mTextAlign; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/IMarker.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/IMarker.ets new file mode 100644 index 0000000..f48702c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/IMarker.ets @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint from '../data/Paint'; +import EntryOhos from '../data/EntryOhos'; +import Highlight from '../highlight/Highlight'; +import MPPointF from '../utils/MPPointF'; + +export default interface IMarker { + + /** + * @return The desired (general) offset you wish the IMarker to have on the x- and y-axis. + * By returning x: -(width / 2) you will center the IMarker horizontally. + * By returning y: -(height / 2) you will center the IMarker vertically. + */ + getOffset():MPPointF; + + /** + * @return The offset for drawing at the specific `point`. This allows conditional adjusting of the Marker position. + * If you have no adjustments to make, return getOffset(). + * + * @param posX This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + * @param posY This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + */ + getOffsetForDrawingAtPoint(posX:number, posY:number):MPPointF; + + /** + * This method enables a specified custom IMarker to update it's content every time the IMarker is redrawn. + * + * @param e The Entry the IMarker belongs to. This can also be any subclass of Entry, like BarEntry or + * CandleEntry, simply cast it at runtime. + * @param highlight The highlight object contains information about the highlighted value such as it's dataset-index, the + * selected range or stack-index (only stacked bar entries). + */ + refreshContent(e:EntryOhos, highlight:Highlight):void; + + /** + * Draws the IMarker on the given position on the screen with the given Canvas object. + * + * @param canvas + * @param posX + * @param posY + */ + draw(posX:number, posY:number); +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Legend.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Legend.ets new file mode 100644 index 0000000..31b6b30 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/Legend.ets @@ -0,0 +1,997 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ColorTemplate from '../utils/ColorTemplate'; +import FSize from '../utils/FSize'; +import Utils from '../utils/Utils'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import {JArrayList} from '../utils/JArrayList'; + +import LegendEntry from './LegendEntry'; +import ComponentBase from './ComponentBase' +import Paint from '../data/Paint' + +export enum LegendHorizontalAlignment { + LEFT, CENTER, RIGHT +} + +export enum LegendVerticalAlignment { + TOP, CENTER, BOTTOM +} + +export enum LegendOrientation { + HORIZONTAL, VERTICAL +} + +export enum LegendDirection { + LEFT_TO_RIGHT, RIGHT_TO_LEFT +} +/** + * Class representing the legend of the chart. The legend will contain one entry + * per color and DataSet. Multiple colors in one DataSet are grouped together. + * The legend object is NOT available before setting data to the chart. + * + */ +export default class Legend extends ComponentBase { + /** + * The legend entries array + */ + private mEntries:LegendEntry[]= []; + + /** + * Entries that will be appended to the end of the auto calculated entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) + */ + private mExtraEntries:LegendEntry[]; + + /** + * Are the legend labels/colors a custom value or auto calculated? If false, + * then it's auto, if true, then custom. default false (automatic legend) + */ + private mIsLegendCustom:boolean = false; + + private mHorizontalAlignment:LegendHorizontalAlignment = LegendHorizontalAlignment.LEFT; + private mVerticalAlignment:LegendVerticalAlignment = LegendVerticalAlignment.BOTTOM; + private mOrientation:LegendOrientation= LegendOrientation.HORIZONTAL; + private mDrawInside:boolean = false; + + /** + * the text direction for the legend + */ + private mDirection:LegendDirection= LegendDirection.LEFT_TO_RIGHT; + + /** + * the shape/form the legend colors are drawn in + */ + private mShape:LegendForm = LegendForm.SQUARE; + + /** + * the size of the legend forms/shapes + */ + private mFormSize:number = 8; + + /** + * the size of the legend forms/shapes + */ + private mFormLineWidth:number = 3; + + /** + * Line dash path effect used for shapes that consist of lines. + */ + private mFormLineDashEffect:Object/*DashPathEffect*/ = null; + + /** + * the space between the legend entries on a horizontal axis, default 6f + */ + private mXEntrySpace:number = 6; + + /** + * the space between the legend entries on a vertical axis, default 5f + */ + private mYEntrySpace:number = 0; + + /** + * the space between the legend entries on a vertical axis, default 2f + * private float mYEntrySpace = 2f; /** the space between the form and the + * actual label/text + */ + private mFormToTextSpace:number = 5; + + /** + * the space that should be left between stacked forms + */ + private mStackSpace:number= 3; + + /** + * the maximum relative size out of the whole chart view in percent + */ + private mMaxSizePercent:number= 0.95; + + /** + * default constructor + */ + constructor(entries ?: LegendEntry[]){ + super(); + this.mTextSize = 20; + this.mXOffset = 5; + this.mYOffset = 3; + if (entries != null && entries != undefined) { + if(entries.length == 0){ + throw new Error("entries array is NULL"); + } + this.mEntries.concat(entries); + } + } + /** + * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. + * + * @param entries + */ + public setEntries(entries:JArrayList ):void { + this.mEntries = entries.toArray(new Array(entries.size())); + } + + public getEntries():LegendEntry[]{ + return this.mEntries; + } + + /** + * returns the maximum length in pixels across all legend labels + formsize + * + formtotextspace + * + * @param p the paint object used for rendering the text + * @return + */ + public getMaximumEntryWidth(p:Paint):number { + + var max:number = 0; + var maxFormSize:number = 0; + var formToTextSpace:number = Utils.convertDpToPixel(this.mFormToTextSpace); + + for (let entry of this.mEntries) { + var formSize:number = Utils.convertDpToPixel( + Number.isNaN(entry.formSize) + ? this.mFormSize : entry.formSize); + if (formSize > maxFormSize) + maxFormSize = formSize; + + var label:string= entry.label; + if (label == null) continue; + + var length:number =Utils.calcTextWidth(p, label); + + if (length > max) + max = length; + } + + return max + maxFormSize + formToTextSpace; + } + + /** + * returns the maximum height in pixels across all legend labels + * + * @param p the paint object used for rendering the text + * @return + */ + public getMaximumEntryHeight(p:Paint):number { + + var max:number = 0; + + for (let entry of this.mEntries) { + var label:string = entry.label; + if (label == null) continue; + + var length:number = Utils.calcTextHeight(p, label); + + if (length > max) + max = length; + } + + return max; + } + + public getExtraEntries(): LegendEntry[]{ + return this.mExtraEntries; + } + + /** + * Entries that will be appended to the end of the auto calculated + * entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() + * to let the changes take effect) + */ + public setExtra(colors ?: number[], labels ?: string[], entries ?: JArrayList) : void { + if (entries && !colors && !labels) { + this.mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + return; + } + + var entries : JArrayList = new JArrayList(); + + for (var i : number = 0; i < Math.min(colors.length, labels.length); i++) { + var entry : LegendEntry = new LegendEntry(); + entry.formColor = colors[i]; + entry.label = labels[i]; + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == 0) + entry.form = LegendForm.NONE; + else if (entry.formColor == ColorTemplate.COLOR_NONE) + entry.form = LegendForm.EMPTY; + + entries.add(entry); + } + + this.mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + /** + * Sets a custom legend's entries array. + * * A null label will start a group. + * This will disable the feature that automatically calculates the legend + * entries from the datasets. + * Call resetCustom() to re-enable automatic calculation (and then + * notifyDataSetChanged() is needed to auto-calculate the legend again) + */ + public setCustom(entries:JArrayList) : void { + this.mEntries = [] + for (var i : number = 0; i < entries.size(); i++) { + this.mEntries[i] = entries.at(i); + } + this.mIsLegendCustom = true; + } + + /** + * Calling this will disable the custom legend entries (set by + * setCustom(...)). Instead, the entries will again be calculated + * automatically (after notifyDataSetChanged() is called). + */ + public resetCustom() : void { + this.mIsLegendCustom = false; + } + + /** + * @return true if a custom legend entries has been set default + * false (automatic legend) + */ + public isLegendCustom() : boolean { + return this.mIsLegendCustom; + } + + /** + * returns the horizontal alignment of the legend + * + * @return + */ + public getHorizontalAlignment() : LegendHorizontalAlignment { + return this.mHorizontalAlignment; + } + + /** + * sets the horizontal alignment of the legend + * + * @param value + */ + public setHorizontalAlignment(value:LegendHorizontalAlignment) : void { + this.mHorizontalAlignment = value; + } + + /** + * returns the vertical alignment of the legend + * + * @return + */ + public getVerticalAlignment() : LegendVerticalAlignment { + return this.mVerticalAlignment; + } + + /** + * sets the vertical alignment of the legend + * + * @param value + */ + public setVerticalAlignment(value : LegendVerticalAlignment) : void { + this.mVerticalAlignment = value; + } + + /** + * returns the orientation of the legend + * + * @return + */ + public getOrientation() : LegendOrientation { + return this.mOrientation; + } + + /** + * sets the orientation of the legend + * + * @param value + */ + public setOrientation(value : LegendOrientation) : void { + this.mOrientation = value; + } + + /** + * returns whether the legend will draw inside the chart or outside + * + * @return + */ + public isDrawInsideEnabled() : boolean { + return this.mDrawInside; + } + + /** + * sets whether the legend will draw inside the chart or outside + * + * @param value + */ + public setDrawInside(value : boolean) : void { + this.mDrawInside = value; + } + + /** + * returns the text direction of the legend + * + * @return + */ + public getDirection() : LegendDirection { + return this.mDirection; + } + + /** + * sets the text direction of the legend + * + * @param pos + */ + public setDirection(pos : LegendDirection) : void { + this.mDirection = pos; + } + + /** + * returns the current form/shape that is set for the legend + * + * @return + */ + public getForm() : LegendForm { + return this.mShape; + } + + /** + * sets the form/shape of the legend forms + * + * @param shape + */ + public setForm(shape : LegendForm) : void { + this.mShape = shape; + } + + /** + * sets the size in dp of the legend forms, default 8f + * + * @param size + */ + public setFormSize(size : number) : void { + this.mFormSize = size; + } + + /** + * returns the size in dp of the legend forms + * + * @return + */ + public getFormSize() : number { + return this.mFormSize; + } + + /** + * sets the line width in dp for forms that consist of lines, default 3f + * + * @param size + */ + public setFormLineWidth(size : number) : void { + this.mFormLineWidth = size; + } + + /** + * returns the line width in dp for drawing forms that consist of lines + * + * @return + */ + public getFormLineWidth() : number { + return this.mFormLineWidth; + } + + /** + * Sets the line dash path effect used for shapes that consist of lines. + * + * @param dashPathEffect + */ + public setFormLineDashEffect(dashPathEffect : Object) : void { + this.mFormLineDashEffect = dashPathEffect; + } + + /** + * @return The line dash path effect used for shapes that consist of lines. + */ + public getFormLineDashEffect() : Object { + return this.mFormLineDashEffect; + } + + /** + * returns the space between the legend entries on a horizontal axis in + * pixels + * + * @return + */ + public getXEntrySpace() : number { + return this.mXEntrySpace; + } + + /** + * sets the space between the legend entries on a horizontal axis in pixels, + * converts to dp internally + * + * @param space + */ + public setXEntrySpace(space : number) : void { + this.mXEntrySpace = space; + } + + /** + * returns the space between the legend entries on a vertical axis in pixels + * + * @return + */ + public getYEntrySpace() : number { + return this.mYEntrySpace; + } + + /** + * sets the space between the legend entries on a vertical axis in pixels, + * converts to dp internally + * + * @param space + */ + public setYEntrySpace(space : number) : void { + this.mYEntrySpace = space; + } + + /** + * returns the space between the form and the actual label/text + * + * @return + */ + public getFormToTextSpace() : number { + return this.mFormToTextSpace; + } + + /** + * sets the space between the form and the actual label/text, converts to dp + * internally + * + * @param space + */ + public setFormToTextSpace(space : number) : void { + this.mFormToTextSpace = space; + } + + /** + * returns the space that is left out between stacked forms (with no label) + * + * @return + */ + public getStackSpace() : number { + return this.mStackSpace; + } + + /** + * sets the space that is left out between stacked forms (with no label) + * + * @param space + */ + public setStackSpace(space : number) : void { + this.mStackSpace = space; + } + + /** + * the total width of the legend (needed width space) + */ + public mNeededWidth : number = 0; + + /** + * the total height of the legend (needed height space) + */ + public mNeededHeight : number = 0; + + public mTextHeightMax : number = 0; + + public mTextWidthMax : number = 0; + + /** + * flag that indicates if word wrapping is enabled + */ + private mWordWrapEnabled : boolean = false; + + /** + * Should the legend word wrap? / this is currently supported only for: + * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word + * wrapping a legend takes a toll on performance. / you may want to set + * maxSizePercent when word wrapping, to set the point where the text wraps. + * / default: false + * + * @param enabled + */ + public setWordWrapEnabled(enabled : boolean) : void { + this.mWordWrapEnabled = enabled; + } + + /** + * If this is set, then word wrapping the legend is enabled. This means the + * legend will not be cut off if too long. + * + * @return + */ + public isWordWrapEnabled() : boolean { + return this.mWordWrapEnabled; + } + + /** + * The maximum relative size out of the whole chart view. / If the legend is + * to the right/left of the chart, then this affects the width of the + * legend. / If the legend is to the top/bottom of the chart, then this + * affects the height of the legend. / If the legend is the center of the + * piechart, then this defines the size of the rectangular bounds out of the + * size of the "hole". / default: 0.95f (95%) + * + * @return + */ + public getMaxSizePercent() : number { + return this.mMaxSizePercent; + } + + /** + * The maximum relative size out of the whole chart view. / If + * the legend is to the right/left of the chart, then this affects the width + * of the legend. / If the legend is to the top/bottom of the chart, then + * this affects the height of the legend. / default: 0.95f (95%) + * + * @param maxSize + */ + public setMaxSizePercent(maxSize : number) : void { + this.mMaxSizePercent = maxSize; + } + + private mCalculatedLabelSizes : JArrayList = new JArrayList(/*16*/); + private mCalculatedLabelBreakPoints : JArrayList = new JArrayList(/*16*/); + private mCalculatedLineSizes : JArrayList = new JArrayList(/*16*/); + + public getCalculatedLabelSizes() : JArrayList { + return this.mCalculatedLabelSizes; + } + + public getCalculatedLabelBreakPoints() : JArrayList { + return this.mCalculatedLabelBreakPoints; + } + + public getCalculatedLineSizes() : JArrayList { + return this.mCalculatedLineSizes; + } + + /** + * Calculates the dimensions of the Legend. This includes the maximum width + * and height of a single entry, as well as the total width and height of + * the Legend. + * + * @param labelpaint + */ + public calculateDimensions(labelpaint : Paint, viewPortHandler : ViewPortHandler) : void { + + var defaultFormSize: number = Utils.convertDpToPixel(this.mFormSize); + var stackSpace: number = Utils.convertDpToPixel(this.mStackSpace); + var formToTextSpace: number = Utils.convertDpToPixel(this.mFormToTextSpace); + var xEntrySpace: number = Utils.convertDpToPixel(this.mXEntrySpace); + var yEntrySpace: number = Utils.convertDpToPixel(this.mYEntrySpace); + var wordWrapEnabled: boolean = this.mWordWrapEnabled; + var entries: LegendEntry[] = this.mEntries; + var entryCount: number = entries.length; + + this.mTextWidthMax = this.getMaximumEntryWidth(labelpaint); + this.mTextHeightMax = this.getMaximumEntryHeight(labelpaint); + + switch (this.mOrientation) { + case LegendOrientation.VERTICAL: + { + + var maxWidth: number = 0; + var maxHeight: number = 0; + var width: number = 0; + var labelLineHeight: number = Utils.getLineHeight(labelpaint); + var wasStacked: boolean = false; + + for (var i: number = 0; i < entryCount; i++) { + + var e: LegendEntry = entries[i]; + var drawingForm: boolean = e.form != LegendForm.NONE; + var formSize: number = Number.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + var label: string = e.label; + + if (!wasStacked) + width = 0.1; + + if (drawingForm) { + if (wasStacked) + width += stackSpace; + width += formSize; + } + + // grouped forms have null labels + if (label != null) { + + // make a step to the left + if (drawingForm && !wasStacked) + width += formToTextSpace; + else if (wasStacked) { + maxWidth = Math.max(maxWidth, width); + maxHeight += labelLineHeight + yEntrySpace; + width = 0.1; + wasStacked = false; + } + + width += Utils.calcTextWidth(labelpaint, label); + + maxHeight += labelLineHeight + yEntrySpace; + } else { + wasStacked = true; + width += formSize; + if (i < entryCount - 1) + width += stackSpace; + } + + maxWidth = Math.max(maxWidth, width); + } + + this.mNeededWidth = maxWidth; + this.mNeededHeight = maxHeight; + + break; + } + case LegendOrientation.HORIZONTAL: + { + + var labelLineHeight: number = Utils.getLineHeight(labelpaint); + var labelLineSpacing: number = Utils.getLineSpacing(labelpaint) + yEntrySpace; + var contentWidth: number = viewPortHandler.contentWidth() * this.mMaxSizePercent; + + // Start calculating layout + var maxLineWidth: number = 0.0; + var currentLineWidth: number = 0.0; + var requiredWidth: number = 0.0; + var stackedStartIndex: number = -1; + + this.mCalculatedLabelBreakPoints.clear(); + this.mCalculatedLabelSizes.clear(); + this.mCalculatedLineSizes.clear(); + + for (var i: number = 0; i < entryCount; i++) { + + var e: LegendEntry = entries[i]; + var drawingForm: boolean = e.form != LegendForm.NONE; + var formSize: number = Number.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + var label: string = e.label; + + this.mCalculatedLabelBreakPoints.add(false); + + if (stackedStartIndex == -1) { + // we are not stacking, so required width is for this label + // only + requiredWidth = 0.0; + } else { + // add the spacing appropriate for stacked labels/forms + requiredWidth += stackSpace; + } + + // grouped forms have null labels + if (label != null) { + + + this.mCalculatedLabelSizes.add(Utils.calcTextSize(labelpaint, label)); // ��ǩ��length + requiredWidth += drawingForm ? formToTextSpace + formSize : 0.0; + requiredWidth += this.mCalculatedLabelSizes.get(i).width; + } else { + + this.mCalculatedLabelSizes.add(FSize.getInstance(0.0, 0.0)); + requiredWidth += drawingForm ? formSize : 0.0; + + if (stackedStartIndex == -1) { + // mark this index as we might want to break here later + stackedStartIndex = i; + } + } + + if (label != null || i == entryCount - 1) { + + var requiredSpacing: number = currentLineWidth == 0.0 ? 0.0 : xEntrySpace; + + if (!wordWrapEnabled // No word wrapping, it must fit. + // The line is empty, it must fit + || currentLineWidth == 0.0 + // It simply fits + || (contentWidth - currentLineWidth >= + requiredSpacing + requiredWidth)) { + // Expand current line + currentLineWidth += requiredSpacing + requiredWidth; + } else { // It doesn't fit, we need to wrap a line + + // Add current line size to array + this.mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + + // Start a new line + if (stackedStartIndex > -1) { + this.mCalculatedLabelBreakPoints.append(true); + } + + currentLineWidth = requiredWidth; + } + + if (i == entryCount - 1) { + // Add last line size to array + this.mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + } + } + + stackedStartIndex = label != null ? -1 : stackedStartIndex; + } + + this.mNeededWidth = maxLineWidth; + this.mNeededHeight = labelLineHeight + * this.mCalculatedLineSizes.size() + + labelLineSpacing * + (this.mCalculatedLineSizes.size() == 0 + ? 0 + : (this.mCalculatedLineSizes.size() - 1)); + + break; + } + } + + this.mNeededHeight += this.mYOffset; + this.mNeededWidth += this.mXOffset; + } + public getMaxEntryHeight():number { + var max:number = 0; + for (let entry of this.mEntries) { + var label:string = entry.label; + if (label == null) continue; + var length:number = this.getTextSize(); + if (length > max) + max = length; + } + return max; + } + public getMaxEntryWidth():number { + var max:number = 0; + var maxFormSize:number = 0; + var formToTextSpace:number = this.mFormToTextSpace; + for (let entry of this.mEntries) { + var formSize:number = Utils.convertDpToPixel(Number.isNaN(entry.formSize) ? this.mFormSize : entry.formSize); + if (formSize > maxFormSize) + maxFormSize = formSize; + var label:string= entry.label; + if (label == null) continue; + var length:number = label.length * this.getTextSize() / 2; + if (length > max) + max = length; + } + return max + maxFormSize + formToTextSpace; + } + public calculateNeededSizes(viewPortHandler : ViewPortHandler) : void { + var defaultFormSize : number = Utils.convertDpToPixel(this.mFormSize); + var stackSpace : number = this.mStackSpace; + var formToTextSpace : number = this.mFormToTextSpace; + var xEntrySpace : number = this.mXEntrySpace; + var yEntrySpace : number = this.mYEntrySpace; + var wordWrapEnabled : boolean = this.mWordWrapEnabled; + var entries : LegendEntry[] = this.mEntries; + var entryCount : number = entries.length; + console.log("calculateNeededSizes: mFormSize("+this.mFormSize+")+mStackSpace("+this.mStackSpace+")+mFormToTextSpace("+this.mFormToTextSpace+")+mXEntrySpace("+this.mXEntrySpace+")+mWordWrapEnabled("+this.mWordWrapEnabled+")+xoffset("+this.mXOffset+")+yOffset("+this.mYOffset+")"); + this.mTextWidthMax = this.getMaxEntryWidth(); + this.mTextHeightMax = this.getMaxEntryHeight(); + switch (this.mOrientation) { + case LegendOrientation.VERTICAL: { + var maxWidth : number = 0; + var maxHeight : number = 0; + var width : number = 0; + var labelLineHeight : number = this.getTextSize(); + var wasStacked : boolean = false; + for (var i : number = 0; i < entryCount; i++) { + var e : LegendEntry = entries[i]; + var drawingForm : boolean = e.form != LegendForm.NONE; + var formSize : number = Number.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + var label : string = e.label; + if (!wasStacked) { + width = 0.0; + } + if (drawingForm) { + if (wasStacked) { + width += stackSpace; + } + width += formSize; + } + // grouped forms have null labels + if (label != null) { + // make a step to the left + if (drawingForm && !wasStacked) { + width += formToTextSpace; + } + else if (wasStacked) { + maxWidth = Math.max(maxWidth, width); + maxHeight += labelLineHeight + yEntrySpace; + width = 0.1; + wasStacked = false; + } + width += label.length * this.getTextSize() / 2; + maxHeight += labelLineHeight + yEntrySpace; + } else { + wasStacked = true; + width += formSize; + if (i < entryCount - 1) + width += stackSpace; + } + maxWidth = Math.max(maxWidth, width); + } + this.mNeededWidth = maxWidth; + this.mNeededHeight = maxHeight; + break; + } + case LegendOrientation.HORIZONTAL: { + var labelLineHeight : number = this.getTextSize(); + var defaultLineSpace : number = 1.2; + var labelLineSpacing : number = defaultLineSpace + yEntrySpace; + var contentWidth : number = viewPortHandler.contentWidth() * this.mMaxSizePercent; + // Start calculating layout + var maxLineWidth : number = 0.0; + var currentLineWidth : number = 0.0; + var requiredWidth : number = 0.0; + var stackedStartIndex : number = -1; + this.mCalculatedLabelBreakPoints.clear(); + this.mCalculatedLabelSizes.clear(); + this.mCalculatedLineSizes.clear(); + for (var i : number = 0; i < entryCount; i++) { + var e : LegendEntry = entries[i]; + var drawingForm : boolean = e.form != LegendForm.NONE; + var formSize : number = Number.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + var label : string = e.label; + this.mCalculatedLabelBreakPoints.add(false); + if (stackedStartIndex == -1) { + // we are not stacking, so required width is for this label + // only + requiredWidth = 0.0; + } else { + // add the spacing appropriate for stacked labels/forms + requiredWidth += stackSpace; + } + console.log("1.0 i("+i+")requiredWidth("+requiredWidth+") ?= stackSpace("+stackSpace+")"); + // grouped forms have null labels + if (label != null) { + this.mCalculatedLabelSizes.add(new FSize(this.getTextSize()*label.length, this.getTextSize())); // ��ǩ��length + console.log("1.1 i("+i+")FSize(this.getTextSize()*label.length("+this.getTextSize()*label.length+"), getTextSize("+this.getTextSize()+")"); + requiredWidth += drawingForm ? formToTextSpace + formSize : 0.0; + console.log("1.2 i("+i+")requiredWidth("+requiredWidth+") += formToTextSpace("+formToTextSpace+") + formSize("+formSize+")"); + requiredWidth += this.mCalculatedLabelSizes.get(i).width; + console.log("1.3 i("+i+")requiredWidth("+requiredWidth+") += this.mCalculatedLabelSizes.get(i).width("+this.mCalculatedLabelSizes.get(i).width+")"); + } else { + this.mCalculatedLabelSizes.add(FSize.getInstance(0.0, 0.0)); + requiredWidth += drawingForm ? formSize : 0.0; + if (stackedStartIndex == -1) { + // mark this index as we might want to break here later + stackedStartIndex = i; + } + } + if (label != null || i == entryCount - 1) { + var requiredSpacing : number = currentLineWidth == 0.0 ? 0.0 : xEntrySpace; + console.log("2.0 i("+i+")requiredSpacing("+requiredSpacing+")= xEntrySpace("+xEntrySpace+") ? currentLineWidth("+currentLineWidth+")"); + console.log("3.0 i("+i+")contentWidth("+contentWidth+"); currentLineWidth("+currentLineWidth+")"); + if (!wordWrapEnabled // No word wrapping, it must fit. + // The line is empty, it must fit + || currentLineWidth == 0.0 + // It simply fits + || (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) { + // Expand current line + currentLineWidth += requiredSpacing + requiredWidth; + console.log("4.0 i("+i+")line <==> currentLineWidth("+currentLineWidth+")= requiredSpacing("+requiredSpacing+") + requiredWidth("+requiredWidth+")"); + } else { // It doesn't fit, we need to wrap a line + // Add current line size to array + this.mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + console.log("5.0 i("+i+")new line <==> FSize(currentLineWidth("+currentLineWidth+"), labelLineHeight("+labelLineHeight+")"); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + console.log("5.1 i("+i+")new line <==> maxLineWidth("+maxLineWidth+")"); + // Start a new line + if (stackedStartIndex > -1) { + this.mCalculatedLabelBreakPoints.append(true); + } + // Start a new line + this.mCalculatedLabelBreakPoints.append(true); + currentLineWidth = requiredWidth; + console.log("5.2 i("+i+")new line <==> currentLineWidth("+currentLineWidth+")= requiredWidth("+requiredWidth+")"); + } + if (i == entryCount - 1) { //���һ��legend + // Add last line size to array + this.mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + console.log("6.0 i("+i+")FSize(currentLineWidth("+currentLineWidth+"), labelLineHeight("+labelLineHeight+")"); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + console.log("6.1 i("+i+")maxLineWidth=Max(maxLineWidth"+maxLineWidth+"), currentLineWidth("+currentLineWidth+"))"); + } + } + stackedStartIndex = label != null ? -1 : stackedStartIndex; + } + this.mNeededWidth = maxLineWidth; + this.mNeededHeight = labelLineHeight * this.mCalculatedLineSizes.size() + + labelLineSpacing * + (this.mCalculatedLineSizes.size() == 0 + ? 0 + : (this.mCalculatedLineSizes.size() - 1)); + console.log("7.0 mNeededHeight("+this.mNeededHeight+")= labelLineHeight("+labelLineHeight+")*mCalculatedLineSizes.size("+this.mCalculatedLineSizes.size()+")+labelLineSpacing("+labelLineSpacing+")"); + break; + } + } + this.mNeededHeight += this.mYOffset; + this.mNeededWidth += this.mXOffset; + console.log("0.0 this.mNeededHeight="+JSON.stringify(this.mNeededHeight)); + console.log("0.1 this.mNeededWidth="+JSON.stringify(this.mNeededWidth)); + } +} + +export enum LegendForm { + /** + * Avoid drawing a form + */ + NONE, + + /** + * Do not draw the a form, but leave space for it + */ + EMPTY, + + /** + * Use default (default dataset's form to the legend's form) + */ + DEFAULT, + + /** + * Draw a square + */ + SQUARE, + + /** + * Draw a circle + */ + CIRCLE, + + /** + * Draw a horizontal line + */ + LINE +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendEntry.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendEntry.ets new file mode 100644 index 0000000..daea98d --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendEntry.ets @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ColorTemplate from '../utils/ColorTemplate'; +import {LegendForm} from './Legend'; + +export default class LegendEntry { + /** + * + * @param label The legend entry text. A `null` label will start a group. + * @param form The form to draw for this entry. + * @param formSize Set to NaN to use the legend's default. + * @param formLineWidth Set to NaN to use the legend's default. + * @param formLineDashEffect Set to nil to use the legend's default. + * @param formColor The color for drawing the form. + */ + constructor(label ?: string, form ?: LegendForm, formSize ?: number, formLineWidth ?: number, + formLineDashEffect ?: Object/*DashPathEffect*/, formColor ?: number|Color) { + // if (label && form && formSize && formLineWidth && formLineDashEffect && formColor) { + this.label = label; + this.form = form; + this.formSize = formSize; + this.formLineWidth = formLineWidth; + this.formLineDashEffect = formLineDashEffect; + this.formColor = formColor; + // } + } + + /** + * The legend entry text. + * A `null` label will start a group. + */ + public label:string; + + /** + * The form to draw for this entry. + * + * `NONE` will avoid drawing a form, and any related space. + * `EMPTY` will avoid drawing a form, but keep its space. + * `DEFAULT` will use the Legend's default. + */ + public form:LegendForm = LegendForm.DEFAULT; + + /** + * Form size will be considered except for when .None is used + * + * Set as NaN to use the legend's default + */ + public formSize:number = Number.NaN; + + /** + * Line width used for shapes that consist of lines. + * + * Set as NaN to use the legend's default + */ + public formLineWidth:number = Number.NaN; + + /** + * Line dash path effect used for shapes that consist of lines. + * + * Set to null to use the legend's default + */ + public formLineDashEffect:Object/*DashPathEffect*/ = null; + + /** + * The color for drawing the form + */ + public formColor:number|Color// = ColorTemplate.COLOR_NONE; + + public colorArr:Number[]=[] + public colorItemSpace:number=3 + public colorLabelSpace:number=4 + public colorWidth:number=10 + public colorHeight:number=10 + public labelColor:number=Color.Black + public labelTextSize:number=10 + public islableNeedNewLine:boolean=false +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendView.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendView.ets new file mode 100644 index 0000000..94385f7 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LegendView.ets @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Legend, {LegendForm} from './Legend' +import LegendRenderer from '../renderer/LegendRenderer' +import ViewPortHandler from '../utils/ViewPortHandler' +import Paint,{LinePaint,CirclePaint,RectPaint,TextPaint} from '../data/Paint' + +@Component +@Preview +@Entry +struct LegendView { + @State model: LegendView.Model = new LegendView.Model() + + aboutToAppear() { + this.model.mLegend.calculateNeededSizes(this.model.mHandler); + this.model.mHandler.setChartDimens(this.model.mWidth, (this.model.mLegend.mNeededHeight > this.model.mHeight) ? (this.model.mLegend.mNeededHeight + 5) : this.model.mHeight); + + var mLegendRenderer : LegendRenderer = new LegendRenderer(this.model.mHandler, this.model.mLegend); + this.model.paints = this.model.paints.concat(mLegendRenderer.renderLegend()) + console.log("mLegendRenderer.computeLegend"+JSON.stringify(this.model.paints)); + } + + build() { + Stack() { + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + ForEach(this.model.paints, (item:Paint) => { + if(item instanceof CirclePaint){ + Circle() + .width(item.width) + .height(item.height) + .fill(item.stroke) + .position({ x: item.x, y: item.y }) + }else if(item.constructor.name == "RectPaint"){ + Rect({width: item.width, height: item.height}) + .fill(item.stroke) + .position({ x: item.x, y: item.y }) + }else if(item.constructor.name == "TextPaint"){ + Text((item).text) + .fontWeight(item.typeface) + .fontSize(item.textSize) + .fontColor(Color.Black) + .textAlign(item.textAlign) + .position({ x: item.x, y: item.y }) + } + },(item: Paint) => (item.alpha + "").toString()) + } + .size({ width: '90%', height: '90%' }) + } + .size({ width: this.model.mWidth, height: this.model.mHeight }) + } +} + +namespace LegendView { + export class Model { + paints:Paint[] = [] + + mHandler:ViewPortHandler = new ViewPortHandler(); + YEntrySpace : number = 5 + FormToTextSpace : number = 2 + mLegend: Legend = new Legend(); + mWidth: number = 300; + mHeight: number = 50; + + setLegend(mLegend:Legend):Model{ + this.mLegend = mLegend + return this + } + + setWidth(mWidth:number):Model{ + this.mWidth = mWidth + return this + } + + setHeight(mHeight:number):Model{ + this.mHeight = mHeight + return this + } + + public refresh(){ + this.paints = [] + this.mLegend.calculateNeededSizes(this.mHandler); + this.mHandler.setChartDimens(this.mWidth, (this.mLegend.mNeededHeight > this.mHeight) ? (this.mLegend.mNeededHeight + 5) : this.mHeight); + + var mLegendRenderer : LegendRenderer = new LegendRenderer(this.mHandler, this.mLegend); + this.paints = this.paints.concat(mLegendRenderer.renderLegend()) + } + } +} +export default LegendView diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LimitLine.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LimitLine.ets new file mode 100644 index 0000000..4aaa895 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/LimitLine.ets @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint, {Style, DashPathEffect} from '../data/Paint' +import Utils from '../utils/Utils' +import ComponentBase from '../components/ComponentBase' + +/** + * The limit line is an additional feature for all Line-, Bar- and + * ScatterCharts. It allows the displaying of an additional line in the chart + * that marks a certain maximum / limit on the specified axis (x- or y-axis). + * + */ +export default class LimitLine extends ComponentBase { + + /** limit / maximum (the y-value or xIndex) */ + private mLimit: number = 0; + + /** the width of the limit line */ + private mLineWidth: number = 2; + + /** the color of the limit line */ + private mLineColor: number | string = "#ED5B5B"; + + /** the style of the label text */ + private mTextStyle: Style = Style.FILL_AND_STROKE; + + /** label string that is drawn next to the limit line */ + private mLabel: string = ""; + + /** the path effect of this LimitLine that makes dashed lines possible */ + private mDashPathEffect: DashPathEffect = null; + + /** indicates the position of the LimitLine label */ + private mLabelPosition: LimitLabelPosition = LimitLabelPosition.RIGHT_TOP; + + constructor(limit: number, label?: string) { + super() + this.mLimit = limit; + this.mLabel = label; + } + + /** + * Returns the limit that is set for this line. + * + * @return + */ + public getLimit(): number { + return this.mLimit; + } + + /** + * set the line width of the chart (min = 0.2f, max = 12f); default 2f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public setLineWidth(width: number) { + if (width < 0.2) + width = 0.2; + if (width > 12.0) + width = 12.0; + this.mLineWidth = width; + } + + /** + * returns the width of limit line + * + * @return + */ + public getLineWidth(): number { + return this.mLineWidth; + } + + /** + * Sets the linecolor for this LimitLine. Make sure to use + * getResources().getColor(...) + * + * @param color + */ + public setLineColor(color: number | string) { + this.mLineColor = color; + } + + /** + * Returns the color that is used for this LimitLine + * + * @return + */ + public getLineColor(): number | string { + return this.mLineColor; + } + + /** + * Enables the line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public enableDashedLine(lineLength: number, spaceLength: number, phase: number) { + this.mDashPathEffect = new DashPathEffect([lineLength, spaceLength],phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public disableDashedLine() { + this.mDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled, false if not. Default: + * disabled + * + * @return + */ + public isDashedLineEnabled(): boolean { + return this.mDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for this LimitLine + * + * @return + */ + public getDashPathEffect(): DashPathEffect { + return this.mDashPathEffect; + } + + /** + * Sets the color of the value-text that is drawn next to the LimitLine. + * Default: Paint.Style.FILL_AND_STROKE + * + * @param style + */ + public setTextStyle(style: Style) { + this.mTextStyle = style; + } + + /** + * Returns the color of the value-text that is drawn next to the LimitLine. + * + * @return + */ + public getTextStyle(): Style { + return this.mTextStyle; + } + + /** + * Sets the position of the LimitLine value label (either on the right or on + * the left edge of the chart). Not supported for RadarChart. + * + * @param pos + */ + public setLabelPosition(pos: LimitLabelPosition) { + this.mLabelPosition = pos; + } + + /** + * Returns the position of the LimitLine label (value). + * + * @return + */ + public getLabelPosition(): LimitLabelPosition { + return this.mLabelPosition; + } + + /** + * Sets the label that is drawn next to the limit line. Provide "" if no + * label is required. + * + * @param label + */ + public setLabel(label: string) { + this.mLabel = label; + } + + /** + * Returns the label that is drawn next to the limit line. + * + * @return + */ + public getLabel(): string { + return this.mLabel; + } +} + +/** enum that indicates the position of the LimitLine label */ +export enum LimitLabelPosition { + LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/PathView.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/PathView.ets new file mode 100644 index 0000000..e060cf3 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/PathView.ets @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LineChartModel } from '../charts/LineChart'; +import ScaleMode from '../data/ScaleMode'; +import MyRect from '../data/Rect'; +import Paint from '../data/Paint'; +import { XAxis } from './XAxis'; +import YAxis from './YAxis'; +import {PathPaint} from '../data/Paint'; +import { CirclePaint , PathFillPaint,TextPaint, LinePaint, ImagePaint} from '../data/Paint' +import LineChartRenderer from '../renderer/LineChartRenderer'; +import LineData from '../data/LineData'; +import Utils from '../utils/Utils' + +@Entry +@Component +@Preview +export default struct PathView { + @State + model:PathViewModel = new PathViewModel(); + + public aboutToAppear() { + this.model.calcClipPath(); + } + public test():boolean{ + console.log("pathview:"+this.model.width) + return true + } + + build() { + Stack({ alignContent: Alignment.TopStart }) { + if(this.test()){} + if (this.model.paintArry.length > 0) { + ForEach(this.model.paintArry, (item: Paint) => { + + if (item instanceof PathPaint) { + Shape() { + Path() + .commands(item.commands) + .strokeWidth(item.strokeWidth) + .stroke(item.stroke) + .strokeDashArray(item.strokeDashArray) + .strokeDashOffset(item.strokeDashOffset) + } + .fill('none') + } else if (item instanceof PathFillPaint) { + Shape() { + Path() + .commands(item.commandsFill) + } + .fill('none') + .linearGradient({ colors: item.linearGradientColors }) + .clip(new Path().commands(item.commandsFill)) + } else if (item instanceof TextPaint) { + + Text(item.text) + .fontSize(8) + .position({ x: item.getX(), y: item.getY() }) + } else if (item instanceof CirclePaint) { + if (item.isDrawCirclesEnabled()) { + Circle({ width: item.radius * 2, height: item.radius * 2 }) + .position({ x: item.getX() - item.radius, y: item.getY() - item.radius }) + .fill(item.colors != null ? item.colors.at(0) : item.circleColor) + if (item.circleHoleEnabled) { + Circle({ width: item.circleHoleRadius * 2, height: item.circleHoleRadius * 2 }) + .position({ x: item.getX() - item.circleHoleRadius, y: item.getY() - item.circleHoleRadius }) + .fill(item.circleHoleColor) + } + } + } + }, (item: Paint) => "") + if(this.model.heightlightPaint.length > 0){ + ForEach(this.model.heightlightPaint, (item: Paint) => { + if(item instanceof LinePaint){ + Line() + .startPoint(item.startPoint) + .endPoint(item.endPoint) + .fill(item.color) + .stroke(item.color) + .strokeWidth(item.strokeWidth) + .position({ x: 0, y: 0 }) + .visibility(this.model.isShowHeightlight ? Visibility.Visible : Visibility.None) + } else if (item instanceof ImagePaint){ +// Image($r('app.media.marker2')) +// .width(item.width) +// .height(item.height) +// .objectFit(ImageFit.Fill) +// .position({ x:item.x,y:item.y }) +// .visibility(this.model.isShowClickValue ? Visibility.Visible : Visibility.None) + } else if (item instanceof TextPaint){ + Text(item.text) + .position({ x: item.x, y: item.y }) + .fontSize(item.textSize) + .textAlign(item.textAlign) + .fontColor(item.color) + .visibility(this.model.isShowClickValue ? Visibility.Visible : Visibility.None) + } + }, (item: Paint) => "") + } + } + } + .width(this.model.width) + .height(this.model.height) + .backgroundColor(this.model.backgroundColor) + .clip(new Path().commands(this.model.clipPath)) + .position({ x: this.model.positionX, y: this.model.minOffset }) + .onClick((event: ClickEvent)=>{ + this.model.onClick(event); + }) + .onTouch((event: TouchEvent) => { + this.model.onTouch(event) + }) + } + +} + +export class PathViewModel extends ScaleMode{ + yleftAxis: YAxis; + yRightAxis: YAxis; + xAxis: XAxis; + width: number = 300; + height: number = 300; + isInverted: boolean = false; + isShowHeightlight: boolean = true; + isShowClickValue: boolean = true; + minOffset: number = 15; //X轴线偏移量 + positionX: number= 0; + lineData: LineData = new LineData() + xScale: number = 1; + yLeftScale: number = 1; + yRightScale: number = 1; + backgroundColor: Color | string | number = "#00FFFFFF"; // chart区域背景色 默认透明色 + rect: MyRect; + paintArry: Paint[] = []; + valuePaint: Paint[] = []; + heightlightPaint: Paint[] = []; + mRenderer: LineChartRenderer; + eventX: number = 0; + eventY: number = 0; + clickTransX: number = 0; + clickTransY: number = 0; + clipPath: string = ""; + lineChartModel: LineChartModel=null; + + public setYLeftAxis(yleftAxis: YAxis): void{ + this.yleftAxis = yleftAxis; + } + + public setYRightAxis(yRightAxis: YAxis): void{ + this.yRightAxis = yRightAxis; + } + + public setXAxis(xAxis: XAxis): void{ + this.xAxis = xAxis; + } + + public setIsInverted(isInverted: boolean): void{ + this.isInverted = isInverted; + } + + public setIsShowHeightlight(isShowHeightlight: boolean): void{ + this.isShowHeightlight = isShowHeightlight; + } + + public setIsShowClickValue(isShowClickValue: boolean): void{ + this.isShowClickValue = isShowClickValue; + } + + public getLineData(): LineData{ + return this.lineData; + } + + public setPathViewData(lineData: LineData): void{ + this.lineData = lineData; + + this.computerScale(); + + this.mRenderer = new LineChartRenderer(this, this.yleftAxis, this.yRightAxis, this.isInverted); + + this.ondraw(); + } + + private computerScale(): void{ + this.rect = this.lineData.mDisplayRect; + + this.positionX = this.rect.left; + + this.xScale = (this.rect.right - this.rect.left)/(this.xAxis.getAxisMaximum()-this.xAxis.getAxisMinimum()); + this.yLeftScale = (this.rect.bottom - this.rect.top)/(this.yleftAxis.getAxisMaximum()-this.yleftAxis.getAxisMinimum()); + + this.yRightScale = (this.rect.bottom - this.rect.top)/(this.yRightAxis.getAxisMaximum()-this.yRightAxis.getAxisMinimum()); + + } + + private ondraw(){ + let pathPaint: Paint[] = this.mRenderer.drawData(); + let circlePaint: Paint[] = this.mRenderer.drawCircle(); + this.valuePaint = this.mRenderer.drawValues(); + this.heightlightPaint = this.mRenderer.drawHighlighted(); + + this.paintArry = []; + this.paintArry = this.paintArry.concat(pathPaint); + this.paintArry = this.paintArry.concat(circlePaint); + this.paintArry = this.paintArry.concat(this.valuePaint); + } + public onSingleClick(event: ClickEvent){ + this.eventX = event.x; + this.eventY = event.y; +// console.log("jk: event.x = "+event.x+", event.screenX = "+event.screenX) +// let screenCenterX: number = (this.rect.right - this.rect.left) / 2; +// let screenCenterY: number = (this.rect.bottom - this.rect.top) / 2; +// console.log("jk: this.eventY = "+this.eventY+", screenCenterY = "+screenCenterY); + +// if(this.scaleX > 1){ +// this.clickTransX = screenCenterX - this.eventX; +// this.clickTransY = screenCenterY - this.eventY; +// this.ondraw(); +// } + this.heightlightPaint = []; + this.heightlightPaint = this.mRenderer.drawHighlighted(); + } + + transX: number = 0; + transMaxY: number = 0; + transMinY: number = 0; + public onDoubleClick(event: ClickEvent){ + this.currentXSpace = this.centerX * this.scaleX - this.centerX; + this.currentYSpace = this.centerY * this.scaleY - this.centerY; + + + + let maxXBefore: number = this.paintArry[this.paintArry.length - 1].x; + let maxYBefore: number = Number.MIN_VALUE; + let minYBefore: number = Number.MAX_VALUE; + + for(let i = 0; i < this.valuePaint.length; i++){ + if(maxYBefore < this.valuePaint[i].y){ + maxYBefore = this.valuePaint[i].y; + } + if(minYBefore > this.valuePaint[i].y){ + minYBefore = this.valuePaint[i].y; + } + } + + this.ondraw(); + + let maxXAfter: number = this.paintArry[this.paintArry.length - 1].x; + let maxYAfter: number = Number.MIN_VALUE; + let minYAfter: number = Number.MAX_VALUE; + + for(let i = 0; i < this.valuePaint.length; i++){ + if(maxYAfter < this.valuePaint[i].y){ + maxYAfter = this.valuePaint[i].y; + } + if(minYAfter > this.valuePaint[i].y){ + minYAfter = this.valuePaint[i].y; + } + } + + this.transX += maxXAfter - maxXBefore; + this.transMaxY += maxYAfter - maxYBefore; + this.transMinY += minYAfter - minYBefore; + if (this.lineChartModel != null) { + this.lineChartModel.test = "" + this.currentYSpace; + } + this.setXPosition(this.moveX-this.currentXSpace) + let moveYSource=this.leftAxisModel.lastHeight*this.scaleY - this.leftAxisModel.lastHeight + this.leftAxisModel.translate(moveYSource+this.moveY-this.currentYSpace); + + } + + public onMove(event: TouchEvent) { + let finalMoveX = this.currentXSpace - this.moveX + let finalMoveY = this.currentYSpace - this.moveY + if (this.moveX > 0 && finalMoveX <= 0) { + this.moveX = this.currentXSpace + } + if (this.moveY > 0 && finalMoveY <= 0) { + this.moveY = this.currentYSpace + } + + if (this.moveX - this.currentXSpace <= this.width - this.xAixsMode.width) { + this.moveX = this.width - this.xAixsMode.width + this.currentXSpace + } + let scaleYHeight = this.height * this.scaleY + if (this.moveY - this.currentYSpace <= this.height - scaleYHeight) { + this.moveY = this.height - scaleYHeight + this.currentYSpace + } + + + let moveYSource = this.leftAxisModel.height - this.leftAxisModel.lastHeight + this.leftAxisModel.translate(moveYSource + this.moveY - this.currentYSpace); + this.rightAxisModel.translate(moveYSource + this.moveY - this.currentYSpace); + if (this.lineChartModel != null) { + this.lineChartModel.test = "" + this.moveY; + } + this.setXPosition(this.moveX - this.currentXSpace) + this.ondraw(); + } + + public onScale(event: TouchEvent){ + this.currentXSpace = this.centerX * this.scaleX - this.centerX; + this.currentYSpace = this.centerY * this.scaleY - this.centerY; + + this.ondraw(); + + if (this.lineChartModel != null) { + this.lineChartModel.test = "" + this.currentYSpace; + } + this.setXPosition(this.moveX-this.currentXSpace) + let moveYSource=this.leftAxisModel.lastHeight*this.scaleY - this.leftAxisModel.lastHeight + this.leftAxisModel.translate(moveYSource+this.moveY-this.currentYSpace); + } + + public calcClipPath(){ + this.clipPath='M'+Utils.convertDpToPixel(this.rect.left - this.positionX)+' '+Utils.convertDpToPixel(this.rect.top - this.minOffset) + +'L'+Utils.convertDpToPixel(this.rect.right - this.positionX)+' '+Utils.convertDpToPixel(this.rect.top - this.minOffset) + +'L'+Utils.convertDpToPixel(this.rect.right - this.positionX)+' '+Utils.convertDpToPixel(this.rect.bottom - this.minOffset) + +'L'+Utils.convertDpToPixel(this.rect.left - this.positionX)+' '+Utils.convertDpToPixel(this.rect.bottom - this.minOffset) + +' Z' + } + + public setBackgroundColor(color: Color | string | number): void{ + this.backgroundColor = color; + } + + public setMinOffset(minOffset: number): void{ + this.minOffset = minOffset; + } + + public getYLeftAxis(): YAxis{ + return this.yleftAxis; + } + + public getYRightAxis(): YAxis{ + return this.yRightAxis; + } + + public getXAxis(): XAxis{ + return this.xAxis; + } + + // ================================== 动画相关 ====================================== + + private timerId: number = -1; + private curX: number = 0; + private curY: number = 0; + private pathPaint: Paint[] = []; + private circlePaint: Paint[] = []; + private textPaint: Paint[] = []; + private tempArry = [] + + public animateX(durationMillis: number){ + this.clearPaint(); + + let maxCount = this.lineData.getMaxEntryCountSet().getEntryCount(); + + this.timerId = setInterval(()=>{ + this.clearPaint(); + + this.mRenderer.animateX(this.curX++); + + this.onDraw(); + + if(this.curX >= maxCount){ + this.curX = 0; + clearInterval(this.timerId) + } + },durationMillis) + } + + public animateY(durationMillis: number){ + this.clearPaint(); + + let maxCount = this.lineData.getMaxEntryCountSet().getEntryCount(); + + this.timerId = setInterval(()=>{ + this.clearPaint(); + + this.mRenderer.animateY(this.curY += 1 / maxCount); + + this.onDraw(); + + if(this.curY >= 1){ + this.curY = 0; + clearInterval(this.timerId) + } + },durationMillis) + } + + public animateXY(durationMillis: number){ + this.clearPaint(); + let maxCount = this.lineData.getMaxEntryCountSet().getEntryCount(); + + this.timerId = setInterval(()=>{ + this.clearPaint(); + + this.mRenderer.animateX(this.curX++); + this.mRenderer.animateY(this.curY += 1 / maxCount); + + this.onDraw(); + + if(this.curY >= 1){ + this.curX = 0; + this.curY = 0; + clearInterval(this.timerId) + } + },durationMillis) + } + + private clearPaint(){ + this.pathPaint = []; + this.circlePaint = []; + this.textPaint = []; + this.tempArry = [] + this.paintArry = []; + } + + private onDraw(){ + this.pathPaint = this.mRenderer.drawData(); + this.circlePaint = this.mRenderer.drawCircle(); + this.textPaint = this.mRenderer.drawValues(); + this.heightlightPaint = this.mRenderer.drawHighlighted(); + + this.tempArry = this.tempArry.concat(this.pathPaint); + this.tempArry = this.tempArry.concat(this.circlePaint); + this.tempArry = this.tempArry.concat(this.textPaint); + + this.paintArry = this.paintArry.concat(this.tempArry); + } +} + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/XAxis.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/XAxis.ets new file mode 100644 index 0000000..34398c7 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/XAxis.ets @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Utils from '../utils/Utils'; +import AxisBase from './AxisBase'; + +/** + * Class representing the x-axis labels settings. Only use the setter methods to + * modify it. Do not access public variables directly. Be aware that not all + * features the XLabels class provides are suitable for the RadarChart. + * + * @author Philipp Jahoda + */ +export class XAxis extends AxisBase { + + /** + * width of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public mLabelWidth : number = 1; + + /** + * height of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public mLabelHeight : number = 1; + + /** + * width of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public mLabelRotatedWidth : number = 1; + + /** + * height of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public mLabelRotatedHeight : number = 1; + + /** + * This is the angle for drawing the X axis labels (in degrees) + */ + protected mLabelRotationAngle : number = 0; + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart + */ + private mAvoidFirstLastClipping : boolean = false; + + /** + * the position of the x-labels relative to the chart + */ + private mPosition : XAxisPosition = XAxisPosition.TOP; + + public longest:string; + + constructor() { + super(); + this.mYOffset = 4/*Utils.convertDpToPixel(4)*/; // -3 + } + + /** + * returns the position of the x-labels + */ + public getPosition() : XAxisPosition { + return this.mPosition; + } + + /** + * sets the position of the x-labels + * + * @param pos + */ + public setPosition(pos : XAxisPosition) : void { + this.mPosition = pos; + } + + /** + * returns the angle for drawing the X axis labels (in degrees) + */ + public getLabelRotationAngle() : number{ + return this.mLabelRotationAngle; + } + + /** + * sets the angle for drawing the X axis labels (in degrees) + * + * @param angle the angle in degrees + */ + public setLabelRotationAngle(angle : XAxisPosition) : void { + this.mLabelRotationAngle = angle; + } + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart or the screen + * + * @param enabled + */ + public setAvoidFirstLastClipping(enabled : boolean) : void { + this.mAvoidFirstLastClipping = enabled; + } + + /** + * returns true if avoid-first-lastclipping is enabled, false if not + * + * @return + */ + public isAvoidFirstLastClippingEnabled() : boolean { + return this.mAvoidFirstLastClipping; + } +} + +/** + * enum for the position of the x-labels relative to the chart + */ +export enum XAxisPosition { + TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/YAxis.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/YAxis.ets new file mode 100644 index 0000000..fcb7815 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/YAxis.ets @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import router from '../utils/Utils'; +import AxisBase from './AxisBase'; +import Utils from '../utils/Utils'; +import Paint from '../data/Paint' + +/** + * Class representing the y-axis labels settings and its entries. Only use the setter methods to + * modify it. Do not + * access public variables directly. Be aware that not all features the YLabels class provides + * are suitable for the + * RadarChart. Customizations that affect the value range of the axis need to be applied before + * setting data for the + * chart. + * + * @author Philipp Jahoda + */ + +export enum AxisDependency { + LEFT, RIGHT +} + +export enum YAxisLabelPosition { + OUTSIDE_CHART, INSIDE_CHART +} + +export default class YAxis extends AxisBase { + + /** + * indicates if the bottom y-label entry is drawn or not + */ + private mDrawBottomYLabelEntry : boolean = true; + + /** + * indicates if the top y-label entry is drawn or not + */ + private mDrawTopYLabelEntry : boolean = true; + + /** + * flag that indicates if the axis is inverted or not + */ + protected mInverted : boolean = false; + + /** + * flag that indicates if the zero-line should be drawn regardless of other grid lines + */ + protected mDrawZeroLine : boolean = false; + + /** + * flag indicating that auto scale min restriction should be used + */ + private mUseAutoScaleRestrictionMin : boolean = false; + + /** + * flag indicating that auto scale max restriction should be used + */ + private mUseAutoScaleRestrictionMax : boolean = false; + + /** + * Color of the zero line + */ + protected mZeroLineColor : number = Color.Gray; + + /** + * Width of the zero line in pixels + */ + protected mZeroLineWidth : number = 1; + + /** + * axis space from the largest value to the top in percent of the total axis range + */ + protected mSpacePercentTop : number = 10; + + /** + * axis space from the smallest value to the bottom in percent of the total axis range + */ + protected mSpacePercentBottom : number = 10; + + /** + * the position of the y-labels relative to the chart + */ + private mPosition : YAxisLabelPosition = YAxisLabelPosition.OUTSIDE_CHART; + + /** + * the horizontal offset of the y-label + */ + private mXLabelOffset : number = 0.0; + + /** + * the side this axis object represents + */ + private mAxisDependency : AxisDependency; + + /** + * the minimum width that the axis should take (in dp). + *

+ * default: 0.0 + */ + protected mMinWidth : number = 0; + + /** + * the maximum width that the axis can take (in dp). + * use Inifinity for disabling the maximum + * default: Float.POSITIVE_INFINITY (no maximum specified) + */ + protected mMaxWidth : number = Number.POSITIVE_INFINITY; + + constructor(position ?: AxisDependency) { + super(); + + // default left + if (!position) { + this.mAxisDependency = AxisDependency.LEFT; + } else { + this.mAxisDependency = position; + } + this.mYOffset = 0; + } + + public getAxisDependency() : AxisDependency { + return this.mAxisDependency; + } + + /** + * @return the minimum width that the axis should take (in dp). + */ + public getMinWidth() : number { + return this.mMinWidth; + } + + /** + * Sets the minimum width that the axis should take (in dp). + * + * @param minWidth + */ + public setMinWidth(minWidth : number) : void { + this.mMinWidth = minWidth; + } + + /** + * @return the maximum width that the axis can take (in dp). + */ + public getMaxWidth() : number { + return this.mMaxWidth; + } + + /** + * Sets the maximum width that the axis can take (in dp). + * + * @param maxWidth + */ + public setMaxWidth(maxWidth : number) : void { + this.mMaxWidth = maxWidth; + } + + /** + * returns the position of the y-labels + */ + public getLabelPosition() : YAxisLabelPosition { + return this.mPosition; + } + + /** + * sets the position of the y-labels + * + * @param pos + */ + public setPosition(pos : YAxisLabelPosition) : void { + this.mPosition = pos; + } + + /** + * returns the horizontal offset of the y-label + */ + public getLabelXOffset() : number { + return this.mXLabelOffset; + } + + /** + * sets the horizontal offset of the y-label + * + * @param xOffset + */ + public setLabelXOffset(xOffset : number) : void { + this.mXLabelOffset = xOffset; + } + + /** + * returns true if drawing the top y-axis label entry is enabled + * + * @return + */ + public isDrawTopYLabelEntryEnabled() : boolean { + return this.mDrawTopYLabelEntry; + } + + /** + * returns true if drawing the bottom y-axis label entry is enabled + * + * @return + */ + public isDrawBottomYLabelEntryEnabled() : boolean { + return this.mDrawBottomYLabelEntry; + } + + /** + * set this to true to enable drawing the top y-label entry. Disabling this can be helpful + * when the top y-label and + * left x-label interfere with each other. default: true + * + * @param enabled + */ + public setDrawTopYLabelEntry(enabled : boolean) : void { + this.mDrawTopYLabelEntry = enabled; + } + + /** + * If this is set to true, the y-axis is inverted which means that low values are on top of + * the chart, high values + * on bottom. + * + * @param enabled + */ + public setInverted(enabled : boolean) : void { + this.mInverted = enabled; + } + + /** + * If this returns true, the y-axis is inverted. + * + * @return + */ + public isInverted() : boolean { + return this.mInverted; + } + + /** + * This method is deprecated. + * Use setAxisMinimum(...) / setAxisMaximum(...) instead. + * + * @param startAtZero + */ +// @Deprecated + public setStartAtZero(startAtZero : boolean) : void { + if (startAtZero) + super.setAxisMinimum(0); + else + super.resetAxisMinimum(); + } + + /** + * Sets the top axis space in percent of the full range. Default 10f + * + * @param percent + */ + public setSpaceTop(percent : number) : void { + this.mSpacePercentTop = percent; + } + + /** + * Returns the top axis space in percent of the full range. Default 10f + * + * @return + */ + public getSpaceTop() : number { + return this.mSpacePercentTop; + } + + /** + * Sets the bottom axis space in percent of the full range. Default 10f + * + * @param percent + */ + public setSpaceBottom(percent : number) : void { + this.mSpacePercentBottom = percent; + } + + /** + * Returns the bottom axis space in percent of the full range. Default 10f + * + * @return + */ + public getSpaceBottom() : number { + return this.mSpacePercentBottom; + } + + public isDrawZeroLineEnabled() : boolean { + return this.mDrawZeroLine; + } + + /** + * Set this to true to draw the zero-line regardless of weather other + * grid-lines are enabled or not. Default: false + * + * @param mDrawZeroLine + */ + public setDrawZeroLine(mDrawZeroLine : boolean) : void { + this.mDrawZeroLine = mDrawZeroLine; + } + + public getZeroLineColor() : number { + return this.mZeroLineColor; + } + + /** + * Sets the color of the zero line + * + * @param color + */ + public setZeroLineColor(color : number) : void { + this.mZeroLineColor = color; + } + + public getZeroLineWidth() : number { + return this.mZeroLineWidth; + } + + /** + * Sets the width of the zero line in dp + * + * @param width + */ + public setZeroLineWidth(width : number) : void { + this.mZeroLineWidth = Utils.convertDpToPixel(width); + } + + /** + * This is for normal (not horizontal) charts horizontal spacing. + * + * @param p + * @return + */ + public getRequiredWidthSpace(p : Paint) : number { + + p.setTextSize(this.mTextSize); + + var label : string = super.getLongestLabel(); + var width : number = Utils.calcTextWidth(p, label) + super.getXOffset() * 2; + + var minWidth : number = this.getMinWidth(); + var maxWidth : number = this.getMaxWidth(); + + if (minWidth > 0) + minWidth = Utils.convertDpToPixel(minWidth); + + if (maxWidth > 0 && maxWidth != Number.POSITIVE_INFINITY) + maxWidth = Utils.convertDpToPixel(maxWidth); + + width = Math.max(minWidth, Math.min(width, maxWidth > 0.0 ? maxWidth : width)); + + return width; + } + + /** + * This is for HorizontalBarChart vertical spacing. + * + * @param p + * @return + */ + public getRequiredHeightSpace(p : Paint) : number { + + p.setTextSize(this.mTextSize); + + var label : string = super.getLongestLabel(); + return Utils.calcTextHeight(p, label) + super.getYOffset() * 2; + } + + /** + * Returns true if this axis needs horizontal offset, false if no offset is needed. + * + * @return + */ + public needsOffset() : boolean { + if (super.isEnabled() && super.isDrawLabelsEnabled() && this.getLabelPosition() == YAxisLabelPosition + .OUTSIDE_CHART) + return true; + else + return false; + } + + /** + * Returns true if autoscale restriction for axis min value is enabled + */ +// @Deprecated + public isUseAutoScaleMinRestriction( ) : boolean { + return this.mUseAutoScaleRestrictionMin; + } + + /** + * Sets autoscale restriction for axis min value as enabled/disabled + */ +// @Deprecated + public setUseAutoScaleMinRestriction(isEnabled : boolean) : void { + this.mUseAutoScaleRestrictionMin = isEnabled; + } + + /** + * Returns true if autoscale restriction for axis max value is enabled + */ +// @Deprecated + public isUseAutoScaleMaxRestriction() : boolean { + return this.mUseAutoScaleRestrictionMax; + } + + /** + * Sets autoscale restriction for axis max value as enabled/disabled + */ +// @Deprecated + public setUseAutoScaleMaxRestriction(isEnabled : boolean) : void { + this.mUseAutoScaleRestrictionMax = isEnabled; + } + + +// @Override + public calculate(dataMin : number, dataMax : number) : void { + + var min : number = dataMin; + var max : number = dataMax; + + // Make sure max is greater than min + // Discussion: https://github.com/danielgindi/Charts/pull/3650#discussion_r221409991 + if (min > max) + { + if (this.mCustomAxisMax && this.mCustomAxisMin) + { + var t : number = min; + min = max; + max = t; + } + else if (this.mCustomAxisMax) + { + min = max < 0 ? max * 1.5 : max * 0.5; + } + else if (this.mCustomAxisMin) + { + max = min < 0 ? min * 0.5 : min * 1.5; + } + } + + var range : number = Math.abs(max - min); + + // in case all values are equal + if (range == 0) { + max = max + 1; + min = min - 1; + } + + // recalculate + range = Math.abs(max - min); + + // calc extra spacing + this.mAxisMinimum = this.mCustomAxisMin ? this.mAxisMinimum : min - (range / 100) * this.getSpaceBottom(); + this.mAxisMaximum = this.mCustomAxisMax ? this.mAxisMaximum : max + (range / 100) * this.getSpaceTop(); + + this.mAxisRange = Math.abs(this.mAxisMinimum - this.mAxisMaximum); + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/XAxisView.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/XAxisView.ets new file mode 100644 index 0000000..9a9a982 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/XAxisView.ets @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ScaleMode from '../../data/ScaleMode'; +import LimitLine,{LimitLabelPosition} from '../LimitLine'; +import Paint,{LinePaint,TextPaint,PathPaint} from '../../data/Paint' +import {XAxis,XAxisPosition} from '../XAxis' +import XAxisRenderer from '../../renderer/XAxisRenderer' +import Transformer from '../..//utils/Transformer' +import ViewPortHandler from '../../utils/ViewPortHandler' +import XAixsMode from '../../data/XAixsMode'; + +@Entry +@Component +@Preview +export default struct XAxisView { + + paints:Paint[] = [] + handler:ViewPortHandler = new ViewPortHandler(); + @State + topAxis:XAxis = new XAxis(); + @State + bottomAxis:XAxis = new XAxis(); + @State + minOffset:number = 15; + @State + yLeftLongestLabel:string="AAA" + @State + yRightLongestLabel:string="AAA" + @State + XLimtLine:LimitLine = new LimitLine(35, "Index 10"); + + @State + scaleMode:ScaleMode=new ScaleMode() + aboutToAppear(){ + this.scaleMode.xAixsMode.draw() +// let minYOffset=this.topAxis.getTextSize()+this.topAxis.getYOffset(); +// this.minOffset=this.minOffset0) { + ForEach(this.scaleMode.xAixsMode.paints, (item: Paint) => { + if (item instanceof LinePaint) { + Line() + .startPoint(item.startPoint) + .endPoint(item.endPoint) + .fill(item.fill) + .stroke(item.stroke) + .strokeWidth(item.strokeWidth) + .strokeDashArray(item.strokeDashArray) + .strokeDashOffset(item.strokeDashOffset) + .strokeOpacity(item.alpha) + .position({ x: 0, y: 0 }) + } else if (item instanceof TextPaint) { + Text(item.text) + .position({ x: item.x, y: item.y }) + .fontWeight(item.typeface) + .fontSize(item.textSize) + .textAlign(item.textAlign) + } else if (item instanceof PathPaint) { + Path() + .commands(item.commands) + .fill(item.fill) + .stroke(item.stroke) + .strokeWidth(item.strokeWidth == 0 ? 1 : item.strokeWidth) + .strokeDashArray(item.strokeDashArray) + .strokeDashOffset(item.strokeDashOffset) + .strokeOpacity(item.alpha) + .position({ x: item.x, y: item.y }) + } + }, (item: Paint) => (item.alpha + "").toString()) + } + } + .width(this.scaleMode.xAixsMode.width) + .height(this.scaleMode.xAixsMode.height) + .position({x:this.scaleMode.xAixsMode.xPosition}) + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/YAxisView.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/YAxisView.ets new file mode 100644 index 0000000..d363e20 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/components/renderer/YAxisView.ets @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint, {LinePaint, TextPaint, PathPaint} from '../../data/Paint' +import YAxis, {YAxisLabelPosition, AxisDependency} from '../../components/YAxis' +import YAxisRenderer from '../../renderer/YAxisRenderer' +import Transformer from '../../utils/Transformer' +import ViewPortHandler from '../../utils/ViewPortHandler' +import Matrix from '../../utils/Matrix' +import MyRect from '../../data/Rect' +import LimitLine, {LimitLabelPosition} from '../../components/LimitLine' +import Utils from '../../utils/Utils' + +@Component +export default struct YAxisView { + @State model: YAxisModel = new YAxisModel(); + yAxis: YAxis = null; + minOffset: number = 15; + + public aboutToAppear() { + this.model.invalidate(); + } + + build() { + Stack(){ + Stack() { + ForEach(this.model.paints, (item: Paint) => { + if (item instanceof LinePaint) { + Line() + .startPoint(item.startPoint) + .endPoint(item.endPoint) + .fill(item.fill) + .stroke(item.stroke) + .strokeWidth(item.strokeWidth) + .strokeDashArray(item.strokeDashArray) + .strokeDashOffset(item.strokeDashOffset) + .strokeOpacity(item.alpha) + .position({ x: 0, y: 0 }) + } else if (item instanceof TextPaint) { + Text(item.text) + .width(item.width) + .height(item.height) + .position({ x: item.x, y: item.y}) + .fontWeight(item.typeface) + .fontSize(item.textSize) + .textAlign(item.textAlign) + .padding({ left: 5, right: 5 }) + } else if (item instanceof PathPaint) { + Path() + .commands(item.commands) + .fill(item.fill) + .stroke(item.stroke) + .strokeWidth(item.strokeWidth == 0 ? 1 : item.strokeWidth) + .strokeDashArray(item.strokeDashArray) + .strokeDashOffset(item.strokeDashOffset) + .strokeOpacity(item.alpha) + .position({ x: item.x, y: item.y }) + } + }, (item: Paint) => (item.alpha + "").toString()) + } + .width(this.model.width) + .height(this.model.height) + .position({y: -(this.model.height - this.model.lastHeight) + this.model.translateY}) + } + .clip(new Path().commands(this.model.clipPath)) + } +} + +export class YAxisModel{ + width: number = 300; + height: number = 300; + handler: ViewPortHandler = new ViewPortHandler(); + paints: Paint[] = [] + minOffset: number = 15; + mTran: Transformer = new Transformer(this.handler); + yAxis: YAxis = null; + mAxisRenderer: YAxisRenderer; + lastHeight: number = 0; + translateY: number = 0; + clipPath: string = ""; + + public initYAxis() { + this.mTran.prepareMatrixOffset(this.yAxis.isInverted()); + this.mAxisRenderer = new YAxisRenderer(this.handler, this.yAxis, this.mTran); + this.mAxisRenderer.computeAxis(this.yAxis.mAxisMinimum, this.yAxis.mAxisMaximum, this.yAxis.isInverted()) + } + + public initViewPortHandler(){ + this.handler.restrainViewPort(this.minOffset, this.minOffset, this.minOffset, this.minOffset) + this.handler.setChartDimens(this.width, this.height); + } + + public invalidate(){ + this.initViewPortHandler(); + this.initYAxis(); + this.paints.length = 0; + this.paints = this.paints.concat(this.mAxisRenderer.renderAxisLine()); + this.paints = this.paints.concat(this.mAxisRenderer.renderAxisLabels()); + this.paints = this.paints.concat(this.mAxisRenderer.renderGridLines()); + this.paints = this.paints.concat(this.mAxisRenderer.renderLimitLines()); + this.calcXAixsModeClipPath(); + } + + public setWidth(width: number){ + this.width = width + } + + public setHeight(height: number){ + this.height = height; + this.lastHeight = height; + } + + public setMinOffset(minOffset: number){ + this.minOffset = minOffset; + } + + public setYAxis(yAxis: YAxis){ + this.yAxis = yAxis; + } + + public scale(scaleY: number){ + this.height = this.lastHeight * scaleY; + this.invalidate(); + } + + public translate(translateY: number){ + this.translateY = translateY; + this.calcXAixsModeClipPath(); + } + + public calcXAixsModeClipPath(){ + this.clipPath = 'M'+Utils.convertDpToPixel(0)+' '+Utils.convertDpToPixel(this.translateY >= (this.height - this.lastHeight)?0:this.minOffset) + +'L'+Utils.convertDpToPixel(this.width)+' '+Utils.convertDpToPixel(this.translateY <= (this.height - this.lastHeight)?0:this.minOffset) + +'L'+Utils.convertDpToPixel(this.width)+' '+Utils.convertDpToPixel(this.translateY > 0?this.lastHeight-this.minOffset:this.lastHeight) + +'L'+Utils.convertDpToPixel(0)+' '+Utils.convertDpToPixel(this.translateY > 0?this.lastHeight-this.minOffset:this.lastHeight) + +' Z' + } + +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleData.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleData.ets new file mode 100644 index 0000000..4c31dab --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleData.ets @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import IBarLineScatterCandleBubbleDataSet from '../interfaces/datasets/IBarLineScatterCandleBubbleDataSet'; +import {JArrayList} from '../utils/JArrayList'; +import ChartData from './ChartData'; +import EntryOhos from './EntryOhos'; + +/** + * Baseclass for all Line, Bar, Scatter, Candle and Bubble data. + * + */ +export default abstract class BarLineScatterCandleBubbleData> +extends ChartData { + + constructor(sets ?: JArrayList) { + super(sets); + } +} + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleDataSet.ets new file mode 100644 index 0000000..7bb7e8e --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BarLineScatterCandleBubbleDataSet.ets @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from './EntryOhos' +import {DataSet, Rounding} from './DataSet' +import IBarLineScatterCandleBubbleDataSet from '../interfaces/datasets/IBarLineScatterCandleBubbleDataSet' +import {JArrayList} from '../utils/JArrayList' + +/** + * Baseclass of all DataSets for Bar-, Line-, Scatter- and CandleStickChart. + * + */ +export default abstract class BarLineScatterCandleBubbleDataSet extends DataSet implements IBarLineScatterCandleBubbleDataSet { + + /** + * default highlight color + */ + protected mHighLightColor: number =0xffbb73; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + } + + /** + * Sets the color that is used for drawing the highlight indicators. Dont + * forget to resolve the color using getResources().getColor(...) or + * Color.rgb(...). + * + * @param color + */ + public setHighLightColor(color: number): void { + this.mHighLightColor = color; + } + + public getHighLightColor(): number { + return this.mHighLightColor; + } + + protected copyTo(barLineScatterCandleBubbleDataSet: BarLineScatterCandleBubbleDataSet):void { + super.copyTo(barLineScatterCandleBubbleDataSet); + barLineScatterCandleBubbleDataSet.mHighLightColor = this.mHighLightColor; + } + +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseDataSet.ets new file mode 100644 index 0000000..aa47028 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseDataSet.ets @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DashPathEffect } from './Paint'; +import EntryOhos from './EntryOhos' +import IDataSet from '../interfaces/datasets/IDataSet' +import {JArrayList} from '../utils/JArrayList' +import {AxisDependency} from '../components/YAxis' +import IValueFormatter from '../formatter/IValueFormatter' +import {LegendForm} from '../components/Legend' +import MPPointF from '../utils/MPPointF' +import Color from '../utils/ColorTemplate' +import ColorTemplate from '../utils/ColorTemplate' +import Utils from '../utils/Utils' +import {Rounding} from './DataSet' + +/** + * This is the base dataset of all DataSets. It's purpose is to implement critical methods + * provided by the IDataSet interface. + */ +export default abstract class BaseDataSet implements IDataSet { + + /** + * List representing all colors that are used for this DataSet + */ + protected mColors: JArrayList; + + /** + * List representing all colors that are used for drawing the actual values for this DataSet + */ + protected mValueColors: JArrayList = null; + + /** + * label that describes the DataSet or the data the DataSet represents + */ + private mLabel: string = "DataSet"; + + /** + * this specifies which axis this DataSet should be plotted against + */ + protected mAxisDependency: AxisDependency = AxisDependency.LEFT; + + /** + * if true, value highlightning is enabled + */ + protected mHighlightEnabled: boolean = true; + + /** + * custom formatter that is used instead of the auto-formatter if set + */ + protected mValueFormatter: IValueFormatter; + + /** + * the typeface used for the value text + */ + protected mValueTypeface: FontWeight /*Typeface*/ + private mForm: LegendForm = LegendForm.DEFAULT; + private mFormSize: number = Number.NaN; + private mFormLineWidth: number = Number.NaN; + private mFormLineDashEffect: DashPathEffect = null; + + /** + * if true, y-values are drawn on the chart + */ + protected mDrawValues: boolean = true; + + /** + * if true, y-icons are drawn on the chart + */ + protected mDrawIcons: boolean = true; + + /** + * the offset for drawing icons (in dp) + */ + protected mIconsOffset: MPPointF = new MPPointF(); + + /** + * the size of the value-text labels + */ + protected mValueTextSize: number = 17; + + /** + * flag that indicates if the DataSet is visible or not + */ + protected mVisible: boolean = true; + + /** + * Default constructor. + */ + constructor(label?: string) { + this.mColors = new JArrayList(); + this.mValueColors = new JArrayList(); + // default color + this.mColors.add(0x8ceaff); + this.mValueColors.add(0x000000); + this.mLabel = label; + } + + /** + * Use this method to tell the data set that the underlying data has changed. + */ + public notifyDataSetChanged(): void { + this.calcMinMax(); + } + + + /** + * ###### ###### COLOR GETTING RELATED METHODS ##### ###### + */ + public getColors(): JArrayList { + return this.mColors; + } + + public getValueColors(): JArrayList { + return this.mValueColors; + } + + public getColor(index?: number): number { + if(!index||index==null||index==undefined){ + index=0; + } + return this.mColors.get(index % this.mColors.size()).valueOf(); + } + + + /** + * ###### ###### COLOR SETTING RELATED METHODS ##### ###### + */ + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public setColorsByList(colors: JArrayList): void { + this.mColors = colors; + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public setColorsByVariable(colors: number[]): void { + this.mColors = ColorTemplate.createColors(colors); + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. You can use + * "new int[] { R.color.red, R.color.green, ... }" to provide colors for + * this method. Internally, the colors are resolved using + * getResources().getColor(...) + * + * @param colors + */ + public setColorsByArr(colors: number[]): void { + + if (this.mColors == null) { + this.mColors = new JArrayList (); + } + this.mColors.clear(); + for (let color of colors) { + this.mColors.add(color); + } + } + + /** + * Adds a new color to the colors array of the DataSet. + * + * @param color + */ + public addColor(color: number): void { + if (this.mColors == null) { + this.mColors = new JArrayList(); + } + this.mColors.add(color); + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public setColorByColor(color: Number): void { + this.resetColors(); + this.mColors.add(color); + } + + /** + * Sets a color with a specific alpha value. + * + * @param color + * @param alpha from 0-255 + */ + public setColorByAlpha(color: number, alpha: number): void { + var mColor:number = ColorTemplate.argb(alpha, ColorTemplate.red(color), ColorTemplate.green(color), ColorTemplate.blue(color)); + this.setColorsByVariable([mColor]) + } + + /** + * Sets colors with a specific alpha value. + * + * @param colors + * @param alpha + */ + public setColorsByArrAndAlpha(colors: number[], alpha: number): void { + this.resetColors(); + for (let color of colors) { + this.addColor(ColorTemplate.argb(alpha, ColorTemplate.red(color), ColorTemplate.green(color), ColorTemplate.blue(color))); + } + } + + /** + * Resets all colors of this DataSet and recreates the colors array. + */ + public resetColors(): void { + if (this.mColors == null) { + this.mColors = new JArrayList(); + } + this.mColors.clear(); + } + + /** + * ###### ###### OTHER STYLING RELATED METHODS ##### ###### + */ + public setLabel(label: string): void { + this.mLabel = label; + } + + public getLabel(): string { + return this.mLabel; + } + + public setHighlightEnabled(enabled: boolean): void { + this.mHighlightEnabled = enabled; + } + + public isHighlightEnabled(): boolean { + return this.mHighlightEnabled; + } + + public setValueFormatter(f: IValueFormatter): void { + + if (f == null) + return; + else + this.mValueFormatter = f; + } + + public getValueFormatter(): IValueFormatter { + if (this.needsFormatter()) + return Utils.getDefaultValueFormatter(); + return this.mValueFormatter; + } + + public needsFormatter(): boolean { + return this.mValueFormatter == null; + } + + public setValueTextColor(color: number): void { + this.mValueColors.clear(); + this.mValueColors.add(color); + } + + public setValueTextColors(colors: JArrayList): void { + this.mValueColors = colors; + } + + public setValueTypeface(tf: FontWeight /*Typeface*/ + ): void { + this.mValueTypeface = tf; + } + + public setValueTextSize(size: number): void { + this.mValueTextSize = size; + } + + public getValueTextColor(index?: number): number { + if (!index) { + index = 0 + } + return this.mValueColors.get(index % this.mValueColors.size()).valueOf(); + } + + public getValueTypeface(): FontWeight /*Typeface*/ + { + return this.mValueTypeface; + } + + public getValueTextSize(): number { + return this.mValueTextSize; + } + + public setForm(form: LegendForm): void { + this.mForm = form; + } + + public getForm(): LegendForm { + return this.mForm; + } + + public setFormSize(formSize: number): void { + this.mFormSize = formSize; + } + + public getFormSize(): number { + return this.mFormSize; + } + + public setFormLineWidth(formLineWidth: number): void { + this.mFormLineWidth = formLineWidth; + } + + public getFormLineWidth(): number { + return this.mFormLineWidth; + } + + public setFormLineDashEffect(dashPathEffect: DashPathEffect + ): void { + this.mFormLineDashEffect = dashPathEffect; + } + + public getFormLineDashEffect(): DashPathEffect + { + return this.mFormLineDashEffect; + } + + public setDrawValues(enabled: boolean): void { + this.mDrawValues = enabled; + } + + public isDrawValuesEnabled(): boolean { + return this.mDrawValues; + } + + public setDrawIcons(enabled: boolean): void { + this.mDrawIcons = enabled; + } + + public isDrawIconsEnabled(): boolean { + return this.mDrawIcons; + } + + public setIconsOffset(offsetDp: MPPointF): void { + + this.mIconsOffset.x = offsetDp.x; + this.mIconsOffset.y = offsetDp.y; + } + + public getIconsOffset(): MPPointF { + return this.mIconsOffset; + } + + public setVisible(visible: boolean): void { + this.mVisible = visible; + } + + public isVisible(): boolean { + return this.mVisible; + } + + public getAxisDependency(): AxisDependency{ + return this.mAxisDependency; + } + + public setAxisDependency(dependency: AxisDependency): void { + this.mAxisDependency = dependency; + } + + + /** + * ###### ###### DATA RELATED METHODS ###### ###### + */ + public getIndexInEntries(xIndex: number): number { + for (let i = 0;i < this.getEntryCount(); i++) { + if (xIndex == this.getEntryForIndex(i).getX()) { + return i; + } + } + return -1; + } + + public removeFirst(): boolean { + if (this.getEntryCount() > 0) { + var entry: T = this.getEntryForIndex(0); + return this.removeEntry(entry); + } else { + return false; + } + } + + public removeLast(): boolean { + if (this.getEntryCount() > 0) { + var e: T = this.getEntryForIndex(this.getEntryCount() - 1); + return this.removeEntry(e); + } else + return false; + } + + public removeEntryByXValue(xValue: number): boolean { + var e: T = this.getEntryForXValue(xValue, Number.NaN); + return this.removeEntry(e); + } + + public removeEntryByIndex(index: number): boolean { + var e: T = this.getEntryForIndex(index); + return this.removeEntry(e); + } + + public contains(e: T): boolean { + for (let i = 0; i < this.getEntryCount(); i++) { + if (this.getEntryForIndex(i) == e) + return true; + } + return false; + } + protected copyTo(baseDataSet: BaseDataSet): void { + baseDataSet.mAxisDependency = this.mAxisDependency; + baseDataSet.mColors = this.mColors; + baseDataSet.mDrawIcons = this.mDrawIcons; + baseDataSet.mDrawValues = this.mDrawValues; + baseDataSet.mForm = this.mForm; + baseDataSet.mFormLineDashEffect = this.mFormLineDashEffect; + baseDataSet.mFormLineWidth = this.mFormLineWidth; + baseDataSet.mFormSize = this.mFormSize; + baseDataSet.mHighlightEnabled = this.mHighlightEnabled; + baseDataSet.mIconsOffset = this.mIconsOffset; + baseDataSet.mValueColors = this.mValueColors; + baseDataSet.mValueFormatter = this.mValueFormatter; + baseDataSet.mValueColors = this.mValueColors; + baseDataSet.mValueTextSize = this.mValueTextSize; + baseDataSet.mVisible = this.mVisible; + } + + getYMin(): number{ + return 0 + } + + /** + * returns the maximum y-value this DataSet holds + * + * @return + */ + getYMax(): number{ + return 0 + } + + /** + * returns the minimum x-value this DataSet holds + * + * @return + */ + getXMin(): number{ + return 0 + } + + /** + * returns the maximum x-value this DataSet holds + * + * @return + */ + getXMax(): number{ + return 0 + } + + getEntryCount(): number{ + return 0; + } + + calcMinMax(e?: T): void{ + } + + calcMinMaxY(fromX: number, toX: number): void{ + } + + getEntryForXValue(xValue: number, closestToY: number, rounding?: Rounding): T{ + return null + } + + getEntriesForXValue(xValue: number): JArrayList{ + return null + } + + getEntryForIndex(index: number): T{ + return null + } + + getEntryIndex(xValue: number, closestToY: number, rounding: Rounding): number{ + return 0 + } + + getEntryIndexByEntry(e: T): number{ + return 0 + } + + addEntry(e : T) : boolean{ + return false + } + + addEntryOrdered(e : T) : void{ + } + + removeEntry(e : T) : boolean{ + return false + } + + clear() : void{ + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseEntry.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseEntry.ets new file mode 100644 index 0000000..edd06ce --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/BaseEntry.ets @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImagePaint } from './Paint'; + +export default abstract class BaseEntry{ + /** the y value */ + private y:number= 0; + /** optional spot for additional data this Entry represents */ + private mData:Object = null; + /** optional icon image */ + private mIcon:ImagePaint = null; + + constructor(y?:number,icon?:ImagePaint,data?:Object) { + if(y != undefined){ + this.y=y; + } + if(icon != undefined){ + this.mIcon = icon; + } + if(data != undefined){ + this.mData = data; + } + } + /** + * Returns the y value of this Entry. + * + * @return + */ + public getY():number { + return this.y; + } + + /** + * Sets the icon drawable + * + * @param icon + */ + public setIcon(icon:ImagePaint):void { + this.mIcon = icon; + } + + /** + * Returns the icon of this Entry. + * + * @return + */ + public getIcon():ImagePaint{ + return this.mIcon; + } + + /** + * Sets the y-value for the Entry. + * + * @param y + */ + public setY( y:number):void { + this.y = y; + } + + /** + * Returns the data, additional information that this Entry represents, or + * null, if no data has been specified. + * + * @return + */ + public getData():Object { + return this.mData; + } + + /** + * Sets additional data this Entry should represent. + * + * @param data + */ + public setData( data:Object):void { + this.mData = data; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleDataSet.ets new file mode 100644 index 0000000..f2ca56c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleDataSet.ets @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {DataSet} from './DataSet'; +import LineScatterCandleRadarDataSet from './LineScatterCandleRadarDataSet'; +import ColorTemplate from '../utils/ColorTemplate'; +import {JArrayList} from '../utils/JArrayList'; +import CandleEntry from './CandleEntry'; +import Utils from '../utils/Utils'; +import ICandleDataSet from '../interfaces/datasets/ICandleDataSet'; +import Paint, {Style} from '../data/paint'; + +/** + * DataSet for the CandleStickChart. + * + */ +export default class CandleDataSet extends LineScatterCandleRadarDataSet implements ICandleDataSet { + + /** + * the width of the shadow of the candle + */ + private mShadowWidth: number = 3; + + /** + * should the candle bars show? + * when false, only "ticks" will show + *

+ * - default: true + */ + private mShowCandleBar: boolean = true; + + /** + * the space between the candle entries, default 0.1f (10%) + */ + private mBarSpace: number = 0.1; + + /** + * use candle color for the shadow + */ + private mShadowColorSameAsCandle: boolean = false; + + /** + * paint style when open < close + * increasing candlesticks are traditionally hollow + */ + protected mIncreasingPaintStyle: Style= Style.STROKE; + /** + * paint style when open > close + * descreasing candlesticks are traditionally filled + */ + protected mDecreasingPaintStyle: Style = Style.FILL; + /** + * color for open == close + */ + protected mNeutralColor: number = ColorTemplate.COLOR_SKIP; + + /** + * color for open < close + */ + protected mIncreasingColor: number = ColorTemplate.COLOR_SKIP; + + /** + * color for open > close + */ + protected mDecreasingColor: number = ColorTemplate.COLOR_SKIP; + + /** + * shadow line color, set -1 for backward compatibility and uses default + * color + */ + protected mShadowColor: number = ColorTemplate.COLOR_SKIP; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + } + + public copy(): DataSet { + let entries = new JArrayList(); + for (let i = 0; i < this.mEntries.size(); i++) { + entries.add(this.mEntries.get(i).copy()); + } + let copied = new CandleDataSet(entries, this.getLabel()); + this.copyTo(copied); + return copied; + } + + protected copyTo(candleDataSet: CandleDataSet): void { + super.copyTo(candleDataSet); + candleDataSet.mShadowWidth = this.mShadowWidth; + candleDataSet.mShowCandleBar = this.mShowCandleBar; + candleDataSet.mBarSpace = this.mBarSpace; + candleDataSet.mShadowColorSameAsCandle = this.mShadowColorSameAsCandle; + candleDataSet.mHighLightColor = this.mHighLightColor; + candleDataSet.mIncreasingPaintStyle = this.mIncreasingPaintStyle; + candleDataSet.mDecreasingPaintStyle = this.mDecreasingPaintStyle; + candleDataSet.mNeutralColor = this.mNeutralColor; + candleDataSet.mIncreasingColor = this.mIncreasingColor; + candleDataSet.mDecreasingColor = this.mDecreasingColor; + candleDataSet.mShadowColor = this.mShadowColor; + } + + public myCalcMinMax(e: CandleEntry): void { + + if (e.getLow() < this.mYMin) + this.mYMin = e.getLow(); + + if (e.getHigh() > this.mYMax) + this.mYMax = e.getHigh(); + + this.calcMinMaxX(e); + } + + protected myCalcMinMaxY(e: CandleEntry): void { + + if (e.getHigh() < this.mYMin) + this.mYMin = e.getHigh(); + + if (e.getHigh() > this.mYMax) + this.mYMax = e.getHigh(); + + if (e.getLow() < this.mYMin) + this.mYMin = e.getLow(); + + if (e.getLow() > this.mYMax) + this.mYMax = e.getLow(); + } + + /** + * Sets the space that is left out on the left and right side of each + * candle, default 0.1f (10%), max 0.45f, min 0f + * + * @param space + */ + public setBarSpace(space: number): void { + + if (space < 0) + space = 0; + if (space > 0.45) + space = 0.45; + + this.mBarSpace = space; + } + + public getBarSpace(): number { + return this.mBarSpace; + } + + /** + * Sets the width of the candle-shadow-line in pixels. Default 3f. + * + * @param width + */ + public setShadowWidth(width: number): void { + this.mShadowWidth = Utils.convertDpToPixel(width); + } + + public getShadowWidth(): number { + return this.mShadowWidth; + } + + /** + * Sets whether the candle bars should show? + * + * @param showCandleBar + */ + public setShowCandleBar(showCandleBar: boolean): void { + this.mShowCandleBar = showCandleBar; + } + + public getShowCandleBar(): boolean { + return this.mShowCandleBar; + } + + // TODO + /** + * It is necessary to implement ColorsList class that will encapsulate + * colors list functionality, because It's wrong to copy paste setColor, + * addColor, ... resetColors for each time when we want to add a coloring + * options for one of objects + * + * @author Mesrop + */ + + /** BELOW THIS COLOR HANDLING */ + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open == close. + * + * @param color + */ + public setNeutralColor(color: number): void { + this.mNeutralColor = color; + } + + public getNeutralColor(): number { + return this.mNeutralColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open <= close. + * + * @param color + */ + public setIncreasingColor(color: number): void { + this.mIncreasingColor = color; + } + + public getIncreasingColor(): number { + return this.mIncreasingColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open > close. + * + * @param color + */ + public setDecreasingColor(color: number): void { + this.mDecreasingColor = color; + } + + public getDecreasingColor(): number { + return this.mDecreasingColor; + } + + public getIncreasingPaintStyle(): Style { + return this.mIncreasingPaintStyle; + } + + /** + * Sets paint style when open < close + * + * @param paintStyle + */ + public setIncreasingPaintStyle(paintStyle: Style): void { + this.mIncreasingPaintStyle = paintStyle; + } + + public getDecreasingPaintStyle(): Style { + return this.mDecreasingPaintStyle; + } + + /** + * Sets paint style when open > close + * + * @param decreasingPaintStyle + */ + public setDecreasingPaintStyle(decreasingPaintStyle: Style): void { + this.mDecreasingPaintStyle = decreasingPaintStyle; + } + + public getShadowColor(): number { + return this.mShadowColor; + } + + /** + * Sets shadow color for all entries + * + * @param shadowColor + */ + public setShadowColor(shadowColor: number): void { + this.mShadowColor = shadowColor; + } + + public getShadowColorSameAsCandle(): boolean { + return this.mShadowColorSameAsCandle; + } + + /** + * Sets shadow color to be the same color as the candle color + * + * @param shadowColorSameAsCandle + */ + public setShadowColorSameAsCandle(shadowColorSameAsCandle: boolean): void { + this.mShadowColorSameAsCandle = shadowColorSameAsCandle; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleEntry.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleEntry.ets new file mode 100644 index 0000000..e5d11ff --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/CandleEntry.ets @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImagePaint } from './Paint'; +import EntryOhos from './EntryOhos'; + +/** + * Subclass of Entry that holds all values for one entry in a CandleStickChart. + */ +export default class CandleEntry extends EntryOhos { + + /** shadow-high value */ + private mShadowHigh:number = 0; + + /** shadow-low value */ + private mShadowLow:number= 0; + + /** close value */ + private mClose:number = 0; + + /** open value */ + private mOpen:number = 0; + constructor(x:number, shadowH?:number, shadowL?:number, open?:number, close?:number, + icon?:ImagePaint, data?:Object) { + super(x, (shadowH + shadowL) / 2, icon, data); + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + /** + * Returns the overall range (difference) between shadow-high and + * shadow-low. + * + * @return + */ + public getShadowRange():number { + return Math.abs(this.mShadowHigh - this.mShadowLow); + } + + /** + * Returns the body size (difference between open and close). + * + * @return + */ + public getBodyRange():number { + return Math.abs(this.mOpen - this.mClose); + } + + /** + * Returns the center value of the candle. (Middle value between high and + * low) + */ + public getY():number { + return super.getY(); + } + + public copy():CandleEntry{ + + var c:CandleEntry = new CandleEntry(this.getX(), this.mShadowHigh, this.mShadowLow, this.mOpen, + this.mClose,null, this.getData()); + + return c; + } + + /** + * Returns the upper shadows highest value. + * + * @return + */ + public getHigh():number { + return this.mShadowHigh; + } + + public setHigh(mShadowHigh:number):void { + this.mShadowHigh = mShadowHigh; + } + + /** + * Returns the lower shadows lowest value. + * + * @return + */ + public getLow():number { + return this.mShadowLow; + } + + public setLow(mShadowLow:number):void { + this.mShadowLow = mShadowLow; + } + + /** + * Returns the bodys close value. + * + * @return + */ + public getClose():number { + return this.mClose; + } + + public setClose(mClose:number) :void{ + this.mClose = mClose; + } + + /** + * Returns the bodys open value. + * + * @return + */ + public getOpen():number { + return this.mOpen; + } + + public setOpen( mOpen:number):void{ + this.mOpen = mOpen; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ChartData.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ChartData.ets new file mode 100644 index 0000000..d63a92c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ChartData.ets @@ -0,0 +1,823 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AxisDependency} from '../components/YAxis'; +import IValueFormatter from '../formatter/IValueFormatter'; +import Highlight from '../highlight/Highlight'; +import IDataSet from '../interfaces/datasets/IDataSet'; +import {JArrayList} from '../utils/JArrayList'; +import MyRect from './Rect'; +import EntryOhos from './EntryOhos'; + +/** + * Class that holds all relevant data that represents the chart. That involves + * at least one (or more) DataSets, and an array of x-values. + */ +export default class ChartData> { + + /** + * maximum y-value in the value array across all axes + */ + protected mYMax : number = -Number.MAX_VALUE; + + /** + * the minimum y-value in the value array across all axes + */ + protected mYMin : number = Number.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected mXMax : number = -Number.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected mXMin : number = Number.MAX_VALUE; + + protected mLeftAxisMax : number = -Number.MAX_VALUE; + + protected mLeftAxisMin : number = Number.MAX_VALUE; + + protected mRightAxisMax = -Number.MAX_VALUE; + + protected mRightAxisMin : number = Number.MAX_VALUE; + + public mDisplayRect: MyRect = new MyRect(); + + /** + * array that holds all DataSets the ChartData object represents + */ + protected mDataSets : JArrayList; + + /** + * Default constructor. + */ + constructor(dataSets ?: JArrayList) { + if (!dataSets) { + this.mDataSets = new JArrayList(); + } else { + this.mDataSets = new JArrayList(); + this.mDataSets.addAll(dataSets); + this.notifyDataChanged(); + } + } + + /** + * Created because Arrays.asList(...) does not support modification. + * + * @param array + * @return + */ + private arrayToList(array : T[]) : JArrayList { + + let list = new JArrayList(); + + for (let i = 0; i < array.length; i++) { + let data : T = array[i]; + list.add(data); + } + + return list; + } + + /** + * Call this method to let the ChartData know that the underlying data has + * changed. Calling this performs all necessary recalculations needed when + * the contained data has changed. + */ + public notifyDataChanged() : void { + this.calcMinMax(); + } + + /** + * Calc minimum and maximum y-values over all DataSets. + * Tell DataSets to recalculate their min and max y-values, this is only needed for autoScaleMinMax. + * + * @param fromX the x-value to start the calculation from + * @param toX the x-value to which the calculation should be performed + */ + public calcMinMaxY(fromX : number, toX : number) : void { + + for (let i = 0; i < this.mDataSets.listSize; i++) { + let data : T = this.mDataSets.at(i); + data.calcMinMaxY(fromX, toX); + } + // apply the new data + this.calcMinMax(); + } + + /** + * Calc minimum and maximum values (both x and y) over all DataSets. + */ + public calcMinMax() { + + if (this.mDataSets == null) + return; + + this.mYMax = -Number.MAX_VALUE; + this.mYMin = Number.MAX_VALUE; + this.mXMax = -Number.MAX_VALUE; + this.mXMin = Number.MAX_VALUE; + + for (let dataSet of this.mDataSets.dataSouce) { + this.calcMinMax1(dataSet); + } + + this.mLeftAxisMax = -Number.MAX_VALUE; + this.mLeftAxisMin = Number.MAX_VALUE; + this.mRightAxisMax = -Number.MAX_VALUE; + this.mRightAxisMin = Number.MAX_VALUE; + + // left axis + let firstLeft : T = this.getFirstLeft(this.mDataSets); + + if (firstLeft) { + + this.mLeftAxisMax = firstLeft.getYMax(); + this.mLeftAxisMin = firstLeft.getYMin(); + + for (let dataSet of this.mDataSets.dataSouce) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) { + if (dataSet.getYMin() < this.mLeftAxisMin) + this. mLeftAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > this.mLeftAxisMax) + this. mLeftAxisMax = dataSet.getYMax(); + } + } + } + + // right axis + let firstRight : T = this.getFirstRight(this.mDataSets); + + if (firstRight) { + + this.mRightAxisMax = firstRight.getYMax(); + this.mRightAxisMin = firstRight.getYMin(); + + for (let dataSet of this.mDataSets.dataSouce) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) { + if (dataSet.getYMin() < this.mRightAxisMin) + this.mRightAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > this.mRightAxisMax) + this.mRightAxisMax = dataSet.getYMax(); + } + } + } + } + + /** ONLY GETTERS AND SETTERS BELOW THIS */ + + /** + * returns the number of LineDataSets this object contains + * + * @return + */ + public getDataSetCount() : number { + if (this.mDataSets == null) { + return 0; + } + + return this.mDataSets.listSize; + } + + /** + * Returns the smallest y-value the data object contains. + * + * @return + */ +// public getYMin() : number { +// return this.mYMin; +// } + + /** + * Returns the minimum y-value for the specified axis. + * + * @param axis + * @return + */ + public getYMin(axis ?: AxisDependency) : number{ + if (axis == null) { + return this.mYMin; + } + if (axis == AxisDependency.LEFT) { + + if (this.mLeftAxisMin == Number.MAX_VALUE) { + return this.mRightAxisMin; + } else + return this.mLeftAxisMin; + } else { + if (this.mRightAxisMin == Number.MAX_VALUE) { + return this.mLeftAxisMin; + } else + return this.mRightAxisMin; + } + } + + /** + * Returns the greatest y-value the data object contains. + * + * @return + */ +// public getYMax() : number { +// return this.mYMax; +// } + + /** + * Returns the maximum y-value for the specified axis. + * + * @param axis + * @return + */ + public getYMax(axis ?: AxisDependency) : number { + if (axis == null) { + return this.mYMax; + } + if (axis == AxisDependency.LEFT) { + + if (this.mLeftAxisMax == -Number.MAX_VALUE) { + return this.mRightAxisMax; + } else + return this.mLeftAxisMax; + } else { + if (this.mRightAxisMax == -Number.MAX_VALUE) { + return this.mLeftAxisMax; + } else + return this.mRightAxisMax; + } + } + + /** + * Returns the minimum x-value this data object contains. + * + * @return + */ + public getXMin() : number { + return this.mXMin; + } + + /** + * Returns the maximum x-value this data object contains. + * + * @return + */ + public getXMax() : number { + return this.mXMax; + } + + /** + * Returns all DataSet objects this ChartData object holds. + * + * @return + */ + public getDataSets() : JArrayList { + return this.mDataSets; + } + + /** + * Retrieve the index of a DataSet with a specific label from the ChartData. + * Search can be case sensitive or not. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param dataSets the DataSet array to search + * @param label + * @param ignorecase if true, the search is not case-sensitive + * @return + */ + protected getDataSetIndexByLabel(dataSets : JArrayList , label : string, + ignorecase : boolean) : number { + + if (ignorecase) { + for (let i : number = 0; i < dataSets.size(); i++) + if (label.toLowerCase() == dataSets.get(i).getLabel().toLowerCase()) + return i; + } else { + for (let i : number = 0; i < dataSets.size(); i++) + if (label == dataSets.get(i).getLabel()) + return i; + } + + return -1; + } + + /** + * Returns the labels of all DataSets as a string array. + * + * @return + */ + public getDataSetLabels() : string[] { + + let types : string[] = new Array(this.mDataSets.listSize); + + for (let i : number = 0; i < this.mDataSets.listSize; i++) { + types[i] = this.mDataSets.get(i).getLabel(); + } + + return types; + } + + /** + * Get the Entry for a corresponding highlight object + * + * @param highlight + * @return the entry that is highlighted + */ + public getEntryForHighlight(highlight : Highlight) : EntryOhos { + if (highlight.getDataSetIndex() >= this.mDataSets.size()) + return null; + else { + return this.mDataSets.get(highlight.getDataSetIndex()).getEntryForXValue(highlight.getX(), highlight.getY()); + } + } + + /** + * Returns the DataSet object with the given label. Search can be case + * sensitive or not. IMPORTANT: This method does calculations at runtime. + * Use with care in performance critical situations. + * + * @param label + * @param ignorecase + * @return + */ + public getDataSetByLabel(label : string, ignorecase : boolean) : T { + + let index : number = this.getDataSetIndexByLabel(this.mDataSets, label, ignorecase); + + if (index < 0 || index >= this.mDataSets.size()) + return null; + else + return this.mDataSets.get(index); + } + + public getDataSetByIndex(index : number) : T { + + if (this.mDataSets == null || index < 0 || index >= this.mDataSets.size()) + return null; + + return this.mDataSets.get(index); + } + + /** + * Adds a DataSet dynamically. + * + * @param d + */ + public addDataSet(d : T) : void { + + if (d == null) + return; + + this.calcMinMax1(d); + + this.mDataSets.add(d); + } + + /** + * Removes the given DataSet from this data object. Also recalculates all + * minimum and maximum values. Returns true if a DataSet was removed, false + * if no DataSet could be removed. + * + * @param d + */ + public removeDataSet(d : T) : boolean{ + + if (d == null) + return false; + + let removed : boolean = this.mDataSets.remove(d); + + // if a DataSet was removed + if (removed) { + this.notifyDataChanged(); + } + + return removed; + } + + /** + * Removes the DataSet at the given index in the DataSet array from the data + * object. Also recalculates all minimum and maximum values. Returns true if + * a DataSet was removed, false if no DataSet could be removed. + * + * @param index + */ + public removeDataSetUnused(index : number) : boolean { + + if (index >= this.mDataSets.size() || index < 0) + return false; + + let data : T = this.mDataSets.get(index); + return this.removeDataSet(data); + } + + /** + * Adds an Entry to the DataSet at the specified index. + * Entries are added to the end of the list. + * + * @param e + * @param dataSetIndex + */ + public addEntry(e : EntryOhos, dataSetIndex : number) : void { + + if (this.mDataSets.size() > dataSetIndex && dataSetIndex >= 0) { + + let data : IDataSet = this.mDataSets.get(dataSetIndex); + // add the entry to the dataset + if (!data.addEntry(e)) + return; + + this.calcMinMax2(e, data.getAxisDependency()); + + } else { + console.log("addEntry", "Cannot add Entry because dataSetIndex too high or too low."); + } + } + + /** + * Adjusts the current minimum and maximum values based on the provided Entry object. + * + * @param e + * @param axis + */ + protected calcMinMax2(e : EntryOhos, axis : AxisDependency) : void { + + if (this.mYMax < e.getY()) + this.mYMax = e.getY(); + if (this.mYMin > e.getY()) + this.mYMin = e.getY(); + + if (this.mXMax < e.getX()) + this.mXMax = e.getX(); + if (this.mXMin > e.getX()) + this.mXMin = e.getX(); + + if (axis == AxisDependency.LEFT) { + + if (this.mLeftAxisMax < e.getY()) + this.mLeftAxisMax = e.getY(); + if (this.mLeftAxisMin > e.getY()) + this.mLeftAxisMin = e.getY(); + } else { + if (this.mRightAxisMax < e.getY()) + this.mRightAxisMax = e.getY(); + if (this.mRightAxisMin > e.getY()) + this.mRightAxisMin = e.getY(); + } + } + + /** + * Adjusts the minimum and maximum values based on the given DataSet. + * + * @param d + */ + protected calcMinMax1(d : T) : void { + + if (this.mYMax < d.getYMax()) + this.mYMax = d.getYMax(); + if (this.mYMin > d.getYMin()) + this.mYMin = d.getYMin(); + + if (this.mXMax < d.getXMax()) + this.mXMax = d.getXMax(); + if (this.mXMin > d.getXMin()) + this.mXMin = d.getXMin(); + + if (d.getAxisDependency() == AxisDependency.LEFT) { + + if (this.mLeftAxisMax < d.getYMax()) + this.mLeftAxisMax = d.getYMax(); + if (this.mLeftAxisMin > d.getYMin()) + this.mLeftAxisMin = d.getYMin(); + } else { + if (this.mRightAxisMax < d.getYMax()) + this.mRightAxisMax = d.getYMax(); + if (this.mRightAxisMin > d.getYMin()) + this.mRightAxisMin = d.getYMin(); + } + } + + /** + * Removes the given Entry object from the DataSet at the specified index. + * + * @param e + * @param dataSetIndex + */ + public removeEntry(e : EntryOhos, dataSetIndex : number) : boolean { + + // entry null, outofbounds + if (e == null || dataSetIndex >= this.mDataSets.listSize) + return false; + + let data : IDataSet = this.mDataSets.get(dataSetIndex); + + if (data != null) { + // remove the entry from the dataset + let removed : boolean = data.removeEntry(e); + if (removed) { + this.notifyDataChanged(); + } + + return removed; + } else + return false; + } + + /** + * Removes the Entry object closest to the given DataSet at the + * specified index. Returns true if an Entry was removed, false if no Entry + * was found that meets the specified requirements. + * + * @param xValue + * @param dataSetIndex + * @return + */ + public removeEntryUnused(xValue : number, dataSetIndex : number) : boolean { + + if (dataSetIndex >= this.mDataSets.size()) + return false; + + let dataSet : IDataSet = this.mDataSets.get(dataSetIndex); + let e : EntryOhos = dataSet.getEntryForXValue(xValue, Number.NaN); + + if (e == null) + return false; + + return this.removeEntry(e, dataSetIndex); + } + + /** + * Returns the DataSet that contains the provided Entry, or null, if no + * DataSet contains this Entry. + * + * @param e + * @return + */ + public getDataSetForEntry(e : EntryOhos) : T { + + if (e == null) + return null; + + for (let i = 0; i < this.mDataSets.size(); i++) { + + let dataSet : T = this.mDataSets.get(i); + + for (let j = 0; j < dataSet.getEntryCount(); j++) { + if (e.equalTo(dataSet.getEntryForXValue(e.getX(), e.getY()))) + return dataSet; + } + } + + return null; + } + + /** + * Returns all colors used across all DataSet objects this object + * represents. + * + * @return + */ + public getColors() : number[] { + + if (this.mDataSets == null) + return null; + + let clrcnt : number = 0; + + for (let i = 0; i < this.mDataSets.listSize; i++) { + clrcnt += this.mDataSets.get(i).getColors().size(); + } + + let colors : number[] = new Array(clrcnt); + let cnt : number = 0 + + for (let i = 0; i < this.mDataSets.size(); i++) { + + let clrs : JArrayList = this.mDataSets.get(i).getColors(); + + for (let j=0; j) : T { + for (let dataSet of sets.dataSouce) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) + return dataSet; + } + return null; + } + + /** + * Returns the first DataSet from the datasets-array that has it's dependency on the right axis. + * Returns null if no DataSet with right dependency could be found. + * + * @return + */ + public getFirstRight(sets : JArrayList) : T { + for (let dataSet of sets.dataSouce) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) + return dataSet; + } + return null; + } + + /** + * Sets a custom IValueFormatter for all DataSets this data object contains. + * + * @param f + */ + public setValueFormatter(f : IValueFormatter) : void { + if (f == null) + return; + else { + for (let data of this.mDataSets.dataSouce) { + data.setValueFormatter(f); + } + } + } + + /** + * Sets the color of the value-text (color in which the value-labels are + * drawn) for all DataSets this data object contains. + * + * @param color + */ + public setValueTextColor(color : number) : void { + for (let data of this.mDataSets.dataSouce) { + data.setValueTextColor(color); + } + } + + /** + * Sets the same list of value-colors for all DataSets this + * data object contains. + * + * @param colors + */ + public setValueTextColors(colors : JArrayList) : void { + for (let data of this.mDataSets.dataSouce) { + data.setValueTextColors(colors); + } + } + + /** + * Sets the Typeface for all value-labels for all DataSets this data object + * contains. + * + * @param tf + */ + public setValueTypeface(tf : FontWeight) : void { + for (let data of this.mDataSets.dataSouce) { + data.setValueTypeface(tf); + } + } + + /** + * Sets the size (in dp) of the value-text for all DataSets this data object + * contains. + * + * @param size + */ + public setValueTextSize(size : number) : void { + for (let data of this.mDataSets.dataSouce) { + data.setValueTextSize(size); + } + } + + /** + * Enables / disables drawing values (value-text) for all DataSets this data + * object contains. + * + * @param enabled + */ + public setDrawValues(enabled : boolean) : void { + for (let data of this.mDataSets.dataSouce) { + data.setDrawValues(enabled); + } + } + + /** + * Enables / disables highlighting values for all DataSets this data object + * contains. If set to true, this means that values can + * be highlighted programmatically or by touch gesture. + */ + public setHighlightEnabled(enabled : boolean) : void { + for (let data of this.mDataSets.dataSouce) { + data.setHighlightEnabled(enabled); + } + } + + /** + * Returns true if highlighting of all underlying values is enabled, false + * if not. + * + * @return + */ + public isHighlightEnabled() : boolean { + for (let data of this.mDataSets.dataSouce) { + if (!data.isHighlightEnabled()) + return false; + } + return true; + } + + /** + * Clears this data object from all DataSets and removes all Entries. Don't + * forget to invalidate the chart after this. + */ + clearValues() : void { + if (this.mDataSets != null) { + this.mDataSets.clear(); + } + this.notifyDataChanged(); + } + + /** + * Checks if this data object contains the specified DataSet. Returns true + * if so, false if not. + * + * @param dataSet + * @return + */ + public contains(dataSet : T) : boolean { + + for (let data of this.mDataSets.dataSouce) { + if (data == dataSet) + return true; + } + + return false; + } + + /** + * Returns the total entry count across all DataSet objects this data object contains. + * + * @return + */ + public getEntryCount() : number { + + let count : number = 0; + + for (let data of this.mDataSets.dataSouce) { + count += data.getEntryCount(); + } + + return count; + } + + /** + * Returns the DataSet object with the maximum number of entries or null if there are no DataSets. + * + * @return + */ + public getMaxEntryCountSet() : T { + + if (this.mDataSets == null || this.mDataSets.isEmpty()) + return null; + + let max : T = this.mDataSets.get(0); + + for (let data of this.mDataSets.dataSouce) { + + if (data.getEntryCount() > max.getEntryCount()) + max = data; + } + + return max; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/DataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/DataSet.ets new file mode 100644 index 0000000..9621dd6 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/DataSet.ets @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {JArrayList} from '../utils/JArrayList'; +import EntryOhos from './EntryOhos' +import BaseDataSet from './BaseDataSet' + +/** + * The DataSet class represents one group or type of entries (Entry) in the + * Chart that belong together. It is designed to logically separate different + * groups of values inside the Chart (e.g. the values for a specific line in the + * LineChart, or the values of a specific group of bars in the BarChart). + * + */ +export abstract class DataSet extends BaseDataSet { + + /** + * the entries that this DataSet represents / holds together + */ + protected mEntries:JArrayList; + + /** + * maximum y-value in the value array + */ + protected mYMax:number= -Number.MAX_VALUE; + + /** + * minimum y-value in the value array + */ + protected mYMin:number = Number.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected mXMax:number= -Number.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected mXMin:number = Number.MAX_VALUE; + + + /** + * Creates a new DataSet object with the given values (entries) it represents. Also, a + * label that describes the DataSet can be specified. The label can also be + * used to retrieve the DataSet from a ChartData object. + * + * @param entries + * @param label + */ + constructor(entries : JArrayList, label : string) { + super(label); + this.mEntries = entries; + + if (!this.mEntries ||this.mEntries== null) + this.mEntries = new JArrayList(); + + this.calcMinMax(); + } + + + public calcMinMax():void { + this.mYMax = -Number.MAX_VALUE; + this.mYMin = Number.MAX_VALUE; + this.mXMax = -Number.MAX_VALUE; + this.mXMin = Number.MAX_VALUE; + if (!this.mEntries||this.mEntries == null || this.mEntries.isEmpty()) + return; + for (let e of this.mEntries.dataSouce) { + this.myCalcMinMax(e); + } + } + + public myCalcMinMax(e ?: T) : void { + if (!e) + return; + this.calcMinMaxX(e); + this.myCalcMinMaxY(e); + } + + public calcMinMaxY( fromX:number, toX:number):void{ + this.mYMax = -Number.MAX_VALUE; + this.mYMin = Number.MAX_VALUE; + + if (!this.mEntries||this.mEntries == null || this.mEntries.isEmpty()) { + return; + } + var indexFrom : number = this.getEntryIndex(fromX, Number.NaN, Rounding.DOWN); + var indexTo : number = this.getEntryIndex(toX, Number.NaN, Rounding.UP); + + if (indexTo < indexFrom) return; + + for (var i : number = indexFrom; i <= indexTo; i++) { + // only recalculate y + this.myCalcMinMaxY(this.mEntries.get(i)); + } + } + + + protected calcMinMaxX(e : T) : void { + + if (e.getX() < this.mXMin) + this.mXMin = e.getX(); + + if (e.getX() > this.mXMax) + this.mXMax = e.getX(); + } + + protected myCalcMinMaxY(e : T) : void { + if (e.getY() < this.mYMin) + this.mYMin = e.getY(); + + if (e.getY() > this.mYMax) + this.mYMax = e.getY(); + } + + public getEntryCount() : number { + return this.mEntries.size(); + } + + // /** + // * This method is deprecated. + // * Use getEntries() instead. + // * + // * @return + // */ + // @Deprecated + public getValues() : JArrayList { + return this.mEntries; + } + + /** + * Returns the array of entries that this DataSet represents. + * + * @return + */ + public getEntries() : JArrayList { + return this.mEntries; + } + + // /** + // * This method is deprecated. + // * Use setEntries(...) instead. + // * + // * @param values + // */ + // @Deprecated + public setValues(values : JArrayList) : void { + this.setEntries(values); + } + + /** + * Sets the array of entries that this DataSet represents, and calls notifyDataSetChanged() + * + * @return + */ + public setEntries(entries : JArrayList) : void { + this.mEntries = entries; + this.notifyDataSetChanged(); + } + + /** + * + * @param dataSet + */ + public copy(): DataSet { + return null; + } + + protected copyTo(dataSet :DataSet) : void { + super.copyTo(dataSet); + } + + public toString():string { + var str:string=this.toSimpleString() + for (var i : number = 0; i < this.mEntries.size(); i++) { + str+=this.mEntries.get(i).toString() + " " + } + return str; + } + + /** + * Returns a simple string representation of the DataSet with the type and + * the number of Entries. + * + * @return + */ + public toSimpleString() : string { + var str:string="DataSet, label: " + (!this.getLabel() ? "" : this.getLabel()) + ", entries: " + this.mEntries.size() +"\n"; + return str + } + + public getYMin() : number { + return this.mYMin; + } + + public getYMax() : number { + return this.mYMax; + } + + public getXMin() : number { + return this.mXMin; + } + + public getXMax() : number { + return this.mXMax; + } + + public addEntryOrdered(e : T) : void { + + if (!e) + return; + + if (!this.mEntries) { + this.mEntries = new JArrayList(); + } + + this.myCalcMinMax(e); + + if (this.mEntries.size() > 0 && this.mEntries.get(this.mEntries.size() - 1).getX() > e.getX()) { + var closestIndex : number = this.getEntryIndex(e.getX(), e.getY(), Rounding.UP); + // this.mEntries.add(closestIndex, e); + } else { + this.mEntries.add(e); + } + } + + public clear() : void { + this.mEntries.clear(); + this.notifyDataSetChanged(); + } + + public addEntry(e : T) : boolean { + + if (!e) + return false; + + var values : JArrayList = this.getEntries(); + if (!values) { + values = new JArrayList(); + } + + this.myCalcMinMax(e); + + // add the entry + values.add(e); + return true; + } + + public removeEntry(e : T) : boolean { + if (!e) + return false; + + if (!this.mEntries) + return false; + + // remove the entry + var removed : boolean = this.mEntries.remove(e); + + if (removed) { + this.calcMinMax(); + } + + return removed; + } + + public getEntryIndexByEntry(e : T) : number { + return this.mEntries.indexOf(e); + } + + public getEntryForXValue(xValue : number, closestToY : number, rounding : Rounding) : T { + var myRounding : Rounding + if(rounding==null){ + myRounding=Rounding.CLOSEST; + }else{ + myRounding=rounding; + } + var index : number = this.getEntryIndex(xValue, closestToY, myRounding); + if (index > -1) + return this.mEntries.get(index); + } + + public getEntryForIndex(index : number):T { + return this.mEntries.get(index); + } + public getEntryIndex(xValue : number, closestToY : number, rounding : Rounding) : number { + + if (!this.mEntries|| this.mEntries.isEmpty()) + return -1; + + var low : number = 0; + var high : number = this.mEntries.size() - 1; + var closest : number = high; + + while (low < high) { + var m : number = Math.floor((low + high) / 2); + var d1 : number = this.mEntries.get(m).getX() - xValue; + var d2 : number = this.mEntries.get(m + 1).getX() - xValue; + var ad1 : number = Math.abs(d1), ad2 = Math.abs(d2); + + if (ad2 < ad1) { + // [m + 1] is closer to xValue + // Search in an higher place + low = m + 1; + } else if (ad1 < ad2) { + // [m] is closer to xValue + // Search in a lower place + high = m; + } else { + // We have multiple sequential x-value with same distance + if (d1 >= 0.0) { + // Search in a lower place + high = m; + } else if (d1 < 0.0) { + // Search in an higher place + low = m + 1; + } + } + closest = high; + } + + if (closest != -1) { + var closestXValue : number = this.mEntries.get(closest).getX(); + if (rounding == Rounding.UP) { + // If rounding up, and found x-value is lower than specified x, and we can go upper... + if (closestXValue < xValue && closest < this.mEntries.size() - 1) { + ++closest; + } + } else if (rounding == Rounding.DOWN) { + // If rounding down, and found x-value is upper than specified x, and we can go lower... + if (closestXValue > xValue && closest > 0) { + --closest; + } + } + // Search by closest to y-value + if (!Number.isNaN(closestToY)) { + while (closest > 0 && this.mEntries.get(closest - 1).getX() == closestXValue) + closest -= 1; + + var closestYValue : number = this.mEntries.get(closest).getY(); + var closestYIndex : number = closest; + + while (true) { + closest += 1; + if (closest >= this.mEntries.size()) + break; + + var value : EntryOhos = this.mEntries.get(closest); + + if (value.getX() != closestXValue) + break; + + if (Math.abs(value.getY() - closestToY) <= Math.abs(closestYValue - closestToY)) { + closestYValue = closestToY; + closestYIndex = closest; + } + } + closest = closestYIndex; + } + } + return closest; + } + + public getEntriesForXValue(xValue : number) : JArrayList { + + var entries : JArrayList = new JArrayList(); + var low : number = 0; + var high : number = this.mEntries.size() - 1; + + while (low <= high) { + var m : number = (high + low) / 2; + var entry:T = this.mEntries.get(m); + // if we have a match + if (xValue == entry.getX()) { + while (m > 0 && this.mEntries.get(m - 1).getX() == xValue) + m--; + + high = this.mEntries.size(); + + // loop over all "equal" entries + for (; m < high; m++) { + entry = this.mEntries.get(m); + if (entry.getX() == xValue) { + entries.add(entry); + } else { + break; + } + } + + break; + } else { + if (xValue > entry.getX()) + low = m + 1; + else + high = m - 1; + } + } + + return entries; + } +} + +/** + * Determines how to round DataSet index values for + * {@link DataSet#getEntryIndex(float, float, Rounding)} DataSet.getEntryIndex()} + * when an exact x-index is not found. + */ +export enum Rounding { + UP, + DOWN, + CLOSEST, +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/EntryOhos.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/EntryOhos.ets new file mode 100644 index 0000000..c98795a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/EntryOhos.ets @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Utils from '../utils/Utils'; +import { ImagePaint } from './Paint'; +import BaseEntry from './BaseEntry'; + + +/** + * Class representing one entry in the chart. Might contain multiple values. + * Might only contain a single value depending on the used constructor. + * + */ +export default class EntryOhos extends BaseEntry{ + + private x:number = 0; + + constructor(x?:number, y?:number,icon?:ImagePaint,data?:Object) { + super(y, icon, data); + this.x = x; + } + + /** + * Returns the x-value of this Entry object. + * + * @return + */ + public getX():number{ + return this.x; + } + + /** + * Sets the x-value of this Entry object. + * + * @param x + */ + public setX(x:number):void { + this.x = x; + } + + /** + * returns an exact copy of the entry + * + * @return + */ + public copy():EntryOhos{ + var e:EntryOhos= new EntryOhos(this.x, this.getY(),null, this.getData()); + return e; + } + + /** + * Compares value, xIndex and data of the entries. Returns true if entries + * are equal in those points, false if not. Does not check by hash-code like + * it's done by the "equals" method. + * + * @param e + * @return + */ + public equalTo( e:EntryOhos):boolean{ + + if (!e) + return false; + + if (e.getData() != this.getData()) + return false; + + if (Math.abs(e.x - this.x) > Utils.FLOAT_EPSILON) + return false; + + if (Math.abs(e.getY() - this.getY()) > Utils.FLOAT_EPSILON) + return false; + + return true; + } + + /** + * returns a string representation of the entry containing x-index and value + */ + public toString():String { + return "Entry, x: " + this.x + " y: " + this.getY(); + } + + public describeContents():number { + return 0; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineData.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineData.ets new file mode 100644 index 0000000..163740c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineData.ets @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ILineDataSet from '../interfaces/datasets/ILineDataSet'; +import {JArrayList} from '../utils/JArrayList'; +import BarLineScatterCandleBubbleData from './BarLineScatterCandleBubbleData'; + +/** + * Data object that encapsulates all data associated with a LineChart. + * + */ +export default class LineData extends BarLineScatterCandleBubbleData { + + public constructor(dataSets ?: JArrayList) { + super(dataSets); + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineDataSet.ets new file mode 100644 index 0000000..1ccbce1 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineDataSet.ets @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DashPathEffect } from './Paint'; +import { DataSet } from './DataSet'; +import LineRadarDataSet from './LineRadarDataSet' +import EntryOhos from './EntryOhos' +import ILineDataSet from '../interfaces/datasets/ILineDataSet' +import IFillFormatter from '../formatter/IFillFormatter' +import DefaultFillFormatter from '../formatter/DefaultFillFormatter' +import Utils from '../utils/Utils' +import {JArrayList} from '../utils/JArrayList' +import ColorTemplate from '../utils/ColorTemplate' + + +export class LineDataSet extends LineRadarDataSet implements ILineDataSet { + /** + * Drawing mode for this line dataset + **/ + private mMode: Mode = Mode.LINEAR; + + private fillStyle: FillStyle = FillStyle.MIN; + + /** + * List representing all colors that are used for the circles + */ + private mCircleColors: JArrayList = null; + + private mCircleColor: number = Color.White; + + /** + * the color of the inner circles + */ + private mCircleHoleColor: number = Color.White; + + /** + * the radius of the circle-shaped value indicators + */ + private mCircleRadius: number = 8; + + /** + * the hole radius of the circle-shaped value indicators + */ + private mCircleHoleRadius: number = 4; + + /** + * sets the intensity of the cubic lines + */ + private mCubicIntensity: number = 0.2; + + /** + * the path effect of this DataSet that makes dashed lines possible + */ + private mDashPathEffect: DashPathEffect = null; + + /** + * formatter for customizing the position of the fill-line + */ + private mFillFormatter = new DefaultFillFormatter(); + + /** + * if true, drawing circles is enabled + */ + private mDrawCircles: boolean = true; + private mDrawCircleHole: boolean = true; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + if (!this.mCircleColors) { + this.mCircleColors = new JArrayList(); + } + this.mCircleColors.clear(); + // this.mCircleColors.add(Color.rgb(140, 234, 255)); + this.mCircleColors.add(0x8ceaff); + + } + + public copy(): DataSet { + let entries = new JArrayList(); + for (let i = 0; i < this.mEntries.size(); i++) { + entries.add(this.mEntries.get(i).copy()); + } + let copied = new LineDataSet(entries, this.getLabel()); + this.copyTo(copied); + return copied; + } + + protected copyTo(lineDataSet: LineDataSet): void { + super.copyTo(lineDataSet); + lineDataSet.mCircleColors = this.mCircleColors; + lineDataSet.mCircleHoleColor = this.mCircleHoleColor; + lineDataSet.mCircleHoleRadius = this.mCircleHoleRadius; + lineDataSet.mCircleRadius = this.mCircleRadius; + lineDataSet.mCubicIntensity = this.mCubicIntensity; + lineDataSet.mDashPathEffect = this.mDashPathEffect; + lineDataSet.mDrawCircleHole = this.mDrawCircleHole; + lineDataSet.mDrawCircles = this.mDrawCircleHole; + lineDataSet.mFillFormatter = this.mFillFormatter; + lineDataSet.mMode = this.mMode; + } + + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + public getMode(): Mode { + return this.mMode; + } + + /** + * Returns the drawing mode for this LineDataSet + * + * @return + */ + public setMode(mode: Mode): void { + this.mMode = mode; + } + + /** + * Sets the intensity for cubic lines (if enabled). Max = 1f = very cubic, + * Min = 0.05f = low cubic effect, Default: 0.2f + * + * @param intensity + */ + public setCubicIntensity(intensity: number): void { + if (intensity > 1) + intensity = 1; + if (intensity < 0.05) + intensity = 0.05; + ; + this.mCubicIntensity = intensity; + } + + public getCubicIntensity(): number { + return this.mCubicIntensity; + } + + /** + * Sets the radius of the drawn circles. + * Default radius = 4f, Min = 1f + * + * @param radius + */ + public setCircleRadius(radius: number): void { + + if (radius >= 1) { +// this.mCircleRadius = Utils.convertDpToPixel(radius); + this.mCircleRadius = radius; + } else { + console.log("LineDataSet", "Circle radius cannot be < 1") + } + } + + public getCircleRadius(): number { + return this.mCircleRadius; + } + + /** + * Sets the hole radius of the drawn circles. + * Default radius = 2f, Min = 0.5f + * + * @param holeRadius + */ + public setCircleHoleRadius(holeRadius: number): void{ + + if (holeRadius >= 0.5) { +// this.mCircleHoleRadius = Utils.convertDpToPixel(holeRadius); + this.mCircleHoleRadius = holeRadius; + } else { + console.log("LineDataSet", "Circle radius cannot be < 0.5") + } + } + + public getCircleHoleRadius(): number { + return this.mCircleHoleRadius; + } + + /** + * sets the size (radius) of the circle shpaed value indicators, + * default size = 4f + *

+ * This method is deprecated because of unclarity. Use setCircleRadius instead. + * + * @param size + */ + // @Deprecated + public setCircleSize(size: number): void { + this.setCircleRadius(size); + } + + /** + * This function is deprecated because of unclarity. Use getCircleRadius instead. + */ + // @Deprecated + public getCircleSize(): number { + return this.getCircleRadius(); + } + + public enableDashedLine(lineLength: number, spaceLength: number, phase: number): void { + this.mDashPathEffect = new DashPathEffect([lineLength, spaceLength], phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public disableDashedLine(): void { + this.mDashPathEffect = null; + } + + public isDashedLineEnabled(): boolean { + return this.mDashPathEffect == null ? false : true; + } + + public getDashPathEffect(): DashPathEffect { + return this.mDashPathEffect; + } + + /** + * set this to true to enable the drawing of circle indicators for this + * DataSet, default true + * + * @param enabled + */ + public setDrawCircles(enabled: boolean): void { + this.mDrawCircles = enabled; + } + + public isDrawCirclesEnabled(): boolean { + return this.mDrawCircles; + } + + // @Deprecated + public isDrawCubicEnabled(): boolean { + return this.mMode == Mode.CUBIC_BEZIER; + } + + // @Deprecated + public isDrawSteppedEnabled(): boolean { + return this.mMode == Mode.STEPPED; + } + + /** ALL CODE BELOW RELATED TO CIRCLE-COLORS */ + + /** + * returns all colors specified for the circles + * + * @return + */ + public getCircleColors(): JArrayList { + return this.mCircleColors; + } + + public getCircleColorByIndex(index: number): number { + return this.mCircleColors.get(index).valueOf(); + } + + public getCircleColorCount(): number { + return this.mCircleColors.length(); + } + + + public setCircleColorsByArray(colors: JArrayList): void{ + this.mCircleColors = colors; + } + + public setCircleColors(colors:number[]):void { + this.mCircleColors = ColorTemplate.createColors(colors); + } + + public setCircleColorsByArrayAndCon(colors: number[]): void { + var clrs: JArrayList = this.mCircleColors; + if (clrs == null) { + clrs = new JArrayList(); + } + clrs.clear(); + for (let color of colors) { + clrs.add(color); + } + this.mCircleColors = clrs; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public setCircleColor(color: number): void { + this.resetCircleColors(); + this.mCircleColor = color; + this.mCircleColors.add(color); + } + + public getCircleColor(): number { + return this.mCircleColor; + } + + /** + * resets the circle-colors array and creates a new one + */ + public resetCircleColors(): void { + if (this.mCircleColors == null) { + this.mCircleColors = new JArrayList(); + } + this.mCircleColors.clear(); + } + + /** + * Sets the color of the inner circle of the line-circles. + * + * @param color + */ + public setCircleHoleColor(color: number): void { + this.mCircleHoleColor = color; + } + + public getCircleHoleColor(): number { + return this.mCircleHoleColor; + } + + /** + * Set this to true to allow drawing a hole in each data circle. + * + * @param enabled + */ + public setDrawCircleHole(enabled: boolean): void { + this.mDrawCircleHole = enabled; + } + + public isDrawCircleHoleEnabled(): boolean { + return this.mDrawCircleHole; + } + + /** + * Sets a custom IFillFormatter to the chart that handles the position of the + * filled-line for each DataSet. Set this to null to use the default logic. + * + * @param formatter + */ + public setFillFormatter(formatter: IFillFormatter): void { + + if (!formatter) { + this.mFillFormatter = new DefaultFillFormatter(); + } else { + if (formatter instanceof DefaultFillFormatter) { + this.mFillFormatter = formatter; + } + } + } + + public getFillFormatter(): IFillFormatter { + return this.mFillFormatter; + } + getDashPathEffectHighlight():DashPathEffect{ + return + } + + + public setFillStyle(fillStyle: FillStyle) { + this.fillStyle = fillStyle; + } + + public getFillStyle(): FillStyle { + return this.fillStyle; + } +} + +export type ColorStop = [Color | string | number,number]; + +export enum Mode { + LINEAR, + STEPPED, + CUBIC_BEZIER, + HORIZONTAL_BEZIER +} + +export enum FillStyle { + MIN, + MAX +} + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineRadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineRadarDataSet.ets new file mode 100644 index 0000000..2b9829c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineRadarDataSet.ets @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ColorStop } from '../data/LineDataSet'; +import EntryOhos from './EntryOhos' +import LineScatterCandleRadarDataSet from './LineScatterCandleRadarDataSet' +import ILineRadarDataSet from '../interfaces/datasets/ILineRadarDataSet' +import {JArrayList} from '../utils/JArrayList' +import Utils from '../utils/Utils' + +/** + * Base dataset for line and radar DataSets. + */ +export default abstract class LineRadarDataSet extends LineScatterCandleRadarDataSet implements ILineRadarDataSet { + +// TODO: Move to using `Fill` class + /** + * the color that is used for filling the line surface + */ +// private mFillColor: number = Color.rgb(140, 234, 255); + private mFillColor: number = 0x8CEAFF; + private mLinearGradientColors: Array; + + /** + * the drawable to be used for filling the line surface + */ + protected mFillDrawable:Object/*Drawable*/; + + /** + * transparency used for filling line surface + */ + private mFillAlpha: number = 85; + + /** + * the width of the drawn data lines + */ + private mLineWidth: number = 2.5; + + /** + * if true, the data will also be drawn filled + */ + private mDrawFilled: boolean = false; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + } + + public getFillColor(): number{ + return this.mFillColor; + } + + /** + * Sets the color that is used for filling the area below the line. + * Resets an eventually set "fillDrawable". + * + * @param color + */ + public setFillColor(color: number): void { + this.mFillColor = color; + this.mFillDrawable = null; + } + + public setGradientFillColor(linearGradientColors: Array): void { + this.mLinearGradientColors = linearGradientColors; + this.mFillDrawable = null; + this.mFillColor = null; + } + + public getGradientFillColor(): Array { + return this.mLinearGradientColors; + } + + public getFillDrawable():Object/*Drawable*/ { + return this.mFillDrawable; + } + + /** + * Sets the drawable to be used to fill the area below the line. + * + * @param drawable + */ +// @TargetApi(18) + public setFillDrawable(drawable:Object/*Drawable*/): void { + this.mFillDrawable = drawable; + } + + public getFillAlpha(): number{ + return this.mFillAlpha; + } + + /** + * sets the alpha value (transparency) that is used for filling the line + * surface (0-255), default: 85 + * + * @param alpha + */ + public setFillAlpha(alpha:number):void { + this.mFillAlpha = alpha; + } + + /** + * set the line width of the chart (min = 0.2f, max = 10f); default 1f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public setLineWidth(width:number):void { + + if (width < 0.0) + width = 0.0; + if (width > 10.0) + width = 10.0; + this.mLineWidth = width; + } + + public getLineWidth():number { + return this.mLineWidth; + } + + public setDrawFilled(filled:boolean):void { + this.mDrawFilled = filled; + } + + public isDrawFilledEnabled():boolean { + return this.mDrawFilled; + } + + protected copyTo(lineRadarDataSet:LineRadarDataSet): void { + super.copyTo(lineRadarDataSet); + lineRadarDataSet.mDrawFilled = this.mDrawFilled; + lineRadarDataSet.mFillAlpha = this.mFillAlpha; + lineRadarDataSet.mFillColor = this.mFillColor; + lineRadarDataSet.mFillDrawable = this.mFillDrawable; + lineRadarDataSet.mLineWidth = this.mLineWidth; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineScatterCandleRadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineScatterCandleRadarDataSet.ets new file mode 100644 index 0000000..9ecde71 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/LineScatterCandleRadarDataSet.ets @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DashPathEffect } from './Paint'; +import EntryOhos from './EntryOhos' +import BarLineScatterCandleBubbleDataSet from './BarLineScatterCandleBubbleDataSet' +import ILineScatterCandleRadarDataSet from '../interfaces/datasets/ILineScatterCandleRadarDataSet' +import Utils from '../utils/Utils' +import {JArrayList} from '../utils/JArrayList' + +export default abstract class LineScatterCandleRadarDataSet extends BarLineScatterCandleBubbleDataSet implements ILineScatterCandleRadarDataSet { + protected mDrawVerticalHighlightIndicator: boolean = true; + protected mDrawHorizontalHighlightIndicator: boolean = true; + + /** the width of the highlight indicator lines */ + protected mHighlightLineWidth: number = 0.5; + + /** the path effect for dashed highlight-lines */ + protected mHighlightDashPathEffect:DashPathEffect = null; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + this.mHighlightLineWidth = Utils.convertDpToPixel(0.5); + } + + /** + * Enables / disables the horizontal highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public setDrawHorizontalHighlightIndicator(enabled: boolean): void { + this.mDrawHorizontalHighlightIndicator = enabled; + } + + /** + * Enables / disables the vertical highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public setDrawVerticalHighlightIndicator(enabled: boolean): void { + this.mDrawVerticalHighlightIndicator = enabled; + } + + /** + * Enables / disables both vertical and horizontal highlight-indicators. + * @param enabled + */ + public setDrawHighlightIndicators(enabled: boolean): void { + this.setDrawVerticalHighlightIndicator(enabled); + this.setDrawHorizontalHighlightIndicator(enabled); + } + + public isVerticalHighlightIndicatorEnabled(): boolean { + return this.mDrawVerticalHighlightIndicator; + } + + public isHorizontalHighlightIndicatorEnabled(): boolean { + return this.mDrawHorizontalHighlightIndicator; + } + + /** + * Sets the width of the highlight line in dp. + * @param width + */ + public setHighlightLineWidth(width: number): void { + this.mHighlightLineWidth = Utils.convertDpToPixel(width); + } + + public getHighlightLineWidth(): number{ + return this.mHighlightLineWidth; + } + + /** + * Enables the highlight-line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the line-pieces + * @param phase offset, in degrees (normally, use 0) + */ + public enableDashedHighlightLine(lineLength: number, spaceLength: number, phase: number): void { + var arr=[lineLength, spaceLength]; + this.mHighlightDashPathEffect = new DashPathEffect(arr, + phase); + } + + /** + * Disables the highlight-line to be drawn in dashed mode. + */ + public disableDashedHighlightLine(): void { + this.mHighlightDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled for highlight lines, false if not. + * Default: disabled + * @return + */ + public isDashedHighlightLineEnabled(): boolean { + return !this.mHighlightDashPathEffect? false : true; + } + + public getDashPathEffectHighlight():DashPathEffect /*DashPathEffect*/ { + return this.mHighlightDashPathEffect; + } + + protected copyTo(lineScatterCandleRadarDataSet: LineScatterCandleRadarDataSet): void { + super.copyTo(lineScatterCandleRadarDataSet); + lineScatterCandleRadarDataSet.mDrawHorizontalHighlightIndicator = this.mDrawHorizontalHighlightIndicator; + lineScatterCandleRadarDataSet.mDrawVerticalHighlightIndicator = this.mDrawVerticalHighlightIndicator; + lineScatterCandleRadarDataSet.mHighlightLineWidth = this.mHighlightLineWidth; + lineScatterCandleRadarDataSet.mHighlightDashPathEffect = this.mHighlightDashPathEffect; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Paint.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Paint.ets new file mode 100644 index 0000000..4586f43 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Paint.ets @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ColorStop } from '../data/LineDataSet'; +import { JArrayList } from '../utils/JArrayList'; +import { AxisDependency } from '../components/YAxis'; +import { FillStyle } from './LineDataSet'; +/** + * 画笔属性类,用于绘制时使用画笔属性 + * @param value + */ +export default class Paint { + color: Color | number | string | Resource = Color.Black; + textSize: number = 10; + fill: Color | number | string | Resource = null; + stroke: Color | number | string | Resource = null; + strokeWidth: number = 0; + strokeRadius: number = 0; + style: Style = null; + alpha: number = 1; + typeface: FontWeight = FontWeight.Normal; + textAlign: TextAlign = TextAlign.Start; + align: Align = Align.LEFT; + strokeDashArray: number[] = [0,0]; + strokeDashOffset: number = 0; + dashPathEffect: DashPathEffect; + x: number = 0; + y: number = 0; + width: number | string = null; + height: number | string = null; + visibility: Visibility = Visibility.Visible + clickPosition = -1; + value = 0; + translateX = 0; + translateY = 0; + + constructor(paint?:Paint){ + if(paint != null && paint != undefined){ + this.color = paint.color; + this.textSize = paint.textSize; + this.fill = paint.fill; + this.stroke = paint.stroke; + this.strokeWidth = paint.strokeWidth; + this.strokeRadius = paint.strokeRadius; + this.style = paint.style; + this.alpha = paint.alpha; + this.typeface = paint.typeface; + this.textAlign = paint.textAlign; + this.strokeDashArray = paint.strokeDashArray; + this.strokeDashOffset = paint.strokeDashOffset; + this.dashPathEffect = paint.dashPathEffect; + this.x = paint.x; + this.y = paint.y; + this.width = paint.width; + this.height = paint.height; + this.visibility = paint.visibility; + this.clickPosition = paint.clickPosition; + this.value = paint.value; + } + } + + public set(paint:Paint){ + this.color = paint.color; + this.textSize = paint.textSize; + this.fill = paint.fill; + this.stroke = paint.stroke; + this.strokeWidth = paint.strokeWidth; + this.strokeRadius = paint.strokeRadius; + this.style = paint.style; + this.alpha = paint.alpha; + this.typeface = paint.typeface; + this.textAlign = paint.textAlign; + this.strokeDashArray = paint.strokeDashArray; + this.strokeDashOffset = paint.strokeDashOffset; + this.dashPathEffect = paint.dashPathEffect; + this.x = paint.x; + this.y = paint.y; + this.width = paint.width; + this.height = paint.height; + this.visibility = paint.visibility; + this.clickPosition = paint.clickPosition; + this.value = paint.value; + } + + setColor(value: Color | number | string | Resource) { + this.color = value + switch(this.style){ + case Style.STROKE: + this.setStroke(value) + break; + case Style.FILL: + this.setFill(value) + break; + case Style.FILL_AND_STROKE: + this.setStroke(value) + this.setFill(value) + break; + } + } + + getColor(): Color | number | string | Resource{ + return this.color; + } + + setTextSize(value: number) { + this.textSize = value + } + + getTextSize(): number{ + return this.textSize; + } + + setFill(value: Color | number | string | Resource){ + this.fill = value + } + + setStroke(value: Color | number | string | Resource){ + this.stroke = value + } + + setStrokeWidth(value: number) { + this.strokeWidth = value + } + + getStrokeWidth(): number{ + return this.strokeWidth; + } + + setStrokeRadius(value: number){ + this.strokeRadius = value; + } + + getStrokeRadius(): number{ + return this.strokeRadius; + } + + setStyle(value: Style) { + this.style = value + switch(value){ + case Style.STROKE: + this.setStroke(this.color); + break; + case Style.FILL: + this.setFill(this.color); + break; + case Style.FILL_AND_STROKE: + this.setStroke(this.color); + this.setFill(this.color); + break; + } + } + + getStyle(): Style{ + return this.style + } + + setAlpha(value: number) { + this.alpha = value + } + + getAlpha(): number{ + return this.alpha; + } + + setTypeface(value: FontWeight) { + this.typeface = value + } + + getTypeface(): number{ + return this.typeface; + } + + setTextAlign(value: TextAlign) { + this.textAlign = value + } + + getTextAlign(): TextAlign{ + return this.textAlign; + } + + setAlign(value: Align) { + this.align = value + } + + getAlign(): Align{ + return this.align; + } + + setStrokeDashArray(value: number[]) { + this.strokeDashArray = value + } + + getStrokeDashArray(): number[]{ + return this.strokeDashArray + } + + setStrokeDashOffset(value: number) { + this.strokeDashOffset = value + } + + getStrokeDashOffset(): number{ + return this.strokeDashOffset + } + + setDashPathEffect(value: DashPathEffect) { + this.dashPathEffect = value; + if(this.dashPathEffect != null){ + this.setStrokeDashArray(this.dashPathEffect.dash) + this.setStrokeDashOffset(this.dashPathEffect.offset) + } + } + + getDashPathEffect(): DashPathEffect{ + return this.dashPathEffect; + } + + setX(value: number) { + this.x = value + } + + getX(): number{ + return this.x + } + + setY(value: number) { + this.y = value + } + + getY(): number{ + return this.y + } + + setWidth(value: number | string) { + this.width = value; + } + + getWidth(): number | string{ + return this.width + } + + setHeight(value: number | string) { + this.height = value + } + + getHeight(): number | string{ + return this.height + } + + setVisibility(visibility: Visibility){ + this.visibility = visibility; + } + + setClickPosition(position: number){ + this.clickPosition = position; + } + +} + +/** + * 用于绘制Line的属性类 + */ +export class LinePaint extends Paint { + startPoint: number[] = [0, 0]; + endPoint: number[] = [0, 0] + + constructor(paint?:LinePaint){ + super(paint) + if(paint != null && paint != undefined){ + this.startPoint = paint.startPoint; + this.endPoint = paint.endPoint; + } + } + + setStartPoint(value: number[]) { + this.startPoint = value + } + + getStartPoint(): number[]{ + return this.startPoint + } + + setEndPoint(value: number[]) { + this.endPoint = value + } + + getEndPoint(): number[]{ + return this.endPoint + } +} + +export class TextPaint extends Paint { + text: string = ""; + constructor(paint?:TextPaint){ + super(paint) + if(paint != null && paint != undefined){ + this.text = paint.text; + } + } + translateX: number = 0; + translateY: number = 0; + rotate: number = 0; + setText(value: string) { + this.text = value + } + + getText(): string{ + return this.text + } + + setTranslateX(value: number) { + this.translateX = value + } + + getTranslateX(): number{ + return this.translateX + } + setTranslateY(value: number) { + this.translateY = value + } + + getTranslateY(): number{ + return this.translateY + } + setRotate(value: number) { + this.rotate = value + } + + getRotate(): number{ + return this.rotate + } +} + +export class PathPaint extends Paint { + commands: string = ""; + commandsFill: string = ""; + filled: boolean = false; + linearGradientColors: Array = null; + enabled: boolean = false; + circleColor: string | number | Color = Color.White; + colors:JArrayList = null; + radius: number = 2; + circleHoleRadius: number = 1; + circleHoleColor: string | number | Color = Color.White; + circleHoleEnabled: boolean = true; + drawValueEnable: boolean = true; + rotate:number + rotateText:number + percentage:string = '' + filledColor:number = 0 + label:string = '' + axisDependency:AxisDependency = AxisDependency.LEFT + lineSvg:string = '' + fillStyle: FillStyle = FillStyle.MIN; + positionX:number + positionY:number + labelX:number + labelY:number + iconX:number = 0 + iconY:number = 0 + + constructor(paint?:PathPaint){ + super(paint) + if(paint != null && paint != undefined){ + this.commands = paint.commands; + } + } + + setIconY(iconY:number){ + this.iconY = iconY + } + setIconX(iconX:number){ + this.iconX = iconX + } + setLabelX(labelX:number){ + this.labelX = labelX + } + setLabelY(labelY:number){ + this.labelY = labelY + } + setPositionX(positionX:number){ + this.positionX = positionX + } + setPositionY(positionY:number){ + this.positionY = positionY + } + + setLineSvg(lineSvg:string){ + this.lineSvg = lineSvg + } + setLabel(label:string){ + this.label = label + } + + getLabel():string{ + return this.label + } + setFilledColor(filledColor:number){ + this.filledColor = filledColor + } + getFilledColor():number{ + return this.filledColor + } + setRotateText(rotateText: number){ + this.rotateText = rotateText; + } + + getRotateText():number{ + return this.rotateText + } + + setPercentage(percentage:string){ + this.percentage = percentage + } + + getPercentage():string{ + return this.percentage + } + + setRotate(value:number){ + this.rotate = value + } + + setCommands(value: string){ + this.commands = value; + } + + setCommandsFill(commandsFill: string){ + this.commandsFill = commandsFill; + } + + public setFillStyle(fillStyle: FillStyle){ + this.fillStyle = fillStyle; + } + + setDrawFilled(filled:boolean){ + this.filled = filled + } + + isDrawFilledEnabled(): boolean{ + return this.filled + } + + setGradientFillColor(linearGradientColors: Array): void{ + this.linearGradientColors = linearGradientColors; + } + + setColors(colors: JArrayList){ + this.colors = colors + } + + setDrawCircles(enabled: boolean){ + this.enabled = enabled + } + + isDrawCirclesEnabled(): boolean{ + return this.enabled + } + + setCirclesColor(circleColor: string | number | Color){ + this.circleColor = circleColor; + } + + setCircleRadius(radius: number){ + this.radius = radius; + } + + setCircleHoleRadius(circleHoleRadius: number){ + this.circleHoleRadius = circleHoleRadius; + } + + setCircleHoleColor(circleHoleColor: string | number | Color){ + this.circleHoleColor = circleHoleColor; + } + + setDrawCircleHole(circleHoleEnabled: boolean){ + this.circleHoleEnabled = circleHoleEnabled; + } + + setDrawValueEnable(drawValueEnable: boolean){ + this.drawValueEnable = drawValueEnable; + } + + setAxisDependency(axisDependency:AxisDependency){ + this.axisDependency = axisDependency; + } + +} + +export class PathFillPaint extends Paint { + commandsFill: string = ""; + filled: boolean = false; + linearGradientColors: Array = null; + + constructor(paint?:PathFillPaint){ + super(paint) + } + + setCommandsFill(commandsFill: string){ + this.commandsFill = commandsFill; + } + + setDrawFilled(filled:boolean){ + this.filled = filled + } + + isDrawFilledEnabled(): boolean{ + return this.filled + } + + setGradientFillColor(linearGradientColors: Array): void{ + this.linearGradientColors = linearGradientColors; + } +} + +export enum Style { + FILL, + STROKE, + FILL_AND_STROKE +} + +/** + * 用于绘制Legend的属性类 + */ +export class RectPaint extends Paint { + startPoint: number[] = [0, 0] + linearGradientColors: Array + + constructor(paint?:RectPaint){ + super(paint) + if(paint != null && paint != undefined){ + this.startPoint = paint.startPoint; + } + } + + setStartPoint(value: number[]) { + this.x = value[0] + this.y = value[1] + this.startPoint = value + } + + getStartPoint(): number[]{ + return this.startPoint + } + + setGradientFillColor(linearGradientColors: Array): void{ + this.linearGradientColors = linearGradientColors; + } + +} + +export class BackGroundPaint extends RectPaint { + backgroundColor: number = 0xffffff + + constructor(paint?:BackGroundPaint){ + super(paint) + if(paint != null && paint != undefined){ + this.backgroundColor = paint.backgroundColor; + } + } + + setBackgroundColor(value: number) { + this.backgroundColor = value + } + + getBackgroundColor(): number{ + return this.backgroundColor + } +} +export class LinearGradientPaint extends RectPaint { + constructor(paint?:BackGroundPaint){ + super(paint) + } +} +export class IndexPositionPaint extends RectPaint { + dataSetIndex:number + dataIndex:number + constructor(paint?:IndexPositionPaint){ + super(paint) + } + setDataSetIndex(value: number) { + this.dataSetIndex = value + } + + getDataSetIndex(): number{ + return this.dataSetIndex + } + setDataIndex(value: number) { + this.dataIndex = value + } + + getDataIndex(): number{ + return this.dataIndex + } +} + +export class CirclePaint extends Paint { + + enabled: boolean = false; + circleColor: string | number | Color = Color.White; + colors:JArrayList = null; + radius: number = 2; + circleHoleRadius: number = 1; + circleHoleColor: string | number | Color = Color.White; + circleHoleEnabled: boolean = true; + + constructor(paint?:Paint){ + super(paint) + } + + setDrawCircles(enabled: boolean){ + this.enabled = enabled + } + + isDrawCirclesEnabled(): boolean{ + return this.enabled + } + + setCirclesColor(circleColor: string | number | Color){ + this.circleColor = circleColor; + } + + setCircleRadius(radius: number){ + this.radius = radius; + } + + setCircleHoleRadius(circleHoleRadius: number){ + this.circleHoleRadius = circleHoleRadius; + } + + setCircleHoleColor(circleHoleColor: string | number | Color){ + this.circleHoleColor = circleHoleColor; + } + + setDrawCircleHole(circleHoleEnabled: boolean){ + this.circleHoleEnabled = circleHoleEnabled; + } +} + + +export class ImagePaint extends Paint { + + icon: string|Resource = null; + constructor(paint?:ImagePaint){ + super(paint) + if(paint != null && paint != undefined){ + this.icon = paint.icon; + } + } + + setIcon(value: string|Resource){ + this.icon = value; + } + + getIcon(): string|Resource{ + return this.icon; + } + +} + +export enum Align { + LEFT, + CENTER, + RIGHT +} + +export class DashPathEffect{ + + dash: number[]; + offset: number + + constructor(dash:number[],offset:number){ + this.dash = dash; + this.offset = offset; + } +} + +export class FontMetrics { + /** + * The maximum distance above the baseline for the tallest glyph in + * the font at a given text size. + */ + public top : number; + /** + * The recommended distance above the baseline for singled spaced text. + */ + public ascent : number; + /** + * The recommended distance below the baseline for singled spaced text. + */ + public descent : number; + /** + * The maximum distance below the baseline for the lowest glyph in + * the font at a given text size. + */ + public bottom : number; + /** + * The recommended additional space to add between lines of text. + */ + public leading : number; +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarChartMode.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarChartMode.ets new file mode 100644 index 0000000..cad0ee8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarChartMode.ets @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ChartAnimator from '../animation/ChartAnimator'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import RadarChartRenderer from '../renderer/RadarChartRenderer'; +import XAxisRendererRadarChart from '../renderer/XAxisRendererRadarChart'; +import YAxisRendererRadarChart from '../renderer/YAxisRendererRadarChart'; +import { XAxis } from '../components/XAxis'; +import RadarData from './RadarData'; +import YAxis from '../components/YAxis' +import Paint, { IndexPositionPaint , CirclePaint, TextPaint} from './Paint'; +import MPPointF from '../utils/MPPointF'; +import MyRect from './Rect'; +import Utils from '../utils/Utils'; +import Highlight from '../highlight/Highlight'; + +export default class RadarChartMode{ + + public width: number; + public height: number; + public minOffset: number; + public paddingTop:number; + public paddingLeft:number=30; + public xExtraOffset=0 + public yExtraOffset=0 + public mRotateEnabled:boolean = true; + /** + * width of the main web lines + */ + public mWebLineWidth:number = 2.5; + + /** + * width of the inner web lines + */ + public mInnerWebLineWidth:number = 1.5; + + /** + * color for the main web lines + */ + public mWebColor:number = 0x64cccccc; + + /** + * color for the inner web + */ + public mWebColorInner:number =0x64cccccc; + + /** + * transparency the grid is drawn with (0-255) + */ + public mWebAlpha:number = 150; + + /** + * flag indicating if the web lines should be drawn or not + */ + public mDrawWeb:boolean = true; + + /** + * modulus that determines how many labels and web-lines are skipped before the next is drawn + */ + public mSkipWebLineCount:number = 0; + + public yAxis:YAxis; + public xAxis:XAxis; + public xScale:number; + public yScale:number; + + data:RadarData= new RadarData(); + displayCenterY: number = 0; + mRotationAngle:number = 270; + mRawRotationAngle = 270; + mStartAngle:number=0 + yAxisRenderer:YAxisRendererRadarChart + xAxisRenderer:XAxisRendererRadarChart + radarRender:RadarChartRenderer + handler:ViewPortHandler + mAnimator:ChartAnimator + constructor(yAxis?:YAxis,data?:RadarData){ + if(yAxis){ + this.yAxis=yAxis; + } + if(data){ + this.data=data; + } + } + public getRotationAngle():number { + return this.mRotationAngle; + } + public setYAxis(mYAxis:YAxis) :RadarChartMode{ + this.yAxis=mYAxis + return this; + } + public getYAxis() :YAxis{ + return this.yAxis; + } + public setXAxis(xAxis:XAxis) :RadarChartMode{ + this.xAxis=xAxis + return this; + } + public setYExtraOffset(yExtraOffset:number){ + this.yExtraOffset=yExtraOffset + return this + } + public getXAxis() :XAxis{ + return this.xAxis; + } + public getWebLineWidth():number { + return this.mWebLineWidth; + } + public getWebLineWidthInner():number { + return this.mInnerWebLineWidth; + } + public getWebAlpha():number { + return this.mWebAlpha; + } + public getWebColor():number { + return this.mWebColor; + } + public getWebColorInner():number { + return this.mWebColorInner; + } + public getSkipWebLineCount():number { + return this.mSkipWebLineCount; + } + public getYChartMax():number { + return this.yAxis.mAxisMaximum; + } + + /** + * Returns the minimum value this chart can display on it's y-axis. + */ + public getYChartMin():number { + return this.yAxis.mAxisMinimum; + } + + /** + * Returns the range of y-values this chart can display. + * + * @return + */ + public getYRange():number{ + return this.yAxis.mAxisRange; + } + public getSliceAngle():number{ + return 360/ this.data.getMaxEntryCountSet().getEntryCount(); + } + public setWidth(width:number):RadarChartMode{ + this.width=width + return this + } + public setHeight(height:number):RadarChartMode{ + this.height=height + return this + } + public setMinOffset(minOffset:number):RadarChartMode{ + this.minOffset=minOffset + return this + } + public setPaddingTop(paddingTop:number):RadarChartMode{ + this.paddingTop=paddingTop + return this + } + public setPaddingLeft(paddingLeft:number):RadarChartMode{ + this.paddingLeft=paddingLeft + return this + } + public getWidth():number{ + return this.width + } + public getHeight():number{ + return this.height + } + public getMinOffset():number{ + return this.minOffset + } + public getPaddingTop():number{ + return this.paddingTop + } + public getPaddingLeft():number{ + return this.paddingLeft + } + + public setData(data:RadarData):RadarChartMode{ + this.data=data + return this + } + public getData():RadarData{ + return this.data + } + public setXScale(xScale:number):RadarChartMode{ + this.xScale=xScale + return this + } + public getXScale():number{ + return this.xScale + } + public setYScale(yScale:number):RadarChartMode{ + this.yScale=yScale + return this + } + public getYScale():number{ + return this.yScale + } + public setDisplayCenterY(displayCenterY:number):RadarChartMode{ + this.displayCenterY=displayCenterY + return this + } + public getDisplayCenterY():number{ + return this.displayCenterY + } + public getRawRotationAngle():number { + return this.mRawRotationAngle; + } + public setRotationAngle(angle:number) { + this.mRawRotationAngle = angle; + this.mRotationAngle = Utils.getNormalizedAngle(this.mRawRotationAngle); + } + public calcScale(){ + let rect = this.data.mDisplayRect; + this.displayCenterY = (rect.bottom - rect.top)/2; + let minX=this.xAxis.getAxisMinimum()>0?0:this.xAxis.getAxisMinimum() + let miny=this.yAxis.getAxisMinimum()>0?0:this.xAxis.getAxisMinimum() + + this.xScale = (rect.right - rect.left)/(this.xAxis.getAxisMaximum()-minX); + this.yScale = (rect.bottom - rect.top)/(this.yAxis.getAxisMaximum()-miny); + } + public init(){ + this.calcScale(); + this.handler = new ViewPortHandler(); + this.handler.restrainViewPort(this.minOffset, this.minOffset, this.minOffset, this.minOffset) + this.handler.setChartDimens(this.width, this.height); + + this.xAxisRenderer=new XAxisRendererRadarChart(this); + this.yAxisRenderer=new YAxisRendererRadarChart(this); + this.yAxisRenderer.computeAxis(this.yAxis.mAxisMinimum, this.yAxis.mAxisMaximum, this.yAxis.isInverted()) + + this.mAnimator=new ChartAnimator(); + this.radarRender=new RadarChartRenderer(this); + + } + public getFactor():number { + let content:MyRect = this.handler.getContentRect(); + return Math.min(content.width() / 2, content.height() / 2) / this.getYRange(); + } + + public getCenterOffsets():MPPointF{ + return this.handler.getContentCenter(); + } + public getAngleForPoint( x:number, y:number):number { + + let c:MPPointF = this.getCenterOffsets(); + let tx = x - c.x, ty = y - c.y; + let length = Math.sqrt(tx * tx + ty * ty); + let r = Math.acos(ty / length); + let angle:number = 180*r/Math.PI; + if (x > c.x){ + angle = 360 - angle; + } + angle = angle + 90; + + if (angle > 360) + angle = angle - 360; + MPPointF.recycleInstance(c); + return angle; + } + paints:Paint[]=[] + highLight:Paint[]=[] + indexHighLightPaint:IndexPositionPaint=null; + public drawChart():Paint[]{ + let paintsTemp:Paint[]=[]; + let webPaint:Paint[]=this.radarRender.drawExtras(); + paintsTemp=paintsTemp.concat(webPaint) + + let dataPaint:Paint[]=this.radarRender.drawData(); + paintsTemp=paintsTemp.concat(dataPaint) + + let xDataPaint:Paint[]=this.xAxisRenderer.renderAxisLabels(); + paintsTemp=paintsTemp.concat(xDataPaint) + + let valuePaint:Paint[]=this.radarRender.drawValues(); + paintsTemp=paintsTemp.concat(valuePaint) + this.paints=[] + this.paints=this.paints.concat(paintsTemp) + return this.paints + } + public drawHighLight(){ + if(this.indexHighLightPaint==null||!this.data.isHighlightEnabled()){ + this.indexHighLightPaint=null;//便于旋转的时候判断是否需要绘制点击效果 + this.highLight=[] + this.highLight.push(new CirclePaint()) + this.highLight.push(new TextPaint()) + return + } + let hightL:Highlight=new Highlight(this.indexHighLightPaint.x,this.indexHighLightPaint.y, + this.indexHighLightPaint.dataSetIndex,this.indexHighLightPaint.dataIndex, + -1,vp2px(this.indexHighLightPaint.x),vp2px(this.indexHighLightPaint.y) + ); + this.highLight=this.radarRender.drawHighlighted([hightL]); + } + public drawClick(event: ClickEvent){ + let x = event.screenX-this.paddingLeft-this.xExtraOffset; + let y = event.screenY-this.paddingTop-this.yExtraOffset; + let factor:number=this.getFactor(); + let r:number= this.getYRange() * factor + let center:MPPointF=this.getCenterOffsets(); + let clickToCenterSpace=Math.sqrt((x-center.x)*(x-center.x)+(y-center.y)*(y-center.y)); + if(clickToCenterSpace>r){ + this.indexHighLightPaint=null;//便于旋转的时候判断是否需要绘制点击效果 + this.drawHighLight(); + return + } + if(this.radarRender==null||this.radarRender==undefined){ + return + } + let dataP:Paint[]=this.radarRender.drawDataByType(this.radarRender.TYPE_POINT); + if(dataP==null||dataP==undefined||dataP.length==0){ + return + } + + let minPoint:Paint + let minSpaceResult:number; + for(let hightLight of dataP){ + let xSpace=Math.abs(x-hightLight.x) + let ySpace=Math.abs(y-hightLight.y) + let minSpace=Math.sqrt(xSpace*xSpace+ySpace*ySpace) + if(minPoint==null||minPoint==undefined||minSpace { + + private mLabels : JArrayList; + + constructor(dataSets ?: JArrayList) { + super(dataSets); + } + + /** + * Sets the labels that should be drawn around the RadarChart at the end of each web line. + * + * @param labels + */ + public setLabels(labels : JArrayList) : void { + this.mLabels = labels; + } + + public getLabels() : JArrayList { + return this.mLabels; + } + + // @Override + public getEntryForHighlight(highlight : Highlight) : EntryOhos { + return super.getDataSetByIndex(highlight.getDataSetIndex()).getEntryForIndex(highlight.getX()); + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarDataSet.ets new file mode 100644 index 0000000..60bec04 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarDataSet.ets @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import IRadarDataSet from '../interfaces/datasets/IRadarDataSet'; +import ColorTemplate from '../utils/ColorTemplate'; +import {JArrayList} from '../utils/JArrayList'; +import RadarEntry from './RadarEntry'; +import LineRadarDataSet from './LineRadarDataSet'; +import {DataSet} from './DataSet'; + +export default class RadarDataSet extends LineRadarDataSet implements IRadarDataSet { + + // flag indicating whether highlight circle should be drawn or not + protected mDrawHighlightCircleEnabled : boolean = false; + + protected mHighlightCircleFillColor : number = Color.White; + + // The stroke color for highlight circle. + // If Utils.COLOR_NONE, the color of the dataset is taken. + protected mHighlightCircleStrokeColor : number = ColorTemplate.COLOR_NONE; + + protected mHighlightCircleStrokeAlpha : number = (0.3 * 255); + protected mHighlightCircleInnerRadius : number = 3.0; + protected mHighlightCircleOuterRadius : number = 4.0; + protected mHighlightCircleStrokeWidth : number = 2.0; + + constructor(yVals : JArrayList, label : string ) { + super(yVals, label); + } + + // Returns true if highlight circle should be drawn, false if not + // @Override + public isDrawHighlightCircleEnabled() : boolean { + return this.mDrawHighlightCircleEnabled; + } + + // Sets whether highlight circle should be drawn or not + // @Override + public setDrawHighlightCircleEnabled(enabled : boolean) : void { + this.mDrawHighlightCircleEnabled = enabled; + } + + // @Override + public getHighlightCircleFillColor() : number { + return this.mHighlightCircleFillColor; + } + + public setHighlightCircleFillColor(color : number) : void { + this.mHighlightCircleFillColor = color; + } + + // Returns the stroke color for highlight circle. + // If Utils.COLOR_NONE, the color of the dataset is taken. + // @Override + public getHighlightCircleStrokeColor() : number { + return this.mHighlightCircleStrokeColor; + } + + // Sets the stroke color for highlight circle. + // Set to Utils.COLOR_NONE in order to use the color of the dataset; + public setHighlightCircleStrokeColor(color) : void { + this.mHighlightCircleStrokeColor = color; + } + + // @Override + public getHighlightCircleStrokeAlpha() : number { + return this.mHighlightCircleStrokeAlpha; + } + + public setHighlightCircleStrokeAlpha(alpha : number) : void { + this.mHighlightCircleStrokeAlpha = alpha; + } + + // @Override + public getHighlightCircleInnerRadius() : number { + return this.mHighlightCircleInnerRadius; + } + + public setHighlightCircleInnerRadius(radius : number) : void { + this.mHighlightCircleInnerRadius = radius; + } + + // @Override + public getHighlightCircleOuterRadius() : number{ + return this.mHighlightCircleOuterRadius; + } + + public setHighlightCircleOuterRadius(radius : number) : void { + this.mHighlightCircleOuterRadius = radius; + } + + // @Override + public getHighlightCircleStrokeWidth() : number { + return this.mHighlightCircleStrokeWidth; + } + + public setHighlightCircleStrokeWidth(strokeWidth : number) : void { + this.mHighlightCircleStrokeWidth = strokeWidth; + } + + public copy(): DataSet { + let entries = new JArrayList(); + for (let i = 0; i < this.mEntries.size(); i++) { + entries.add(this.mEntries.get(i).copy()); + } + let copied = new RadarDataSet(entries, this.getLabel()); + this.copyTo(copied); + return copied; + } + + protected copyTo(radarDataSet: RadarDataSet): void { + super.copyTo(radarDataSet); + radarDataSet.mDrawHighlightCircleEnabled = this.mDrawHighlightCircleEnabled; + radarDataSet.mHighlightCircleFillColor = this.mHighlightCircleFillColor; + radarDataSet.mHighlightCircleInnerRadius = this.mHighlightCircleInnerRadius; + radarDataSet.mHighlightCircleStrokeAlpha = this.mHighlightCircleStrokeAlpha; + radarDataSet.mHighlightCircleStrokeColor = this.mHighlightCircleStrokeColor; + radarDataSet.mHighlightCircleStrokeWidth = this.mHighlightCircleStrokeWidth; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarEntry.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarEntry.ets new file mode 100644 index 0000000..808107c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/RadarEntry.ets @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from './EntryOhos'; + +export default class RadarEntry extends EntryOhos { + constructor(value : number, data ?: Object) { + super(0, value,null,data); + } + + /** + * This is the same as getY(). Returns the value of the RadarEntry. + * + * @return + */ + public getValue() : number { + return super.getY(); + } + + public copy() : RadarEntry { + var e = new RadarEntry(super.getY(), super.getData()); + return e; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Rect.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Rect.ets new file mode 100644 index 0000000..c2b187a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Rect.ets @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-nocheck +/** + * 移植的Rect类 + * @param left + * @param top + * @param right + * @param bottom + * @param r + */ +export default class MyRect { + left: number; + top: number; + right: number; + bottom: number; + + + /** + * Create a new rectangle with the specified coordinates. Note: no range + * checking is performed, so the caller must ensure that left <= right and + * top <= bottom. + * + * @param left The X coordinate of the left side of the rectangle + * @param top The Y coordinate of the top of the rectangle + * @param right The X coordinate of the right side of the rectangle + * @param bottom The Y coordinate of the bottom of the rectangle + */ + constructor(left?: number, top?: number, right?: number, bottom?: number, r?: MyRect) { + this.left = left == undefined ? 0 : left + this.top = top == undefined ? 0 : top; + this.right = right == undefined ? 0 : right; + this.bottom = bottom == undefined ? 0 : bottom; + if (r != null || r != undefined) { + this.left = r.left; + this.top = r.top; + this.right = r.right; + this.bottom = r.bottom; + } + } + + /** + * Returns a copy of {@code r} if {@code r} is not {@code null}, or {@code null} otherwise. + * + * @hide + */ + public static copyOrNull(r: MyRect): MyRect { + return r == null ? null : new MyRect(r.left, r.top, r.right, r.bottom); + } + + public equals(o: Object): boolean { + if (this == o) return true; + if (o == null || this != o) return false; + return this.left == o.left && this.top == o.top && this.right == o.right && this.bottom == o.bottom; + } + + public toString(): string { + var sb = ""; + sb += "Rect("; + sb += this.left; + sb += ", "; + sb += this.top; + sb += " - "; + sb += this.right; + sb += ", "; + sb += this.bottom; + sb += ")"; + return sb; + } + + + /** + * Returns true if the rectangle is empty (left >= right or top >= bottom) + */ + public isEmpty(): boolean { + return this.left >= this.right || this.top >= this.bottom; + } + + /** + * @return the rectangle's width. This does not check for a valid rectangle + * (i.e. left <= right) so the result may be negative. + */ + public width(): number { + return this.right - this.left; + } + + /** + * @return the rectangle's height. This does not check for a valid rectangle + * (i.e. top <= bottom) so the result may be negative. + */ + public height(): number { + return this.bottom - this.top; + } + + /** + * @return the horizontal center of the rectangle. If the computed value + * is fractional, this method returns the largest integer that is + * less than the computed value. + */ + public centerX(): number { + return (this.left + this.right) >> 1; + } + + /** + * @return the vertical center of the rectangle. If the computed value + * is fractional, this method returns the largest integer that is + * less than the computed value. + */ + public centerY(): number { + return (this.top + this.bottom) >> 1; + } + + /** + * @return the exact horizontal center of the rectangle as a float. + */ + public exactCenterX(): number { + return (this.left + this.right) * 0.5; + } + + /** + * @return the exact vertical center of the rectangle as a float. + */ + public exactCenterY(): number { + return (this.top + this.bottom) * 0.5; + } + + /** + * Set the rectangle to (0,0,0,0) + */ + public setEmpty() { + this.left = this.right = this.top = this.bottom = 0; + } + + /** + * Set the rectangle's coordinates to the specified values. Note: no range + * checking is performed, so it is up to the caller to ensure that + * left <= right and top <= bottom. + * + * @param left The X coordinate of the left side of the rectangle + * @param top The Y coordinate of the top of the rectangle + * @param right The X coordinate of the right side of the rectangle + * @param bottom The Y coordinate of the bottom of the rectangle + */ + public set(left: number, top: number, right: number, bottom: number) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + /** + * Offset the rectangle by adding dx to its left and right coordinates, and + * adding dy to its top and bottom coordinates. + * + * @param dx The amount to add to the rectangle's left and right coordinates + * @param dy The amount to add to the rectangle's top and bottom coordinates + */ + public offset(dx: number, dy: number) { + this.left += dx; + this.top += dy; + this.right += dx; + this.bottom += dy; + } + + /** + * Offset the rectangle to a specific (left, top) position, + * keeping its width and height the same. + * + * @param newLeft The new "left" coordinate for the rectangle + * @param newTop The new "top" coordinate for the rectangle + */ + public offsetTo(newLeft: number, newTop: number) { + this.right += newLeft - this.left; + this.bottom += newTop - this.top; + this.left = newLeft; + this.top = newTop; + } + + + /** + * Insets the rectangle on all sides specified by the insets. + * @hide + * @param left The amount to add from the rectangle's left + * @param top The amount to add from the rectangle's top + * @param right The amount to subtract from the rectangle's right + * @param bottom The amount to subtract from the rectangle's bottom + */ + public inset(left: number, top: number, right: number, bottom: number) { + this.left += left; + this.top += top; + this.right -= right; + this.bottom -= bottom; + } + + /** + * Returns true if (x,y) is inside the rectangle. The left and top are + * considered to be inside, while the right and bottom are not. This means + * that for a x,y to be contained: left <= x < right and top <= y < bottom. + * An empty rectangle never contains any point. + * + * @param x The X coordinate of the point being tested for containment + * @param y The Y coordinate of the point being tested for containment + * @return true iff (x,y) are contained by the rectangle, where containment + * means left <= x < right and top <= y < bottom + */ + public contains(x: number, y: number): boolean { + return this.left < this.right && this.top < this.bottom // check for empty first + && x >= this.left && x < this.right && y >= this.top && y < this.bottom; + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/cpp/gp_utils.h b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Runnable.ets similarity index 56% rename from device/device_ui/entry/src/main/cpp/gp_utils.h rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Runnable.ets index 6e44d46..27587e6 100644 --- a/device/device_ui/entry/src/main/cpp/gp_utils.h +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/Runnable.ets @@ -11,25 +11,25 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ -#ifndef GP_UTILS_H -#define GP_UTILS_H -#include -#include - -namespace gpUtils { - - void mSplit(const std::string &content, const std::string &sp, std::vector& out); - - bool canOpen(const std::string &path); - - bool canCmd(const std::string &cmd); - - std::string extractNumber(const std::string &str); - - std::string readFile(const std::string &path); - - std::string readCmd(const std::string &cmd); -}; - -#endif //GP_UTILS_H + */ + +import {Poolable} from '../utils/Poolable'; +export default class Runnable extends Poolable { + _type:string; + callback: {}; + + constructor(_type:string,callback:{}){ + super(); + this._type = _type; + this.callback = callback; + } + + protected setType(_type:string){ + this._type = _type; + } + + protected setCallback(callback: {}){ + this.callback = callback; + } + public instantiate(): Poolable{return null}; +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScaleMode.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScaleMode.ets new file mode 100644 index 0000000..afd452f --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScaleMode.ets @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XAixsMode from './XAixsMode'; +import { YAxisModel } from '../components/renderer/YAxisView'; + +export default class ScaleMode { + width: number = 300; //表的宽度 + height: number = 300; //表的高度 + scaleX: number= 1 //当前X累计放大倍数 + scaleY: number= 1 //当前Y累计放大倍数 + preTouchScaleX: number= 1 //上一次 手势、双击缩放后的缩放 + + moveX: number= 0 //累计平移X + moveY: number= 0 //累计平移Y + currentMoveX:number=0//当前次的位移 + currentMoveY:number=0//当前次的位移 + startMoveX: number= 0 + startMoveY: number= 0 + + currentXSpace:number=0//缩放之后 需要移回的距离 + currentYSpace:number=0//缩放之后 需要移回的距离 + + + isZoom: boolean= false + centerX: number= 0 //双指缩放、双击的中心点X + centerY: number= 0 //双指缩放、双击的中心点Y + firstTouch: TouchObject //第一个手指按下点 + secondTouch: TouchObject //第二个手指按下点 + firstClickTime: number= 0 //第一次点击的时间 用于判断是否为双击 + + onDoubleClickScale:number=0.1;//双击放大倍数 增加0.4倍 + leftAxisModel:YAxisModel = new YAxisModel(); + rightAxisModel:YAxisModel = new YAxisModel(); + + xAixsMode:XAixsMode=new XAixsMode() + isNeedScale:boolean=true + + public touchScale(xScale: number, yScale: number,event: TouchEvent) { + this.scaleX = (xScale * this.preTouchScaleX) + this.scaleY = (yScale * this.preTouchScaleX) + if (this.scaleX < 1) { + this.scaleX = 1 + this.centerX = 0 + } + if (this.scaleY < 1) { + this.scaleY = 1 + this.centerY = 0 + } + this.xAixsMode.width=this.width*this.scaleX + this.onScale(event) + this.leftAxisModel.scale(this.scaleY) + this.rightAxisModel.scale(this.scaleY) + } + + public doubleClickScale(event: ClickEvent,xScale: number, yScale: number) { + this.scaleX += xScale + this.scaleY += yScale + this.preTouchScaleX = this.scaleX + if (this.scaleX < 1) { + this.scaleX = 1 + this.centerX = 0 + } + if (this.scaleY < 1) { + this.scaleY = 1 + this.centerY = 0 + } + this.xAixsMode.width=this.width*this.scaleX + this.onDoubleClick(event) + this.leftAxisModel.scale(this.scaleY) + this.rightAxisModel.scale(this.scaleY) + } + + public move(moveX, moveY,event: TouchEvent) { + this.currentMoveX = moveX + this.currentMoveY = moveY + this.moveX += moveX + this.moveY += moveY + this.onMove(event) + } + public setXPosition(xPosition:number){ + this.xAixsMode.xPosition=xPosition + this.xAixsMode.draw() + } + /** + * 重置 + */ + public reset() { + this.scaleX = 1 + this.scaleY = 1 + this.moveX = 0 + this.moveY = 0 + this.centerX = 0 + this.centerY = 0 + this.preTouchScaleX = 1 + this.currentXSpace=0 + this.currentYSpace=0 + } + + /** + * view 调用 点击事件 此事件分发单击、双击事件 + * @param event + */ + public onClick(event: ClickEvent){ + let currnetTime=new Date().getTime(); + let spaceTime=currnetTime-this.firstClickTime + this.firstClickTime=currnetTime + + if(spaceTime<300){ + /** + * 双击放大事件 + */ + this.centerX=event.x; + this.centerY=event.y; + this.doubleClickScale(event,this.onDoubleClickScale,this.onDoubleClickScale) + }else{ + //单击 + this.onSingleClick(event) + } + + } + /** + * view 调用的触摸事件 此事件可分发 缩放、平移两个事件,缩放 监听为两指 + * @param event + */ + public onTouch(event: TouchEvent){ + /** + * 监听按下手指的数量,最大监听两个手指 + */ + if (event.type === TouchType.Down) { + this.firstTouch=event.touches[0] + if(event.touches.length>1){ + this.secondTouch=event.touches[1] + }else{ + this.startMoveX=0 + this.startMoveY=0 + } + } + if (event.type === TouchType.Up) { + //双指 突然变一指 后,界面将不在移动 + if(event.touches.length==1){ + if(this.isZoom==true){ + this.preTouchScaleX=this.scaleX + } + this.isZoom=false + } + } + if (event.type === TouchType.Move) { + + if(event.touches.length==1&&!this.isZoom){ + /** + * 平移 + */ + let spaceX=event.touches[0].x-this.firstTouch.x + let spaceY=event.touches[0].y-this.firstTouch.y + this.move(spaceX-this.startMoveX,spaceY-this.startMoveY,event) + this.startMoveX=spaceX + this.startMoveY=spaceY + }else{ + /** + * 缩放 + */ + this.isZoom=true + //第一次双指按下的距离 + let startSpaceX=Math.abs(this.firstTouch.x-this.secondTouch.x) + let startSpaceY=Math.abs(this.firstTouch.y-this.secondTouch.y) + //当前双指的距离 + let spaceX=Math.abs(event.touches[0].x-event.touches[1].x) + let spaceY=Math.abs(event.touches[0].y-event.touches[1].y) + //当前双指距离 相对 第一次双指 之间的距离 + let moveSpaceX=spaceX-startSpaceX + let moveSpaceY=spaceY-startSpaceY + + //缩放倍数 + let scalcX=Math.abs((this.width+moveSpaceX)/this.width) + let scalcY=Math.abs((this.height+moveSpaceY)/this.height) + + + //双指位移后的中心点 + this.centerX=Math.abs(event.touches[0].x+event.touches[1].x)/2; + this.centerY=Math.abs(event.touches[0].y+event.touches[1].y)/2; + + this.touchScale(scalcX,scalcY,event) + } + } + } + /** + * 需要覆写的 单击事件 + * @param event + */ + public onSingleClick(event: ClickEvent){ + /* scaleX: number= 1 //当前X累计放大倍数 + scaleY: number= 1 //当前Y累计放大倍数 + moveX: number= 0 //累计平移X + moveY: number= 0 //累计平移Y + centerX: number= 0 //双指缩放、双击的中心点X + centerY: number= 0 //双指缩放、双击的中心点Y*/ + //参数可在子类直接调用 + } + /** + * 需要覆写的 双击击事件 + * @param event + */ + public onDoubleClick(event: ClickEvent){ + + } + /** + * 需要覆写的平移事件 + * @param event + */ + public onMove(event: TouchEvent){ + + } + /** + * 需要覆写的平移事件 的缩放事件 + * @param event + */ + public onScale(event: TouchEvent){ + + } + + +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScatterDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScatterDataSet.ets new file mode 100644 index 0000000..108723a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/ScatterDataSet.ets @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//import { DataSet } from './DataSet'; +import LineScatterCandleRadarDataSet from './LineScatterCandleRadarDataSet' +import EntryOhos from './EntryOhos' +import IScatterDataSet from '../interfaces/datasets/IScatterDataSet' +import ColorTemplate from '../utils/ColorTemplate' +//import {ScatterShape} from '../charts/ScatterChart' + +import {JArrayList} from '../utils/JArrayList' +import IShapeRenderer from '../renderer/scatter/IShapeRenderer' + + + + + + + +export class ScatterDataSet extends LineScatterCandleRadarDataSet implements IScatterDataSet { + /** + * the size the scattershape will have, in density pixels + */ + private mShapeSize: number = 15; + + /** + * Renderer responsible for rendering this DataSet, default: square + */ + + + /** + * The radius of the hole in the shape (applies to Square, Circle and Triangle) + * - default: 0.0 + */ + private mScatterShapeHoleRadius: number = 0; + + /** + * Color for the hole in the shape. + * Setting to `ColorTemplate.COLOR_NONE` will behave as transparent. + * - default: ColorTemplate.COLOR_NONE + */ + private mScatterShapeHoleColor: number = ColorTemplate.COLOR_NONE; + + constructor(yVals: JArrayList, label: string) { + super(yVals, label); + } + + public copy(): ScatterDataSet /*DataSet*/ { + let entries = new JArrayList(); + for (let i = 0; i < this.mEntries.size(); i++) { + entries.add(this.mEntries.get(i).copy()); + } + let copied = new ScatterDataSet(entries, this.getLabel()); + this.copyTo(copied); + return copied; + } + + public copyTo(scatterDataSet: ScatterDataSet): void{ + super.copyTo(scatterDataSet); + scatterDataSet.mShapeSize = this.mShapeSize; + + scatterDataSet.mScatterShapeHoleRadius = this.mScatterShapeHoleRadius; + scatterDataSet.mScatterShapeHoleColor = this.mScatterShapeHoleColor; + } + + /** + * Sets the size in density pixels the drawn scattershape will have. This + * only applies for non custom shapes. + * + * @param size + */ + public setScatterShapeSize(size: number): void { + this.mShapeSize = size; + } + + public getScatterShapeSize(): number { + return this.mShapeSize; + } + + /** + * Sets the ScatterShape this DataSet should be drawn with. This will search for an available IShapeRenderer and set this + * renderer for the DataSet. + * + * @param shape + */ + + + /** + * Sets a new IShapeRenderer responsible for drawing this DataSet. + * This can also be used to set a custom IShapeRenderer aside from the default ones. + * + * @param shapeRenderer + */ + + + /** + * Sets the radius of the hole in the shape (applies to Square, Circle and Triangle) + * Set this to <= 0 to remove holes. + * + * @param holeRadius + */ + public setScatterShapeHoleRadius(holeRadius: number): void { + this.mScatterShapeHoleRadius = holeRadius; + } + + public getScatterShapeHoleRadius(): number { + return this.mScatterShapeHoleRadius; + } + + /** + * Sets the color for the hole in the shape. + * + * @param holeColor + */ + public setScatterShapeHoleColor(holeColor: number): void { + this.mScatterShapeHoleColor = holeColor; + } + + public getScatterShapeHoleColor(): number { + return this.mScatterShapeHoleColor; + } + + +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/XAixsMode.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/XAixsMode.ets new file mode 100644 index 0000000..e6b65b1 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/data/XAixsMode.ets @@ -0,0 +1,83 @@ +import Transformer from '../utils/Transformer'; +import XAxisRenderer from '../renderer/XAxisRenderer'; +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import LimitLine from '../components/LimitLine'; +import { XAxis } from '../components/XAxis'; +import Paint from './Paint'; +import ViewPortHandler from '../utils/ViewPortHandler'; + + +export default class XAixsMode{ + + paints:Paint[] = [] + handler:ViewPortHandler = new ViewPortHandler(); + topAxis:XAxis = new XAxis(); + bottomAxis:XAxis = new XAxis(); + width:number = 300; + height:number = 300; + minOffset:number = 15; + yLeftLongestLabel:string="AAA" + yRightLongestLabel:string="AAA" + XLimtLine:LimitLine = new LimitLine(35, "Index 10"); + + xPosition:number=0 + yPosition:number=0 + clipPath:string; + + public draw(){ + this.paints=[] + let minYOffset=this.topAxis.getTextSize()+this.topAxis.getYOffset(); + this.minOffset=this.minOffset -#include -#include -#include -#include "gp_utils.h" -#include "RAM.h" - -pthread_mutex_t RAM::mutex; -RAM *RAM::instance = nullptr; -RAM *RAM::getInstance() -{ - if (instance == nullptr) - { - pthread_mutex_lock(&mutex); - if (instance == nullptr) - { - instance = new RAM(); - } - pthread_mutex_unlock(&mutex); - } - return instance; -} -RAM::RAM() -{ - pthread_mutex_init(&mutex, nullptr); -} -std::map RAM::getRamInfo(std::string pid) +import LineData from '../data/LineData'; +import LineDataProvider from '../interfaces/dataprovider/LineDataProvider'; +import ILineDataSet from '../interfaces/datasets/ILineDataSet'; +import IFillFormatter from './IFillFormatter'; + +/** + * Default formatter that calculates the position of the filled line. + * + * @author Philipp Jahoda + */ +export default class DefaultFillFormatter implements IFillFormatter { - std::map ramInfo; - std::string pss_value = ""; - ramInfo["pss"] = "-1"; - if (pid.size() > 0) { - std::ostringstream cmd_grep; - cmd_grep << "hidumper --mem "; - cmd_grep << pid; - std::string cmd = cmd_grep.str(); - std::string pidLine = gpUtils::readCmd(cmd); - std::vector sps; - gpUtils::mSplit(pidLine, " ", sps); - if (sps.size() > 0) { - pss_value = sps[1]; + + public getFillLinePosition( dataSet:ILineDataSet, dataProvider:LineDataProvider):number { + + var fillMin:number = 0; + var chartMaxY:number = dataProvider.getYChartMax(); + var chartMinY:number= dataProvider.getYChartMin(); + + var data:LineData= dataProvider.getLineData(); + + if (dataSet.getYMax() > 0 && dataSet.getYMin() < 0) { + fillMin = 0; + } else { + + var max:number; + var min:number; + + if (data.getYMax() > 0) + max = 0; + else + max = chartMaxY; + if (data.getYMin() < 0) + min = 0; + else + min = chartMinY; + + fillMin = dataSet.getYMin() >= 0 ? min : max; } + + return fillMin; } - if (atoi(pss_value.c_str()) > 0) { - ramInfo["pss"] = gpUtils::extractNumber(pss_value.c_str()); - } - return ramInfo; -} +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/DefaultValueFormatter.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/DefaultValueFormatter.ets new file mode 100644 index 0000000..838c415 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/DefaultValueFormatter.ets @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import IValueFormatter from './IValueFormatter'; +import EntryOhos from '../data/EntryOhos'; +import ViewPortHandler from '../utils/ViewPortHandler'; + +/** + * Default formatter used for formatting values inside the chart. Uses a DecimalFormat with + * pre-calculated number of digits (depending on max and min value). + * + * @author Philipp Jahoda + */ +export default class DefaultValueFormatter implements IValueFormatter +{ + + /** + * DecimalFormat for formatting + */ + //protected mFormat:DecimalFormat; + + protected mDecimalDigits:number; + + /** + * Constructor that specifies to how many digits the value should be + * formatted. + * + * @param digits + */ + constructor(digits:number){ + this.setup(digits); + } + /** + * Sets up the formatter with a given number of decimal digits. + * + * @param digits + */ + public setup(digits:number):void { + + this.mDecimalDigits = digits; + + var b:string = ""; + for (var i = 0; i < digits; i++) { + if (i == 0) + b+="."; + b+="0"; + } + + //mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + public getFormattedValue( value:number, entry:EntryOhos, dataSetIndex:number, viewPortHandler:ViewPortHandler):string { + + // put more logic here ... + // avoid memory allocations here (for performance reasons) + + return value.toFixed(1)+"" ; + } + + /** + * Returns the number of decimal digits this formatter uses. + * + * @return + */ + public getDecimalDigits():number { + return this.mDecimalDigits; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IAxisValueFormatter.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IAxisValueFormatter.ets new file mode 100644 index 0000000..d569f3c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IAxisValueFormatter.ets @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AxisBase from '../components/AxisBase' +/** + * Custom formatter interface that allows formatting of + * axis labels before they are being drawn. + */ +export default interface IAxisValueFormatter +{ + + /** + * Called when a value from an axis is to be formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param axis the axis the value belongs to + * @return + */ + getFormattedValue(value:number,axis:AxisBase):string; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IFillFormatter.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IFillFormatter.ets new file mode 100644 index 0000000..7afcfe8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IFillFormatter.ets @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import LineDataProvider from '../interfaces/dataprovider/LineDataProvider'; +import ILineDataSet from '../interfaces/datasets/ILineDataSet'; +/** + * Interface for providing a custom logic to where the filling line of a LineDataSet + * should end. This of course only works if setFillEnabled(...) is set to true. + * + * @author Philipp Jahoda + */ +export default interface IFillFormatter +{ + + /** + * Returns the vertical (y-axis) position where the filled-line of the + * LineDataSet should end. + * + * @param dataSet the ILineDataSet that is currently drawn + * @param dataProvider + * @return + */ + getFillLinePosition( dataSet:ILineDataSet, dataProvider:LineDataProvider):number; +} diff --git a/device/device_ui/entry/src/main/ets/common/profiler/NativeTaskFun.ts b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IValueFormatter.ets similarity index 33% rename from device/device_ui/entry/src/main/ets/common/profiler/NativeTaskFun.ts rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IValueFormatter.ets index 6ee272f..63febd9 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/NativeTaskFun.ts +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/formatter/IValueFormatter.ets @@ -1,4 +1,3 @@ - /* * Copyright (C) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,37 +12,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getPidOfAbility } from '../utils/SystemUtils'; -import SPLogger from '../utils/SPLogger' -import nativeProfiler from "libsmartperf.so" - - -const TAG = "NativeTaskFun" -export class NativeTaskFun { - static initAllFun() { - globalThis.CreateNativeFps = ((pkgName: string) => { - let fpsStr: string = nativeProfiler.getFpsData(pkgName) - SPLogger.DEBUG(TAG, "nativeProfiler" + "--> fpsStr:" + fpsStr) - return fpsStr - }) - globalThis.CreateNativeRam = (() => { - let ramStr: string = nativeProfiler.getRamData(globalThis.processPid) - SPLogger.DEBUG(TAG, "nativeProfiler" + "--> ramStr :" + ramStr) - globalThis.ramArr.push(ramStr) - return ramStr - }) - - globalThis.CheckDaemon = (() => { - let status: string = nativeProfiler.checkDaemon() - SPLogger.DEBUG(TAG, "nativeProfiler" + "--> daemon status :" + status) - return status - }) +import EntryOhos from '../data/EntryOhos'; +import ViewPortHandler from '../utils/ViewPortHandler'; +/** + * Interface that allows custom formatting of all values inside the chart before they are + * being drawn to the screen. Simply create your own formatting class and let + * it implement IValueFormatter. Then override the getFormattedValue(...) method + * and return whatever you want. + * + * @author Philipp Jahoda + */ +export default interface IValueFormatter +{ - globalThis.checkAccess = ((path: string) => { - let status: string = nativeProfiler.checkAccess(path) - SPLogger.DEBUG(TAG, "nativeProfiler --> "+ path + " status :" + status) - return status - }) - } + /** + * Called when a value (from labels inside the chart) is formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry + * @param dataSetIndex the index of the DataSet the entry in focus belongs to + * @param viewPortHandler provides information about the current chart state (scale, translation, ...) + * @return the formatted label ready for being drawn + */ + getFormattedValue(value:number, entry:EntryOhos, dataSetIndex:number, viewPortHandler:ViewPortHandler):string; } diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/ChartHighlighter.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/ChartHighlighter.ets new file mode 100644 index 0000000..3064916 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/ChartHighlighter.ets @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import YAxis,{AxisDependency} from '../components/YAxis'; +import BarLineScatterCandleBubbleData from '../data/BarLineScatterCandleBubbleData'; +import { Rounding} from '../data/DataSet'; +import EntryOhos from '../data/EntryOhos'; +import BarLineScatterCandleBubbleDataProvider from '../interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider'; +import IDataSet from '../interfaces/datasets/IDataSet'; +import MPPointD from '../utils/MPPointD'; +import {JArrayList} from '../utils/JArrayList'; +import Highlight from './Highlight'; +import IHighlighter from './IHighlighter'; +import IBarLineScatterCandleBubbleDataSet from '../interfaces/datasets/IBarLineScatterCandleBubbleDataSet'; + +export default class ChartHighlighter implements IHighlighter +{ + + /** + * instance of the data-provider + */ + protected mChart : T; + + /** + * buffer for storing previously highlighted values + */ + protected mHighlightBuffer : JArrayList = new JArrayList(); + + constructor(chart : T) { + this.mChart = chart; + } + +// @Override + public getHighlight(x : number, y : number) : Highlight { + + var pos : MPPointD = this.getValsForTouch(x, y); + var xVal : number = pos.x; + MPPointD.recycleInstance(pos); + + var high : Highlight = this.getHighlightForX(xVal, x, y); + return high; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the corresponding xPos for a given touch-position in pixels. + * + * @param x + * @param y + * @return + */ + protected getValsForTouch(x : number, y : number) : MPPointD { + + // take any transformer to determine the x-axis value + var pos : MPPointD = this.mChart.getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(x, y); + return pos; + } + + /** + * Returns the corresponding Highlight for a given xVal and x- and y-touch position in pixels. + * + * @param xVal + * @param x + * @param y + * @return + */ + protected getHighlightForX(xVal : number, x : number, y : number) : Highlight { + + var closestValues : JArrayList = this.getHighlightsAtXValue(xVal, x, y); + + if(closestValues.isEmpty()) { + return null; + } + + var leftAxisMinDist : number = this.getMinimumDistance(closestValues, y, AxisDependency.LEFT); + var rightAxisMinDist : number = this.getMinimumDistance(closestValues, y, AxisDependency.RIGHT); + + var axis : AxisDependency = leftAxisMinDist < rightAxisMinDist ? AxisDependency.LEFT : AxisDependency.RIGHT; + + var detail : Highlight = this.getClosestHighlightByPixel(closestValues, x, y, axis, this.mChart.getMaxHighlightDistance()); + + return detail; + } + + /** + * Returns the minimum distance from a touch value (in pixels) to the + * closest value (in pixels) that is displayed in the chart. + * + * @param closestValues + * @param pos + * @param axis + * @return + */ + protected getMinimumDistance(closestValues : JArrayList, pos : number, axis : AxisDependency) : number { + + var distance : number = Number.MAX_VALUE; + + for (var i : number = 0; i < closestValues.size(); i++) { + + var high : Highlight = closestValues.get(i); + + if (high.getAxis() == axis) { + + var tempDistance : number = Math.abs(this.getHighlightPos(high) - pos); + if (tempDistance < distance) { + distance = tempDistance; + } + } + } + + return distance; + } + + protected getHighlightPos(h : Highlight) : number { + return h.getYPx(); + } + + /** + * Returns a list of Highlight objects representing the entries closest to the given xVal. + * The returned list contains two objects per DataSet (closest rounding up, closest rounding down). + * + * @param xVal the transformed x-value of the x-touch position + * @param x touch position + * @param y touch position + * @return + */ + protected getHighlightsAtXValue(xVal : number, x : number, y : number) : JArrayList { + + this.mHighlightBuffer.clear(); + + var data : BarLineScatterCandleBubbleData> = this.getData(); + + if (data == null) + return this.mHighlightBuffer; + + for (var i : number = 0, dataSetCount = data.getDataSetCount(); i < dataSetCount; i++) { + + var dataSet : IDataSet = data.getDataSetByIndex(i); + + // don't include DataSets that cannot be highlighted + if (!dataSet.isHighlightEnabled()) + continue; + + this.mHighlightBuffer.addAll(this.buildHighlights(dataSet, i, xVal, Rounding.CLOSEST)); + } + + return this.mHighlightBuffer; + } + + /** + * An array of `Highlight` objects corresponding to the selected xValue and dataSetIndex. + * + * @param set + * @param dataSetIndex + * @param xVal + * @param rounding + * @return + */ + protected buildHighlights(set : IDataSet, dataSetIndex : number, xVal : number, rounding : Rounding) : JArrayList { + + var highlights : JArrayList = new JArrayList(); + + //noinspection unchecked + var entries : JArrayList = set.getEntriesForXValue(xVal); + if (entries.size() == 0) { + // Try to find closest x-value and take all entries for that x-value + var closest : EntryOhos = set.getEntryForXValue(xVal, Number.NaN, rounding); + if (closest != null) + { + //noinspection unchecked + entries = set.getEntriesForXValue(closest.getX()); + } + } + + if (entries.size() == 0) + return highlights; + + for (let e of entries.dataSouce) { + var pixels : MPPointD = this.mChart.getTransformer( + set.getAxisDependency()).getPixelForValues(e.getX(), e.getY()); + + highlights.add(new Highlight( + e.getX(), e.getY(), + pixels.x, pixels.y, + dataSetIndex, set.getAxisDependency())); + } + + return highlights; + } + + /** + * Returns the Highlight of the DataSet that contains the closest value on the + * y-axis. + * + * @param closestValues contains two Highlight objects per DataSet closest to the selected x-position (determined by + * rounding up an down) + * @param x + * @param y + * @param axis the closest axis + * @param minSelectionDistance + * @return + */ + public getClosestHighlightByPixel(closestValues : JArrayList, x : number, y : number, + axis : AxisDependency, minSelectionDistance : number) : Highlight { + + var closest : Highlight = null; + var distance : number = minSelectionDistance; + + for (var i : number = 0; i < closestValues.size(); i++) { + + var high : Highlight = closestValues.get(i); + + if (axis == null || high.getAxis() == axis) { + + var cDistance : number = this.getDistance(x, y, high.getXPx(), high.getYPx()); + + if (cDistance < distance) { + closest = high; + distance = cDistance; + } + } + } + + return closest; + } + + /** + * Calculates the distance between the two given points. + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + * @return + */ + protected getDistance(x1 : number, y1 : number, x2 : number, y2 : number) : number { + //return Math.abs(y1 - y2); + //return Math.abs(x1 - x2); + return Math.hypot(x1 - x2, y1 - y2); + } + + protected getData() : BarLineScatterCandleBubbleData> { + return this.mChart.getData(); + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Highlight.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Highlight.ets new file mode 100644 index 0000000..7b8a659 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Highlight.ets @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AxisDependency} from '../components/YAxis' + +/** + * Contains information needed to determine the highlighted value. + */ +export default class Highlight { + + /** + * the x-value of the highlighted value + */ + private mX:number = Number.NaN; + + /** + * the y-value of the highlighted value + */ + private mY:number = Number.NaN; + + /** + * the x-pixel of the highlight + */ + private mXPx:number; + + /** + * the y-pixel of the highlight + */ + private mYPx:number; + + /** + * the index of the data object - in case it refers to more than one + */ + private mDataIndex:number = -1; + + /** + * the index of the dataset the highlighted value is in + */ + private mDataSetIndex:number; + + /** + * index which value of a stacked bar entry is highlighted, default -1 + */ + private mStackIndex:number = -1; + + /** + * the axis the highlighted value belongs to + */ + private axis: AxisDependency; + + /** + * the x-position (pixels) on which this highlight object was last drawn + */ + private mDrawX:number; + + /** + * the y-position (pixels) on which this highlight object was last drawn + */ + private mDrawY:number; + + constructor(x:number,y?:number,dataSetIndex?:number,dataIndex?:number,stackIndex?:number,xPx?:number,yPx?:number,axis?:AxisDependency){ + this.mX = x; + if(y==null||y==undefined){ + this.mY = Number.NaN + }else{ + this.mY = y; + } + this.mDataSetIndex = dataSetIndex; + this.mStackIndex = stackIndex; + if (dataIndex==null||dataIndex==undefined){ + this.mDataIndex = -1; + } else { + this.mDataIndex = dataIndex; + } + this.mXPx = xPx; + this.mYPx = yPx; + this.axis = axis; + } + + + /** + * returns the x-value of the highlighted value + * + * @return + */ + public getX():number { + return this.mX; + } + + /** + * returns the y-value of the highlighted value + * + * @return + */ + public getY():number { + return this.mY; + } + + /** + * returns the x-position of the highlight in pixels + */ + public getXPx():number { + return this.mXPx; + } + + /** + * returns the y-position of the highlight in pixels + */ + public getYPx():number { + return this.mYPx; + } + + /** + * the index of the data object - in case it refers to more than one + * + * @return + */ + public getDataIndex():number { + return this.mDataIndex; + } + + public setDataIndex(mDataIndex:number):void { + this.mDataIndex = mDataIndex; + } + + /** + * returns the index of the DataSet the highlighted value is in + * + * @return + */ + public getDataSetIndex():number { + return this.mDataSetIndex; + } + + /** + * Only needed if a stacked-barchart entry was highlighted. References the + * selected value within the stacked-entry. + * + * @return + */ + public getStackIndex():number { + return this.mStackIndex; + } + + public isStacked():boolean { + return this.mStackIndex >= 0; + } + + /** + * Returns the axis the highlighted value belongs to. + * + * @return + */ + public getAxis():AxisDependency { + return this.axis; + } + + /** + * Sets the x- and y-position (pixels) where this highlight was last drawn. + * + * @param x + * @param y + */ + public setDraw(x:number, y:number):void { + this.mDrawX = x; + this.mDrawY = y; + } + + /** + * Returns the x-position in pixels where this highlight object was last drawn. + * + * @return + */ + public getDrawX():number { + return this.mDrawX; + } + + /** + * Returns the y-position in pixels where this highlight object was last drawn. + * + * @return + */ + public getDrawY():number { + return this.mDrawY; + } + + /** + * Returns true if this highlight object is equal to the other (compares + * xIndex and dataSetIndex) + * + * @param h + * @return + */ + public equalTo(h:Highlight):boolean { + + if (h == null) + return false; + else { + if (this.mDataSetIndex == h.mDataSetIndex && this.mX == h.mX + && this.mStackIndex == h.mStackIndex && this.mDataIndex == h.mDataIndex) + return true; + else + return false; + } + } + + public toString():String { + return "Highlight, x: " + this.mX + ", y: " + this.mY + ", dataSetIndex: " + this.mDataSetIndex + + ", stackIndex (only stacked barentry): " + this.mStackIndex; + } +} diff --git a/device/device_ui/entry/src/main/cpp/RAM.h b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/IHighlighter.ets similarity index 67% rename from device/device_ui/entry/src/main/cpp/RAM.h rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/IHighlighter.ets index 997119f..7f0c559 100644 --- a/device/device_ui/entry/src/main/cpp/RAM.h +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/IHighlighter.ets @@ -12,20 +12,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef RAM_H -#define RAM_H -#include -#include -class RAM { -public: - static pthread_mutex_t mutex; - static RAM* getInstance(); - std::map getRamInfo(std::string pid); -private: - RAM(); - ~RAM(){}; - static RAM* instance; -}; +import Highlight from './Highlight' -#endif +export default interface IHighlighter +{ + + /** + * Returns a Highlight object corresponding to the given x- and y- touch positions in pixels. + * + * @param x + * @param y + * @return + */ + getHighlight(x:number,y:number):Highlight ; +} diff --git a/device/device_ui/entry/src/main/cpp/FPS.h b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Range.ets similarity index 43% rename from device/device_ui/entry/src/main/cpp/FPS.h rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Range.ets index d0e3fb0..5263ddb 100644 --- a/device/device_ui/entry/src/main/cpp/FPS.h +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/highlight/Range.ets @@ -11,44 +11,37 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ -#ifndef FPS_H -#define FPS_H + */ -#include -#include -#include -#include -#include "pthread.h" -#include "gp_utils.h" +export default class Range { -struct FpsInfo { - int fps; - int pre_fps; - std::vector jitters; - std::queue time_stamp_q; - long long last_frame_ready_time; - long long current_fps_time; - FpsInfo() - { - fps = 0; - pre_fps = 0; - last_frame_ready_time = 0; - current_fps_time = 0; - } -}; -class FPS{ -public: - static FPS* getInstance(); - FpsInfo getFpsInfo(); - FpsInfo GetSurfaceFrameDataGB(std::string name); - void setPackageName(std::string pkgName); - std::string pkg_name; - static pthread_mutex_t mutex; -private: - FPS(); - ~FPS(){}; - static FPS* instance; -}; + public myfrom:number; + public to:number; -#endif //FPS_H + constructor( myfrom:number, to:number) { + this.myfrom = myfrom; + this.to = to; + } + + /** + * Returns true if this range contains (if the value is in between) the given value, false if not. + * + * @param value + * @return + */ + public contains( value:number):boolean { + + if (value > this.myfrom && value <= this.to) + return true; + else + return false; + } + + public isLarger(value:number):boolean { + return value > this.to; + } + + public isSmaller(value:number):boolean { + return value < this.myfrom; + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.ets new file mode 100644 index 0000000..77210e2 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.ets @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ChartInterface from '../dataprovider/ChartInterface' +import BarLineScatterCandleBubbleData from '../../data/BarLineScatterCandleBubbleData' +import IBarLineScatterCandleBubbleDataSet from '../datasets/IBarLineScatterCandleBubbleDataSet' +import EntryOhos from '../../data/EntryOhos' +import Transformer from '../../utils/Transformer' +import {AxisDependency} from '../../components/YAxis' + +export default interface BarLineScatterCandleBubbleDataProvider extends ChartInterface { + + getTransformer(axis: AxisDependency): Transformer; + + isInverted(axis: AxisDependency): boolean; + + getLowestVisibleX(): number; + + getHighestVisibleX(): number; + + getData(): BarLineScatterCandleBubbleData>; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/ChartInterface.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/ChartInterface.ets new file mode 100644 index 0000000..10dfe3e --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/ChartInterface.ets @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MyRect from '../../data/Rect'; +import MPPointF from '../../utils/MPPointF' +import IValueFormatter from '../../formatter/IValueFormatter' +import ChartData from '../../data/ChartData' +import IDataSet from '../datasets/IDataSet' +import EntryOhos from '../../data/EntryOhos' + +/** + * Interface that provides everything there is to know about the dimensions, + * bounds, and range of the chart. + */ +export default interface ChartInterface { + + /** + * Returns the minimum x value of the chart, regardless of zoom or translation. + * + * @return + */ + getXChartMin(): number; + + /** + * Returns the maximum x value of the chart, regardless of zoom or translation. + * + * @return + */ + getXChartMax(): number; + + getXRange(): number; + + /** + * Returns the minimum y value of the chart, regardless of zoom or translation. + * + * @return + */ + getYChartMin(): number; + + /** + * Returns the maximum y value of the chart, regardless of zoom or translation. + * + * @return + */ + getYChartMax(): number; + + /** + * Returns the maximum distance in scren dp a touch can be away from an entry to cause it to get highlighted. + * + * @return + */ + getMaxHighlightDistance(): number; + + getWidth(): number; + + getHeight(): number; + + getCenterOfView(): MPPointF; + + getCenterOffsets(); + + getContentRect(): MyRect/*RectF*/; + + getDefaultValueFormatter(): IValueFormatter; + + getData(): ChartData>; + + getMaxVisibleCount(): number; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/LineDataProvider.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/LineDataProvider.ets new file mode 100644 index 0000000..7bad293 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/dataprovider/LineDataProvider.ets @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BarLineScatterCandleBubbleDataProvider from './BarLineScatterCandleBubbleDataProvider' +import YAxis,{AxisDependency} from '../../components/YAxis' +import LineData from '../../data/LineData'; + +export default interface LineDataProvider extends BarLineScatterCandleBubbleDataProvider { + + getLineData(): LineData; + + getAxis(dependency: AxisDependency): YAxis; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarDataSet.ets new file mode 100644 index 0000000..a7a9ce5 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarDataSet.ets @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import Fill from '../../utils/Fill'; +import {JArrayList} from '../../utils/JArrayList' +import IBarLineScatterCandleBubbleDataSet from './IBarLineScatterCandleBubbleDataSet' + +export default interface IBarDataSet extends IBarLineScatterCandleBubbleDataSet { + + getFills() : JArrayList; + + getFill(index : number) : Fill; + + /** + * Returns true if this DataSet is stacked (stacksize > 1) or not. + * + * @return + */ + isStacked() : boolean; + + /** + * Returns the maximum number of bars that can be stacked upon another in + * this DataSet. This should return 1 for non stacked bars, and > 1 for stacked bars. + * + * @return + */ + getStackSize() : number; + + /** + * Returns the color used for drawing the bar-shadows. The bar shadows is a + * surface behind the bar that indicates the maximum value. + * + * @return + */ + getBarShadowColor() : number; + + /** + * Returns the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + getBarBorderWidth() : number; + + /** + * Returns the color drawing borders around the bars. + * + * @return + */ + getBarBorderColor() : number; + + /** + * Returns the alpha value (transparency) that is used for drawing the + * highlight indicator. + * + * @return + */ + getHighLightAlpha() : number; + + + /** + * Returns the labels used for the different value-stacks in the legend. + * This is only relevant for stacked bar entries. + * + * @return + */ + getStackLabels() : string[]; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.ets new file mode 100644 index 0000000..edf00b8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.ets @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import IDataSet from './IDataSet'; + +export default interface IBarLineScatterCandleBubbleDataSet extends IDataSet { + + /** + * Returns the color that is used for drawing the highlight indicators. + * + * @return + */ + getHighLightColor() : number; +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBubbleDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBubbleDataSet.ets new file mode 100644 index 0000000..05426b8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IBubbleDataSet.ets @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import IBarLineScatterCandleBubbleDataSet from './IBarLineScatterCandleBubbleDataSet'; + +export default interface IBubbleDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Sets the width of the circle that surrounds the bubble when highlighted, + * in dp. + * + * @param width + */ + setHighlightCircleWidth(width : number) : void; + + getMaxSize() : number; + + isNormalizeSizeEnabled() : boolean; + + /** + * Returns the width of the highlight-circle that surrounds the bubble + * @return + */ + getHighlightCircleWidth() : number; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ICandleDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ICandleDataSet.ets new file mode 100644 index 0000000..02a145c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ICandleDataSet.ets @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import ILineScatterCandleRadarDataSet from './ILineScatterCandleRadarDataSet'; +import {Style} from '../../data/Paint' + +export default interface ICandleDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the space that is left out on the left and right side of each + * candle. + * + * @return + */ + getBarSpace() : number; + + /** + * Returns whether the candle bars should show? + * When false, only "ticks" will show + * + * - default: true + * + * @return + */ + getShowCandleBar() : boolean; + + /** + * Returns the width of the candle-shadow-line in pixels. + * + * @return + */ + getShadowWidth() : number; + + /** + * Returns shadow color for all entries + * + * @return + */ + getShadowColor() : number; + + /** + * Returns the neutral color (for open == close) + * + * @return + */ + getNeutralColor() : number; + + /** + * Returns the increasing color (for open < close). + * + * @return + */ + getIncreasingColor() : number; + + /** + * Returns the decreasing color (for open > close). + * + * @return + */ + getDecreasingColor() : number; + + /** + * Returns paint style when open < close + * + * @return + */ + getIncreasingPaintStyle() :Style; + + /** + * Returns paint style when open > close + * + * @return + */ + getDecreasingPaintStyle() : Style; + + /** + * Is the shadow color same as the candle color? + * + * @return + */ + getShadowColorSameAsCandle() : boolean; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IDataSet.ets new file mode 100644 index 0000000..cc427fb --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IDataSet.ets @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DashPathEffect } from '../../data/Paint'; +import {LegendForm} from '../../components/Legend' +import {AxisDependency} from '../../components/YAxis' +import {Rounding} from '../../data/DataSet' +import EntryOhos from '../../data/EntryOhos' +import IValueFormatter from '../../formatter/IValueFormatter' +import MPPointF from '../../utils/MPPointF' +import {JArrayList} from '../../utils/JArrayList' + +export default interface IDataSet { + + /** ###### ###### DATA RELATED METHODS ###### ###### */ + + /** + * returns the minimum y-value this DataSet holds + * + * @return + */ + getYMin() : number; + + /** + * returns the maximum y-value this DataSet holds + * + * @return + */ + getYMax() : number; + + /** + * returns the minimum x-value this DataSet holds + * + * @return + */ + getXMin() : number; + + /** + * returns the maximum x-value this DataSet holds + * + * @return + */ + getXMax() : number; + + /** + * Returns the number of y-values this DataSet represents -> the size of the y-values array + * -> yvals.size() + * + * @return + */ + getEntryCount() : number; + + /** + * Calculates the minimum and maximum x and y values (mXMin, mXMax, mYMin, mYMax). + */ + calcMinMax() : void; + + /** + * Calculates the min and max y-values from the Entry closest to the given fromX to the Entry closest to the given toX value. + * This is only needed for the autoScaleMinMax feature. + * + * @param fromX + * @param toX + */ + calcMinMaxY(fromX : number, toX : number) : void; + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + * + * + */ + getEntryForXValue(xValue : number, closestToY : number, rounding?: Rounding) : T; + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @return + */ + // getEntryForXValue(xValue : number, closestToY : number) : T; + + /** + * Returns all Entry objects found at the given x-value with binary + * search. An empty array if no Entry object at that x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue + * @return + */ + getEntriesForXValue(xValue : number):JArrayList ; + + /** + * Returns the Entry object found at the given index (NOT xIndex) in the values array. + * + * @param index + * @return + */ + getEntryForIndex(index : number) : T; + + /** + * Returns the first Entry index found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + */ + getEntryIndex(xValue : number, closestToY:number, rounding:Rounding) : number; + + /** + * Returns the position of the provided entry in the DataSets Entry array. + * Returns -1 if doesn't exist. + * + * @param e + * @return + */ + getEntryIndexByEntry(e : T) : number; + + + /** + * This method returns the actual + * index in the Entry array of the DataSet for a given xIndex. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param xIndex + * @return + */ + getIndexInEntries(xIndex : number) : number; + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to the end of the list. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + addEntry(e : T) : boolean; + + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to their appropriate index in the values array respective to their x-position. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + addEntryOrdered(e : T) : void; + + /** + * Removes the first Entry (at index 0) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + removeFirst() : boolean; + + /** + * Removes the last Entry (at index size-1) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + removeLast(); + + /** + * Removes an Entry from the DataSets entries array. This will also + * recalculate the current minimum and maximum values of the DataSet and the + * value-sum. Returns true if an Entry was removed, false if no Entry could + * be removed. + * + * @param e + */ + removeEntry(e : T) : boolean; + + /** + * Removes the Entry object closest to the given x-value from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param xValue + */ + removeEntryByXValue(xValue : number) : boolean; + + /** + * Removes the Entry object at the given index in the values array from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param index + * @return + */ + removeEntryByIndex(index : number) : boolean; + + /** + * Checks if this DataSet contains the specified Entry. Returns true if so, + * false if not. NOTE: Performance is pretty bad on this one, do not + * over-use in performance critical situations. + * + * @param entry + * @return + */ + contains(entry : T) : boolean; + + /** + * Removes all values from this DataSet and does all necessary recalculations. + */ + clear() : void; + + + /** ###### ###### STYLING RELATED (& OTHER) METHODS ###### ###### */ + + /** + * Returns the label string that describes the DataSet. + * + * @return + */ + getLabel():string; + + /** + * Sets the label string that describes the DataSet. + * + * @param label + */ + setLabel( label:string) : void; + + /** + * Returns the axis this DataSet should be plotted against. + * + * @return + */ + getAxisDependency(): AxisDependency; + + /** + * Set the y-axis this DataSet should be plotted against (either LEFT or + * RIGHT). Default: LEFT + * + * @param dependency + */ + setAxisDependency( dependency:AxisDependency) : void; + + /** + * returns all the colors that are set for this DataSet + * + * @return + */ + getColors():JArrayList; + + /** + * Returns the first color (index 0) of the colors-array this DataSet + * contains. This is only used for performance reasons when only one color is in the colors array (size == 1) + * + * @return + */ + getColor() : number; + + /** + * Returns the color at the given index of the DataSet's color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + getColor(index : number) : number; + + /** + * returns true if highlighting of values is enabled, false if not + * + * @return + */ + isHighlightEnabled() : boolean; + + /** + * If set to true, value highlighting is enabled which means that values can + * be highlighted programmatically or by touch gesture. + * + * @param enabled + */ + setHighlightEnabled(enabled : boolean) : void; + + /** + * Sets the formatter to be used for drawing the values inside the chart. If + * no formatter is set, the chart will automatically determine a reasonable + * formatting (concerning decimals) for all the values that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter + * calculated by the chart. + * + * @param f + */ + setValueFormatter(f:IValueFormatter) : void; + + /** + * Returns the formatter used for drawing the values inside the chart. + * + * @return + */ + getValueFormatter() : IValueFormatter; + + /** + * Returns true if the valueFormatter object of this DataSet is null. + * + * @return + */ + needsFormatter() : boolean; + + /** + * Sets the color the value-labels of this DataSet should have. + * + * @param color + */ + setValueTextColor(color : number) : void; + + /** + * Sets a list of colors to be used as the colors for the drawn values. + * + * @param colors + */ + setValueTextColors(colors:JArrayList ) : void; + + /** + * Sets a Typeface for the value-labels of this DataSet. + * + * @param tf + */ + setValueTypeface( tf:FontWeight) : void; + + /** + * Sets the text-size of the value-labels of this DataSet in dp. + * + * @param size + */ + setValueTextSize(size : number) : void; + + /** + * Returns only the first color of all colors that are set to be used for the values. + * + * @return + */ + getValueTextColor() : number; + + /** + * Returns the color at the specified index that is used for drawing the values inside the chart. + * Uses modulus internally. + * + * @param index + * @return + */ + getValueTextColor(index : number) : number; + + /** + * Returns the typeface that is used for drawing the values inside the chart + * + * @return + */ + getValueTypeface():FontWeight; + + /** + * Returns the text size that is used for drawing the values inside the chart + * + * @return + */ + getValueTextSize() : number; + + /** + * The form to draw for this dataset in the legend. + *

+ * Return `DEFAULT` to use the default legend form. + */ + getForm():LegendForm; + + /** + * The form size to draw for this dataset in the legend. + *

+ * Return `Float.NaN` to use the default legend form size. + */ + getFormSize() : number; + + /** + * The line width for drawing the form of this dataset in the legend + *

+ * Return `Float.NaN` to use the default legend form line width. + */ + getFormLineWidth() : number; + + /** + * The line dash path effect used for shapes that consist of lines. + *

+ * Return `null` to use the default legend form line dash effect. + */ + getFormLineDashEffect() : DashPathEffect/*DashPathEffect*/; + + /** + * set this to true to draw y-values on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no values will be drawn even + * if this is enabled + * @param enabled + */ + setDrawValues(enabled : boolean) : void; + + /** + * Returns true if y-value drawing is enabled, false if not + * + * @return + */ + isDrawValuesEnabled() : boolean; + + /** + * Set this to true to draw y-icons on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no icons will be drawn even + * if this is enabled + * + * @param enabled + */ + setDrawIcons(enabled : boolean) : void; + + /** + * Returns true if y-icon drawing is enabled, false if not + * + * @return + */ + isDrawIconsEnabled() : boolean; + + /** + * Offset of icons drawn on the chart. + * + * For all charts except Pie and Radar it will be ordinary (x offset,y offset). + * + * For Pie and Radar chart it will be (y offset, distance from center offset); so if you want icon to be rendered under value, you should increase X component of CGPoint, and if you want icon to be rendered closet to center, you should decrease height component of CGPoint. + * @param offset + */ + setIconsOffset( offset:MPPointF) : void; + + /** + * Get the offset for drawing icons. + */ + getIconsOffset():MPPointF; + + /** + * Set the visibility of this DataSet. If not visible, the DataSet will not + * be drawn to the chart upon refreshing it. + * + * @param visible + */ + setVisible(visible : boolean) : void; + + /** + * Returns true if this DataSet is visible inside the chart, or false if it + * is currently hidden. + * + * @return + */ + isVisible() : boolean; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineDataSet.ets new file mode 100644 index 0000000..37e3418 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineDataSet.ets @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import {Mode} from '../../data/LineDataSet'; +import IFillFormatter from '../../formatter/IFillFormatter'; +import ILineRadarDataSet from './ILineRadarDataSet'; +import {DashPathEffect} from '../../data/Paint' +import { FillStyle } from '../../data/LineDataSet' + +export default interface ILineDataSet extends ILineRadarDataSet { + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + getMode() : Mode; + + /** + * Returns the intensity of the cubic lines (the effect intensity). + * Max = 1f = very cubic, Min = 0.05f = low cubic effect, Default: 0.2f + * + * @return + */ + getCubicIntensity() : number; + + /** + * Returns the size of the drawn circles. + */ + getCircleRadius() : number; + + /** + * Returns the hole radius of the drawn circles. + */ + getCircleHoleRadius() : number; + + /** + * Returns the color at the given index of the DataSet's circle-color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + getCircleColorByIndex(index : number) : number; + + getCircleColor() : number; + + /** + * Returns the number of colors in this DataSet's circle-color array. + * + * @return + */ + getCircleColorCount() : number; + + /** + * Returns true if drawing circles for this DataSet is enabled, false if not + * + * @return + */ + isDrawCirclesEnabled() : boolean; + + /** + * Returns the color of the inner circle (the circle-hole). + * + * @return + */ + getCircleHoleColor() : number; + + /** + * Returns true if drawing the circle-holes is enabled, false if not. + * + * @return + */ + isDrawCircleHoleEnabled() : boolean; + + /** + * Returns the DashPathEffect that is used for drawing the lines. + * + * @return + */ + getDashPathEffect() : DashPathEffect; + + /** + * Returns true if the dashed-line effect is enabled, false if not. + * If the DashPathEffect object is null, also return false here. + * + * @return + */ + isDashedLineEnabled() : boolean; + + /** + * Returns the IFillFormatter that is set for this DataSet. + * + * @return + */ + getFillFormatter() : IFillFormatter; + + getFillStyle(): FillStyle; +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineRadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineRadarDataSet.ets new file mode 100644 index 0000000..753caa8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineRadarDataSet.ets @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import ILineScatterCandleRadarDataSet from './ILineScatterCandleRadarDataSet'; +import {ColorStop} from '../../data/LineDataSet'; + +export default interface ILineRadarDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the color that is used for filling the line surface area. + * + * @return + */ + getFillColor() : number; + + /** + * Returns the drawable used for filling the area below the line. + * + * @return + */ + getFillDrawable() :Object/* Drawable*/; + + /** + * Returns the alpha value that is used for filling the line surface, + * default: 85 + * + * @return + */ + getFillAlpha() : number; + + /** + * Returns the stroke-width of the drawn line + * + * @return + */ + getLineWidth() : number; + + /** + * Returns true if filled drawing is enabled, false if not + * + * @return + */ + isDrawFilledEnabled() : boolean; + + /** + * Set to true if the DataSet should be drawn filled (surface), and not just + * as a line, disabling this will give great performance boost. Please note that this method + * uses the canvas.clipPath(...) method for drawing the filled area. + * be turned off. Default: false + * + * @param enabled + */ + setDrawFilled(enabled : boolean) : void; + + getGradientFillColor(): Array; + + setGradientFillColor(linearGradientColors: Array): void; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineScatterCandleRadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineScatterCandleRadarDataSet.ets new file mode 100644 index 0000000..ccc7d75 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/ILineScatterCandleRadarDataSet.ets @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DashPathEffect } from '../../data/Paint'; +import EntryOhos from '../../data/EntryOhos'; +import IBarLineScatterCandleBubbleDataSet from './IBarLineScatterCandleBubbleDataSet'; + +export default interface ILineScatterCandleRadarDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + isVerticalHighlightIndicatorEnabled() : boolean; + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + isHorizontalHighlightIndicatorEnabled() : boolean; + + /** + * Returns the line-width in which highlight lines are to be drawn. + * @return + */ + getHighlightLineWidth() : number; + + /** + * Returns the DashPathEffect that is used for highlighting. + * @return + */ + getDashPathEffectHighlight() : DashPathEffect; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IRadarDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IRadarDataSet.ets new file mode 100644 index 0000000..1375202 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IRadarDataSet.ets @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import RadarEntry from '../../data/RadarEntry'; +import ILineRadarDataSet from './ILineRadarDataSet'; + +export default interface IRadarDataSet extends ILineRadarDataSet { + + // flag indicating whether highlight circle should be drawn or not + isDrawHighlightCircleEnabled() : boolean; + + // Sets whether highlight circle should be drawn or not + setDrawHighlightCircleEnabled(enabled : boolean) : void; + + getHighlightCircleFillColor() : number; + + // The stroke color for highlight circle. + // If Utils.COLOR_NONE, the color of the dataset is taken. + getHighlightCircleStrokeColor() : number; + + getHighlightCircleStrokeAlpha() : number; + + getHighlightCircleInnerRadius() : number; + + getHighlightCircleOuterRadius() : number; + + getHighlightCircleStrokeWidth() : number; + +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IScatterDataSet.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IScatterDataSet.ets new file mode 100644 index 0000000..3f0309c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/interfaces/datasets/IScatterDataSet.ets @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import EntryOhos from '../../data/EntryOhos'; +import ILineScatterCandleRadarDataSet from './ILineScatterCandleRadarDataSet'; +import IShapeRenderer from '../../renderer/scatter/IShapeRenderer'; + +export default interface IScatterDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the currently set scatter shape size + * + * @return + */ + getScatterShapeSize() : number; + + /** + * Returns radius of the hole in the shape + * + * @return + */ + getScatterShapeHoleRadius() : number; + + /** + * Returns the color for the hole in the shape + * + * @return + */ + getScatterShapeHoleColor() : number; + + /** + * Returns the IShapeRenderer responsible for rendering this DataSet. + * + * @return + */ + getShapeRenderer() : IShapeRenderer; +} diff --git a/device/device_ui/entry/src/main/ets/common/profiler/MainWorkTask.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/jobs/ViewPortJob.ets similarity index 31% rename from device/device_ui/entry/src/main/ets/common/profiler/MainWorkTask.ets rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/jobs/ViewPortJob.ets index 94ab554..c4513ff 100644 --- a/device/device_ui/entry/src/main/ets/common/profiler/MainWorkTask.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/jobs/ViewPortJob.ets @@ -12,39 +12,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import worker from '@ohos.worker'; // 导入worker模块 -import { CatchTraceStatus } from '../profiler/base/ProfilerConstant'; -import SPLogger from '../utils/SPLogger' -export let MainWorker = new worker.Worker("workers/worker.js") - -MainWorker.onmessage = function (e) { - SPLogger.DEBUG("MainWorker","MainWorker recv:" + e.data); - let arr = e.data.split("$") - if (arr[0] == "RAM ") { - globalThis.ramArr.push(arr[1]) - } - if (arr[0] == "FPS") { - globalThis.fpsArr.push(arr[1]) - globalThis.fpsJitterArr.push(arr[2]) - globalThis.timerFps=arr[1] - if (globalThis.catchTraceState == CatchTraceStatus.catch_trace_start - || globalThis.catchTraceState == CatchTraceStatus.catch_trace_finish - || globalThis.catchTraceState == CatchTraceStatus.catch_trace_first_running - ) { - if (globalThis.fpsJitterArr != undefined && globalThis.fpsJitterArr != null && globalThis.fpsJitterArr != "") { - let tempQueue: Array = globalThis.fpsJitterArr - let curJitter = tempQueue.pop() - let tempJitterArr = curJitter.split("==") - for (var i = 0; i < tempJitterArr.length; i++) { - let tmp = tempJitterArr[i] - let jitter = parseInt(tmp) / 1e6 - if (jitter > 100) { - globalThis.jitterTrace = true - return - } - } - } + +import Transformer from '../utils/Transformer'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import Runnable from '../data/Runnable'; +import Chart from '../charts/Chart' + +/** + * Runnable that is used for viewport modifications since they cannot be + * executed at any time. This can be used to delay the execution of viewport + * modifications until the onSizeChanged(...) method of the chart-view is called. + * This is especially important if viewport modifying methods are called on the chart + * directly after initialization. + * + * @author Philipp Jahoda + */ +export default abstract class ViewPortJob extends Runnable { + + protected pts:number[] = new Array(2); + + protected mViewPortHandler:ViewPortHandler; + protected xValue:number = 0; + protected yValue:number = 0; + protected mTrans:Transformer; + protected view:Chart; + + constructor(viewPortHandler:ViewPortHandler, xValue:number, yValue:number, + trans:Transformer, v:Chart) { + super(null,null); + this.mViewPortHandler = viewPortHandler; + this.xValue = xValue; + this.yValue = yValue; + this.mTrans = trans; + this.view = v; + + } + + public getXValue():number{ + return this.xValue; } - } -} + public getYValue():number { + return this.yValue; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/ChartTouchListener.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/ChartTouchListener.ets new file mode 100644 index 0000000..69fcb19 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/ChartTouchListener.ets @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Highlight from '../highlight/Highlight'; +import Chart from '../charts/Chart' +import OnChartGestureListener from './OnChartGestureListener' + +export enum ChartGesture { + NONE, DRAG, X_ZOOM, Y_ZOOM, PINCH_ZOOM, ROTATE, SINGLE_TAP, DOUBLE_TAP, LONG_PRESS, FLING +} + +/** + * Created by philipp on 12/06/15. + */ +// @ts-ignore +export default abstract class ChartTouchListener { + + +/** + * the last touch gesture that has been performed + **/ + protected mLastGesture: ChartGesture = ChartGesture.NONE; + +// states + protected static NONE: number = 0; + protected static DRAG: number = 1; + protected static X_ZOOM: number = 2; + protected static Y_ZOOM: number = 3; + protected static PINCH_ZOOM: number = 4; + protected static POST_ZOOM: number = 5; + protected static ROTATE: number = 6; + +/** + * integer field that holds the current touch-state + */ + protected mTouchMode: number = ChartTouchListener.NONE; + +/** + * the last highlighted object (via touch) + */ + protected mLastHighlighted: Highlight; + +/** + * the gesturedetector used for detecting taps and longpresses, ... + */ +// protected mGestureDetector:GestureDetector; + +/** + * the chart the listener represents + */ + protected mChart: T; + + constructor(chart: T) { + this.mChart = chart; + + // mGestureDetector = new GestureDetector(chart.getContext(), this); + } + +/** + * Calls the OnChartGestureListener to do the start callback + * + * @param me + */ + public startAction(me: TouchEvent) { + + var l: OnChartGestureListener = this.mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureStart(me, this.mLastGesture); + } + +/** + * Calls the OnChartGestureListener to do the end callback + * + * @param me + */ + public endAction(me: TouchEvent) { + + var l: OnChartGestureListener = this.mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureEnd(me, this.mLastGesture); + } + +/** + * Sets the last value that was highlighted via touch. + * + * @param high + */ + public setLastHighlighted(high: Highlight) { + this.mLastHighlighted = high; + } + +/** + * returns the touch mode the listener is currently in + * + * @return + */ + public getTouchMode(): number { + return this.mTouchMode; + } + +/** + * Returns the last gesture that has been performed on the chart. + * + * @return + */ + public getLastGesture(): ChartGesture { + return this.mLastGesture; + } + + +/** + * Perform a highlight operation. + * + * @param e + */ + protected performHighlight(h: Highlight, e: TouchEvent) { + + if (h == null || h == this.mLastHighlighted) { + this.mChart.highlightValue(null, true); + this.mLastHighlighted = null; + } else { + this.mChart.highlightValue(h, true); + this.mLastHighlighted = h; + } + } +/** + * returns the distance between two points + * + * @param eventX + * @param startX + * @param eventY + * @param startY + * @return + */ + protected static distance(eventX: number, startX: number, eventY: number, startY: number): number { + let dx: number = eventX - startX; + let dy: number = eventY - startY; + return Math.sqrt(dx * dx + dy * dy); + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartGestureListener.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartGestureListener.ets new file mode 100644 index 0000000..d90eec1 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartGestureListener.ets @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ChartTouchListener, {ChartGesture} from './ChartTouchListener' +/** + * Listener for callbacks when doing gestures on the chart. + * + * @author Philipp Jahoda + */ +export default interface OnChartGestureListener { + + /** + * Callbacks when a touch-gesture has started on the chart (ACTION_DOWN) + * + * @param me + * @param lastPerformedGesture + */ + onChartGestureStart(me: TouchEvent, lastPerformedGesture: ChartGesture); + + /** + * Callbacks when a touch-gesture has ended on the chart (ACTION_UP, ACTION_CANCEL) + * + * @param me + * @param lastPerformedGesture + */ + onChartGestureEnd(me: TouchEvent, lastPerformedGesture: ChartGesture); + + /** + * Callbacks when the chart is longpressed. + * + * @param me + */ + onChartLongPressed(me: TouchEvent); + + /** + * Callbacks when the chart is double-tapped. + * + * @param me + */ + onChartDoubleTapped(me: TouchEvent); + + /** + * Callbacks when the chart is single-tapped. + * + * @param me + */ + onChartSingleTapped(me: TouchEvent); + + /** + * Callbacks then a fling gesture is made on the chart. + * + * @param me1 + * @param me2 + * @param velocityX + * @param velocityY + */ + onChartFling(me1: TouchEvent, me2: TouchEvent, velocityX: number, velocityY: number); + + /** + * Callbacks when the chart is scaled / zoomed via pinch zoom / double-tap gesture. + * + * @param me + * @param scaleX scalefactor on the x-axis + * @param scaleY scalefactor on the y-axis + */ + onChartScale(me: TouchEvent, scaleX: number, scaleY: number); + + /** + * Callbacks when the chart is moved / translated via drag gesture. + * + * @param me + * @param dX translation distance on the x-axis + * @param dY translation distance on the y-axis + */ + onChartTranslate(me: TouchEvent, dX: number, dY: number); +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartValueSelectedListener.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartValueSelectedListener.ets new file mode 100644 index 0000000..b66546c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/listener/OnChartValueSelectedListener.ets @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Highlight from '../highlight/Highlight'; +import EntryOhos from '../data/EntryOhos'; +/** + * Listener for callbacks when selecting values inside the chart by + * touch-gesture. + * + * @author Philipp Jahoda + */ +export default interface OnChartValueSelectedListener { + + /** + * Called when a value has been selected inside the chart. + * + * @param e The selected Entry + * @param h The corresponding highlight object that contains information + * about the highlighted position such as dataSetIndex, ... + */ + onValueSelected(e: EntryOhos, h: Highlight); + + /** + * Called when nothing has been selected or an "un-select" has been made. + */ + onNothingSelected(); +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/AxisRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/AxisRenderer.ets new file mode 100644 index 0000000..409d46c --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/AxisRenderer.ets @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AxisBase from '../components/AxisBase' +import Renderer from '../renderer/Renderer' +import Paint, {Style, LinePaint, TextPaint,PathPaint} from '../data/Paint' +import Utils from '../utils/Utils' +import Transformer from '../utils/Transformer' +import ViewPortHandler from '../utils/ViewPortHandler' +import MPPointD from '../utils/MPPointD' + +/** + * Baseclass of all axis renderers. + * + */ +export default abstract class AxisRenderer extends Renderer { + +/** base axis this axis renderer works with */ + protected mAxis: AxisBase; + +/** transformer to transform values to screen pixels and return */ + protected mTrans: Transformer; + +/** + * paint object for the grid lines + */ + protected mGridPaint: Paint; + +/** + * paint for the x-label values + */ + protected mAxisLabelPaint: Paint; + +/** + * paint for the line surrounding the chart + */ + protected mAxisLinePaint: Paint; + +/** + * paint used for the limit lines + */ + protected mLimitLinePaint: Paint; + + constructor(viewPortHandler: ViewPortHandler, trans: Transformer, axis: AxisBase) { + super(viewPortHandler) + this.mTrans = trans; + this.mAxis = axis; + + if (this.mViewPortHandler != null) { + this.mAxisLabelPaint = new TextPaint(); + + this.mGridPaint = new PathPaint(); + this.mGridPaint.setColor(Color.Gray); + this.mGridPaint.setStrokeWidth(1); + this.mGridPaint.setStyle(Style.STROKE); + this.mGridPaint.setAlpha(90 / 255) + + this.mAxisLinePaint = new LinePaint(); + this.mAxisLinePaint.setColor(Color.Black) + this.mAxisLinePaint.setStrokeWidth(1) + this.mAxisLinePaint.setStyle(Style.STROKE) + + this.mLimitLinePaint = new PathPaint(); + this.mLimitLinePaint.setStyle(Style.STROKE) + } + } + +/** + * Returns the Paint object used for drawing the axis (labels). + * + * @return + */ + public getPaintAxisLabels(): Paint{ + return this.mAxisLabelPaint; + } + + +/** + * Returns the Paint object that is used for drawing the grid-lines of the + * axis. + * + * @return + */ + public getPaintGrid(): Paint { + return this.mGridPaint; + } + +/** + * Returns the Paint object that is used for drawing the axis-line that goes + * alongside the axis. + * + * @return + */ + public getPaintAxisLine(): Paint { + return this.mAxisLinePaint; + } + +/** + * Returns the Transformer object used for transforming the axis values. + * + * @return + */ + public getTransformer(): Transformer { + return this.mTrans; + } + + +/** + * Computes the axis values. + * + * @param min - the minimum value in the data object for this axis + * @param max - the maximum value in the data object for this axis + */ + public computeAxis(min: number, max: number, inverted: boolean) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (this.mViewPortHandler != null && this.mViewPortHandler.contentWidth() > 10 && !this.mViewPortHandler.isFullyZoomedOutY()) { + + var p1: MPPointD = this.mTrans.getValuesByTouchPoint(this.mViewPortHandler.contentLeft(), this.mViewPortHandler.contentTop()); + var p2: MPPointD = this.mTrans.getValuesByTouchPoint(this.mViewPortHandler.contentLeft(), this.mViewPortHandler.contentBottom()); + + if (!inverted) { + + min = p2.y; + max = p1.y; + } else { + + min = p1.y; + max = p2.y; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + this.computeAxisValues(min, max); + } + +/** + * Sets up the axis values. Computes the desired number of labels between the two given extremes. + * + * @return + */ + protected computeAxisValues(min: number, max: number) { + + var yMin = min; + var yMax = max; + + var labelCount = this.mAxis.getLabelCount(); + var range = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || range == Infinity) { + this.mAxis.mEntries = new Array(); + this.mAxis.mCenteredEntries = new Array(); + this.mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + var rawInterval: number = range / labelCount; + var interval: number = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (this.mAxis.isGranularityEnabled()) + interval = interval < this.mAxis.getGranularity() ? this.mAxis.getGranularity() : interval; + + // Normalize interval + var intervalMagnitude = Utils.roundToNextSignificant(Math.pow(10, Math.floor(Math.log10(interval)))); + var intervalSigDigit = Math.floor((interval / intervalMagnitude)); + if (intervalSigDigit > 5) { + // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + + } + + var n = this.mAxis.isCenterAxisLabelsEnabled() ? 1 : 0; + + // force label count + if (this.mAxis.isForceLabelsEnabled()) { + + interval = range / (labelCount - 1); + this.mAxis.mEntryCount = labelCount; + + if (this.mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + this.mAxis.mEntries = new Array(labelCount); + } + + var v = min; + + for (var i = 0; i < labelCount; i++) { + this.mAxis.mEntries[i] = v; + v += interval; + } + + n = labelCount; + + // no forced count + } else { + + var first = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if (this.mAxis.isCenterAxisLabelsEnabled()) { + first -= interval; + } + + var last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + var f; + var i: number; + + if (interval != 0.0 && last != first) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + else if (last == first && n == 0) { + n = 1; + } + + this.mAxis.mEntryCount = n; + + if (this.mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + this.mAxis.mEntries = new Array(n); + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + this.mAxis.mEntries[i] = f; + } + } + + // set decimals + if (interval < 1) { + this.mAxis.mDecimals = Math.floor(Math.ceil(-Math.log10(interval))); + } else { + this.mAxis.mDecimals = 0; + } + + if (this.mAxis.isCenterAxisLabelsEnabled()) { + + if (this.mAxis.mCenteredEntries.length < n) { + this.mAxis.mCenteredEntries = new Array(n); + } + + var offset: number = interval / 2; + + for (var i = 0; i < n; i++) { + this.mAxis.mCenteredEntries[i] = this.mAxis.mEntries[i] + offset; + } + } + } + +/** + * Draws the axis labels to the screen. + * + * @param c + */ + public abstract renderAxisLabels(c: Paint[]); + +/** + * Draws the grid lines belonging to the axis. + * + * @param c + */ + public abstract renderGridLines(c: Paint[]); + +/** + * Draws the line that goes alongside the axis. + * + * @param c + */ + public abstract renderAxisLine(c: Paint[]); + +/** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + public abstract renderLimitLines(c: Paint[]); +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/BarLineScatterCandleBubbleRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/BarLineScatterCandleBubbleRenderer.ets new file mode 100644 index 0000000..af759eb --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/BarLineScatterCandleBubbleRenderer.ets @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MPPointD from '../utils/MPPointD'; +import {XAxis} from '../components/XAxis'; +import EntryOhos from '../data/EntryOhos'; +import {DataSet, Rounding} from '../data/DataSet'; +import ChartAnimator from '../animation/ChartAnimator'; +import DataRenderer from './DataRenderer' +import ViewPortHandler from '../utils/ViewPortHandler' +import IBarLineScatterCandleBubbleDataSet from '../interfaces/datasets/IBarLineScatterCandleBubbleDataSet' +import BarLineScatterCandleBubbleDataProvider from '../interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider' +import IDataSet from '../interfaces/datasets/IDataSet' + +export class XBounds { + + constructor(mAnimator:ChartAnimator){ + this.mAnimator=mAnimator; + } + /** + * minimum visible entry index + */ + public min: number; + + /** + * maximum visible entry index + */ + public max: number; + + /** + * range of visible entry indices + */ + public range: number; + + public mAnimator:ChartAnimator + + /** + * Calculates the minimum and maximum x values as well as the range between them. + * + * @param chart + * @param dataSet + */ + public set(chart: BarLineScatterCandleBubbleDataProvider, dataSet: IBarLineScatterCandleBubbleDataSet,mAnimator:ChartAnimator) { + let phaseX: number = Math.max(0, Math.min(1, mAnimator.getPhaseX())); + + let low: number = chart.getLowestVisibleX(); + let high: number = chart.getHighestVisibleX(); + + let entryFrom: EntryOhos = dataSet.getEntryForXValue(low, NaN, Rounding.DOWN); + let entryTo: EntryOhos = dataSet.getEntryForXValue(high, NaN, Rounding.UP); + + this.min = entryFrom == null ? 0 : dataSet.getEntryIndexByEntry(entryFrom); + this.max = entryTo == null ? 0 : dataSet.getEntryIndexByEntry(entryTo); + this.range = ((this.max - this.min) * phaseX); + } +} + +/** + * Created by Philipp Jahoda on 09/06/16. + */ +export default abstract class BarLineScatterCandleBubbleRenderer extends DataRenderer { + +/** + * buffer for storing the current minimum and maximum visible x + */ + protected mXBounds: XBounds + + constructor(animator: ChartAnimator, viewPortHandler: ViewPortHandler) { + super(animator, viewPortHandler); + this.mXBounds= new XBounds(this.mAnimator); + } + +/** + * Returns true if the DataSet values should be drawn, false if not. + * + * @param set + * @return + */ + protected shouldDrawValues(dataSet: IDataSet): boolean { + return dataSet.isVisible() && (dataSet.isDrawValuesEnabled() || dataSet.isDrawIconsEnabled()); + } + +/** + * Checks if the provided entry object is in bounds for drawing considering the current animation phase. + * + * @param e + * @param set + * @return + */ + protected isInBoundsX(e: EntryOhos, dataSet: IBarLineScatterCandleBubbleDataSet): boolean { + + if (e == null) + return false; + + let entryIndex = dataSet.getEntryIndexByEntry(e); + + if (e == null || entryIndex >= dataSet.getEntryCount() * this.mAnimator.getPhaseX()) { + return false; + } else { + return true; + } + } +} + + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/DataRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/DataRenderer.ets new file mode 100644 index 0000000..326338a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/DataRenderer.ets @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MyRect from '../data/Rect'; +import Highlight from '../highlight/Highlight'; +import EntryOhos from '../data/EntryOhos'; +import Renderer from '../renderer/Renderer' +import ChartAnimator from '../animation/ChartAnimator' +import Paint, {Style,LinePaint,PathPaint, TextPaint} from '../data/Paint' +import ViewPortHandler from '../utils/ViewPortHandler' +import Utils from '../utils/Utils' +import IDataSet from '../interfaces/datasets/IDataSet' +import ChartInterface from '../interfaces/dataprovider/ChartInterface' +import IValueFormatter from '../formatter/IValueFormatter' + +/** + * Superclass of all render classes for the different data types (line, bar, ...). + * + * @author Philipp Jahoda + */ +export default abstract class DataRenderer extends Renderer { + +/** + * the animator object used to perform animations on the chart data + */ + protected mAnimator:ChartAnimator = new ChartAnimator(); + +/** + * main paint object used for rendering + */ + protected mRenderPaint:Paint; + + protected mPathPaint:PathPaint; + +/** + * paint used for highlighting values + */ + protected mHighlightPaint:Paint; + + protected mDrawPaint:Paint; + +/** + * paint object for drawing values (text representing values of chart + * entries) + */ + public mValuePaint:Paint; + + constructor(animator:ChartAnimator, viewPortHandler:ViewPortHandler) { + super(viewPortHandler); + this.mAnimator = animator; + + this.mRenderPaint = new Paint(); + this.mRenderPaint.setStyle(Style.FILL); + + this.mPathPaint = new PathPaint(); + + this.mDrawPaint = new Paint(); + + this.mValuePaint = new Paint(); + this.mValuePaint.setColor(Color.Green); + this.mValuePaint.setTextAlign(TextAlign.Center); + this.mValuePaint.setTextSize(10); + + this.mHighlightPaint = new Paint(); + this.mHighlightPaint.setStyle(Style.STROKE); + this.mHighlightPaint.setStrokeWidth(2); + this.mHighlightPaint.setColor(Color.Orange); + } + + protected isDrawingValuesAllowed(chart:ChartInterface):boolean { + return chart.getData().getEntryCount() < chart.getMaxVisibleCount() + * this.mViewPortHandler.getScaleX(); + } + +/** + * Returns the Paint object this renderer uses for drawing the values + * (value-text). + * + * @return + */ + public getPaintValues():Paint { + return this.mValuePaint; + } + +/** + * Returns the Paint object this renderer uses for drawing highlight + * indicators. + * + * @return + */ + public getPaintHighlight():Paint { + return this.mHighlightPaint; + } + +/** + * Returns the Paint object used for rendering. + * + * @return + */ + public getPaintRender():Paint { + return this.mRenderPaint; + } + + public getPathPaint():PathPaint { + return this.mPathPaint; + } + +/** + * Applies the required styling (provided by the DataSet) to the value-paint + * object. + * + * @param set + */ + protected applyValueTextStyle(dateSet:IDataSet) { + + this.mValuePaint.setTypeface(dateSet.getValueTypeface()); + this.mValuePaint.setTextSize(dateSet.getValueTextSize()); + } + +/** + * Initializes the buffers used for rendering with a new size. Since this + * method performs memory allocations, it should only be called if + * necessary. + */ + public abstract initBuffers(); + + + /** + * Draws the actual data in form of lines, bars, ... depending on Renderer subclass. + * + * @param c + */ + public abstract drawData(): Paint[]; + +/** + * Loops over all Entrys and draws their values. + * + * @param c + */ + public abstract drawValues(): Paint[]; + +/** + * Draws the value of the given entry by using the provided IValueFormatter. + * + * @param c canvas + * @param formatter formatter for custom value-formatting + * @param value the value to be drawn + * @param entry the entry the value belongs to + * @param dataSetIndex the index of the DataSet the drawn Entry belongs to + * @param x position + * @param y position + * @param color + */ + public drawValue(formatter:IValueFormatter, value:number, entry:EntryOhos, dataSetIndex:number, x:number, y:number, color:number): Paint[] { + this.mValuePaint.setColor(color); + let textPaint = new TextPaint(); + textPaint.set(this.mValuePaint); + textPaint.setText(formatter.getFormattedValue(value, entry, dataSetIndex, this.mViewPortHandler)); + textPaint.setX(x) + textPaint.setY(y) + return [textPaint] + } + +/** + * Draws any kind of additional information (e.g. line-circles). + * + * @param c + */ + public abstract drawExtras(): Paint[]; + +/** + * Draws all highlight indicators for the values that are currently highlighted. + * + * @param c + * @param indices the highlighted values + */ + public abstract drawHighlighted(indices:Highlight[] ): Paint[]; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LegendRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LegendRenderer.ets new file mode 100644 index 0000000..2b0c04a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LegendRenderer.ets @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Style, Align, FontMetrics} from '../data/Paint' +import Legend from '../components/Legend' +import {LegendForm, LegendHorizontalAlignment, LegendVerticalAlignment, LegendOrientation, LegendDirection} from '../components/Legend' +import LegendEntry from '../components/LegendEntry' +import ChartData from '../data/ChartData' +import IBarDataSet from '../interfaces/datasets/IBarDataSet' +import IDataSet from '../interfaces/datasets/IDataSet' +import EntryOhos from '../data/EntryOhos' +import ColorTemplate from '../utils/ColorTemplate' +import FSize from '../utils/FSize' +import Utils from '../utils/Utils' +import ViewPortHandler from '../utils/ViewPortHandler' +import Renderer from './Renderer' +import {JArrayList} from '../utils/JArrayList' +import Paint,{LinePaint,TextPaint,PathPaint,CirclePaint,RectPaint} from '../data/Paint' +class Point { + x1: number = 0 + y1: number = 0 + x2: number = 0 + y2: number = 0 + constructor(x1: number, y1: number, x2: number, y2: number) { + this.x1 = x1 + this.y1 = y1 + this.x2 = x2 + this.y2 = y2 + } +} + +export default class LegendRenderer extends Renderer { + + /** + * paint for the legend labels + */ + protected mLegendLabelPaint : Paint; + + /** + * paint used for the legend forms + */ + protected mLegendFormPaint : Paint; + + /** + * the legend object this renderer renders + */ + protected mLegend : Legend; + + constructor(viewPortHandler : ViewPortHandler, legend : Legend) { + super(viewPortHandler); + + this.mLegend = legend; + + this.mLegendLabelPaint = new Paint(); + this.mLegendLabelPaint.setTextSize(9); + this.mLegendLabelPaint.setAlign(Align.LEFT); + + this.mLegendFormPaint = new Paint(/*Paint.ANTI_ALIAS_FLAG*/); + //this.mLegendFormPaint.push(new Paint()) + this.mLegendFormPaint.setStyle(Style.FILL); + } + + /** + * Returns the Paint object used for drawing the Legend labels. + * + * @return + */ + public getLabelPaint() : Paint { + return this.mLegendLabelPaint; + } + + /** + * Returns the Paint object used for drawing the Legend forms. + * + * @return + */ + public getFormPaint() : Paint { + return this.mLegendFormPaint; + } + + + protected computedEntries : JArrayList = new JArrayList(/*16*/); + + /** + * Prepares the legend and calculates all needed forms, labels and colors. + * + * @param data + */ + public computeLegend(data ?: ChartData>) : void { + + if (!this.mLegend.isLegendCustom() && data) { + + this.computedEntries.clear(); + + // loop for building up the colors and labels used in the legend + for (var i : number = 0; i < data.getDataSetCount(); i++) { + + var dataSet : IDataSet = data.getDataSetByIndex(i); + if (dataSet == null) continue; + + var clrs : JArrayList = dataSet.getColors(); + var entryCount : number = dataSet.getEntryCount(); + + // if we have a barchart with stacked bars + if (dataSet.constructor.name == "IBarDataSet" && (dataSet).isStacked()) { + + var bds : IBarDataSet = dataSet; + var sLabels : string[] = bds.getStackLabels(); + + var minEntries : number = Math.min(clrs.size(), bds.getStackSize()); + + for (var j : number = 0; j < minEntries; j++) { + var label : string; + if (sLabels.length > 0) { + var labelIndex : number = j % minEntries; + label = labelIndex < sLabels.length ? sLabels[labelIndex] : null; + } else { + label = null; + } + + this.computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j).valueOf() + )); + } + + if (bds.getLabel() != null) { + // add the legend description label + this.computedEntries.add(new LegendEntry( + dataSet.getLabel(), + LegendForm.NONE, + Number.NaN, + Number.NaN, + null, + ColorTemplate.COLOR_NONE + )); + } + + } else { // all others + + for (var j :number = 0; j < clrs.size() && j < entryCount; j++) { + + var label : string; + + // if multiple colors are set for a DataSet, group them + if (j < clrs.size() - 1 && j < entryCount - 1) { + label = null; + } else { // add label to the last entry + label = data.getDataSetByIndex(i).getLabel(); + } + + this.computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j).valueOf() + )); + } + } + } + + if (this.mLegend.getExtraEntries() != null) { + var datas : LegendEntry[] = this.mLegend.getExtraEntries(); + for (var i : number = 0; i < datas.length; i++) { + this.computedEntries.add(datas[i]); + } + } + + this.mLegend.setEntries(this.computedEntries); + } + + var tf : FontWeight/*Typeface*/ = this.mLegend.getTypeface(); + + if (tf != null) + this.mLegendLabelPaint.setTypeface(tf); + + this.mLegendLabelPaint.setTextSize(this.mLegend.getTextSize()); + this.mLegendLabelPaint.setColor(this.mLegend.getTextColor()); + + // calculate all dimensions of the mLegend + this.mLegend.calculateDimensions(this.mLegendLabelPaint, this.mViewPortHandler); + } + + protected legendFontMetrics : FontMetrics = new FontMetrics(); + + // public void renderLegend(Canvas c) { + public renderLegend() : Paint [] { + + let datas : Point[] = [] + if (!this.mLegend.isEnabled()) + return; + + var tf : FontWeight/*Typeface*/ = this.mLegend.getTypeface(); + + if (tf != null) + this.mLegendLabelPaint.setTypeface(tf); + + this.mLegendLabelPaint.setTextSize(this.mLegend.getTextSize()); + this.mLegendLabelPaint.setColor(this.mLegend.getTextColor()); + + var labelLineHeight : number = Utils.getLineHeight(this.mLegendLabelPaint); + var labelLineSpacing : number = Utils.getLineSpacing(this.mLegendLabelPaint) + + this.mLegend.getYEntrySpace(); + var formYOffset : number = labelLineHeight - Utils.calcTextHeight(this.mLegendLabelPaint, "ABC") / 2.0; + + var entries : LegendEntry[] = this.mLegend.getEntries(); + + var formToTextSpace : number = this.mLegend.getFormToTextSpace(); + var xEntrySpace : number = this.mLegend.getXEntrySpace(); + var orientation : LegendOrientation = this.mLegend.getOrientation(); // "HORIZONTAL" ͼ������ + var horizontalAlignment : LegendHorizontalAlignment = this.mLegend.getHorizontalAlignment(); // "LEFT" ˮƽ���뷽ʽ + var verticalAlignment : LegendVerticalAlignment = this.mLegend.getVerticalAlignment(); // "BOTTOM + var direction : LegendDirection = this.mLegend.getDirection(); // "LEFT_TO_RIGHT" ͼ�����ı����� + var defaultFormSize : number = this.mLegend.getFormSize(); + + // space between the entries �ѵ����֮�����µĿռ� + var stackSpace : number = this.mLegend.getStackSpace(); + + var yoffset : number = this.mLegend.getYOffset(); + var xoffset : number = this.mLegend.getXOffset(); + var originPosX : number = 0.0; + var paints : Paint[] = []; + + switch (horizontalAlignment) { + case LegendHorizontalAlignment.LEFT: + + if (orientation == LegendOrientation.VERTICAL) { + originPosX = xoffset; + } else { + originPosX = this.mViewPortHandler.contentLeft() + xoffset; + } + console.log("1. originPosX("+JSON.stringify(originPosX)+")= xoffset("+xoffset+") + mViewPortHandler.contentLeft("+this.mViewPortHandler.contentLeft()+")"); + if (direction == LegendDirection.RIGHT_TO_LEFT) + originPosX += this.mLegend.mNeededWidth; + + break; + + case LegendHorizontalAlignment.RIGHT: + + if (orientation == LegendOrientation.VERTICAL) + originPosX = this.mViewPortHandler.getChartWidth() - xoffset; + else + originPosX = this.mViewPortHandler.contentRight() - xoffset; + + if (direction == LegendDirection.LEFT_TO_RIGHT) + originPosX -= this.mLegend.mNeededWidth; + + break; + + case LegendHorizontalAlignment.CENTER: + + if (orientation == LegendOrientation.VERTICAL) + originPosX = this.mViewPortHandler.getChartWidth() / 2.0; + else + originPosX = this.mViewPortHandler.contentLeft() + + this.mViewPortHandler.contentWidth() / 2.0; + + originPosX += (direction == LegendDirection.LEFT_TO_RIGHT + ? +xoffset + : -xoffset); + + // Horizontally layed out legends do the center offset on a line basis, + // So here we offset the vertical ones only. + if (orientation == LegendOrientation.VERTICAL) { + originPosX += (direction == LegendDirection.LEFT_TO_RIGHT + ? -this.mLegend.mNeededWidth / 2.0 + xoffset + : this.mLegend.mNeededWidth / 2.0 - xoffset); + } + + break; + } + + switch (orientation) { // "HORIZONTAL" + case LegendOrientation.HORIZONTAL: { + + var calculatedLineSizes : JArrayList = this.mLegend.getCalculatedLineSizes(); + var calculatedLabelSizes : JArrayList = this.mLegend.getCalculatedLabelSizes(); + var calculatedLabelBreakPoints : JArrayList = this.mLegend.getCalculatedLabelBreakPoints(); + + var posX : number = originPosX; + var posY : number = 0.0; + + switch (verticalAlignment) { // "BOTTOM" + case LegendVerticalAlignment.TOP: + posY = yoffset; + console.log("2.0 posY("+JSON.stringify(posY)+")= yoffset("+yoffset+")"); + break; + + case LegendVerticalAlignment.BOTTOM: + posY = this.mViewPortHandler.getChartHeight() - yoffset - this.mLegend.mNeededHeight; + console.log("2.1 posY("+posY+")= mViewPortHandler.getChartHeight("+this.mViewPortHandler.getChartHeight()+")-mNeededHeight("+this.mLegend.mNeededHeight+")-yoffset("+yoffset+")"); + break; + + case LegendVerticalAlignment.CENTER: + posY = (this.mViewPortHandler.getChartHeight() - this.mLegend.mNeededHeight) / 2.0 + yoffset; + console.log("2.1 posY("+posY+")= mViewPortHandler.getChartHeight("+this.mViewPortHandler.getChartHeight()+")-mNeededHeight/2("+this.mLegend.mNeededHeight/2+")+yoffset("+yoffset+")"); + break; + } + + var lineIndex : number = 0; + for (var i : number = 0, count = entries.length; i < count; i++) { + let x1: number = 0 + let y1: number = 0 + let x2: number = 0 + let y2: number = 0 + + var e : LegendEntry = entries[i]; + var drawingForm : boolean = e.form != LegendForm.NONE; + var formSize : number = Number.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + if (i < calculatedLabelBreakPoints.size() && calculatedLabelBreakPoints.get(i)) { // ����ƫ���� + posX = originPosX; + posY += labelLineHeight + labelLineSpacing; + console.log("3.0 i("+i+") posX="+posX+"; posY("+posY+")=labelLineHeight("+labelLineHeight+")+labelLineSpacing("+labelLineSpacing+")"); + } + + if (posX == originPosX && + horizontalAlignment == LegendHorizontalAlignment.CENTER && // "LEFT" + lineIndex < calculatedLineSizes.size()) { + posX += (direction == LegendDirection.RIGHT_TO_LEFT + ? calculatedLineSizes.get(lineIndex).width + : -calculatedLineSizes.get(lineIndex).width) / 2.0; + lineIndex++; + } + + var isStacked : boolean = e.label == null; // grouped forms have null labels + + if (drawingForm) { + if (direction == LegendDirection.RIGHT_TO_LEFT) + posX -= formSize; + + x1 = posX + y1 = posY + console.log("3.1 i("+i+") drawForm <==> posX="+posX+"; posY("+posY+")+formYOffset("+formYOffset+")"); + paints.push(this.drawForm(posX, posY, e, this.mLegend)); + + if (direction == LegendDirection.LEFT_TO_RIGHT) { // "LEFT_TO_RIGHT" + console.log("3.2 i("+i+") posX("+posX+")+=Utils.convertDpToPixel(formSize)("+Utils.convertDpToPixel(formSize)+")"); + posX += Utils.convertDpToPixel(formSize); + } + } + + if (!isStacked) { + if (drawingForm) { + console.log("3.3 i("+i+") posX("+posX+")+=formToTextSpace("+formToTextSpace+")"); + posX += direction == LegendDirection.RIGHT_TO_LEFT ? -formToTextSpace : + formToTextSpace; + } + + if (direction == LegendDirection.RIGHT_TO_LEFT) + posX -= calculatedLabelSizes.get(i).width; + + x2 = posX + y2 = posY + console.log("3.4 i("+i+") drawLabel <==> posX="+posX+"; posY("+posY+")+formYOffset("+formYOffset+")"); + paints.push(this.drawLabel(posX, posY, e.label)); + + if (direction == LegendDirection.LEFT_TO_RIGHT) { + console.log("3.5 i("+i+") posX("+posX+")+=calcTextWidth("+JSON.stringify(Utils.calcTextWidth(this.mLegendLabelPaint, e.label))+")"); + posX += Utils.calcTextWidth(this.mLegendLabelPaint,e.label); + } + console.log("3.6 i("+i+") posX("+posX+")+=xEntrySpace("+xEntrySpace+"); stackSpace("+stackSpace+")"); + posX += direction == LegendDirection.RIGHT_TO_LEFT ? -xEntrySpace : xEntrySpace; + } else + posX += direction == LegendDirection.RIGHT_TO_LEFT ? -stackSpace : stackSpace; + + datas.push(new Point(x1, y1, x2, y2)) + } + + break; + } + + case LegendOrientation.VERTICAL: { + // contains the stacked legend size in pixels + var stack : number = 0; + var wasStacked : boolean = false; + var posY : number = 0.1; + + switch (verticalAlignment) { + case LegendVerticalAlignment.TOP: + posY = (horizontalAlignment == LegendHorizontalAlignment.CENTER + ? 0.1 + : this.mViewPortHandler.contentTop()); + posY += yoffset; + break; + + case LegendVerticalAlignment.BOTTOM: + posY = (horizontalAlignment == LegendHorizontalAlignment.CENTER + ? this.mViewPortHandler.getChartHeight() + : this.mViewPortHandler.contentBottom()); + posY -= this.mLegend.mNeededHeight + yoffset; + break; + + case LegendVerticalAlignment.CENTER: + posY = this.mViewPortHandler.getChartHeight() / 2.0 + - this.mLegend.mNeededHeight / 2.0 + + this.mLegend.getYOffset(); + break; + } + + for (var i : number = 0; i < entries.length; i++) { + + var e : LegendEntry = entries[i]; + var drawingForm : boolean = e.form != LegendForm.NONE; + var formSize : number = Number.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + var posX : number = originPosX; + + if (drawingForm) { + if (direction == LegendDirection.LEFT_TO_RIGHT) + posX += stack; + else + posX -= formSize - stack; + + this.mLegendFormPaint = this.drawForm(/*c,*/ posX, posY + formYOffset, e, this.mLegend); + + if (direction == LegendDirection.LEFT_TO_RIGHT) + posX += formSize; + } + + if (e.label != null) { + + if (drawingForm && !wasStacked) + posX += direction == LegendDirection.LEFT_TO_RIGHT ? formToTextSpace + : -formToTextSpace; + else if (wasStacked) + posX = originPosX; + + if (direction == LegendDirection.RIGHT_TO_LEFT) + posX -= Utils.calcTextWidth(this.mLegendLabelPaint[i], e.label); + + if (!wasStacked) { + this.mLegendLabelPaint = this.drawLabel(/*c,*/ posX, posY + formYOffset, e.label); + } else { + posY += formYOffset + labelLineSpacing; + this.mLegendLabelPaint = this.drawLabel(/*c,*/ posX, posY + formYOffset, e.label); + } + + // make a step down + posY += formYOffset + labelLineSpacing; + stack = 0; + } else { + stack += formSize + stackSpace; + wasStacked = true; + } + paints.push(this.mLegendFormPaint); + paints.push(this.mLegendLabelPaint); + } + + break; + + } + } + console.log("6. LegendRenderer datas:"+JSON.stringify(datas)); + return paints; + } + + /** + * Draws the Legend-form at the given position with the color at the given + * index. + * + * @param c canvas to draw with + * @param x position + * @param y position + * @param entry the entry to render + * @param legend the legend context + */ + protected drawForm( + x : number, y : number, + entry : LegendEntry, + legend : Legend) : Paint { + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == ColorTemplate.COLOR_NONE || + entry.formColor == 0) + return; + + var form : LegendForm = entry.form; + if (form == LegendForm.DEFAULT) + form = legend.getForm(); + + var formSize : number = Utils.convertDpToPixel( + Number.isNaN(entry.formSize) + ? legend.getFormSize() + : entry.formSize); + var half : number = formSize / 2; + + switch (form) { + case LegendForm.NONE: + // Do nothing + break; + + case LegendForm.EMPTY: + // Do not draw, but keep space for the form + break; + + case LegendForm.DEFAULT: + case LegendForm.CIRCLE: + var circlePaint : CirclePaint = new CirclePaint()//= this.mLegendFormPaint as CirclePaint; + circlePaint.setColor(entry.formColor); + circlePaint.setStyle(Style.FILL) + circlePaint.setX(x + half) + circlePaint.setY(y) + circlePaint.setWidth(half) + circlePaint.setHeight(half) + return circlePaint + break; + + case LegendForm.SQUARE: + var rectPaint : RectPaint = new RectPaint();//= this.mLegendFormPaint as RectPaint; + rectPaint.setStroke(entry.formColor) + rectPaint.setStyle(Style.FILL) + rectPaint.setStartPoint([x, y]) + rectPaint.setWidth(formSize) + rectPaint.setHeight(half) + return rectPaint + break; + + case LegendForm.LINE: + { + var linePaint : LinePaint //= this.mLegendFormPaint as LinePaint; + var formLineWidth : number = Utils.convertDpToPixel( + Number.isNaN(entry.formLineWidth) + ? legend.getFormLineWidth() + : entry.formLineWidth); + /*var formLineDashEffect : DashPathEffect = entry.formLineDashEffect == null + ? legend.getFormLineDashEffect() + : entry.formLineDashEffect;*/ + linePaint.setStyle(Style.STROKE) + linePaint.setStrokeWidth(formLineWidth) + + linePaint.setStartPoint([x, y]) + linePaint.setEndPoint([x + formSize, y]) + return linePaint + } + break; + } + } + + /** + * Draws the provided label at the given position. + * + * @param c to draw with + * @param x + * @param y + * @param label the label to draw + */ + // protected void drawLabel(Canvas c, float x, float y, String label) { + protected drawLabel(/*c : Canvas,*/ x : number, y : number, label : string) : Paint { + + var textPaint : TextPaint = new TextPaint(this.mLegendLabelPaint as TextPaint) //= this.mLegendLabelPaint as TextPaint; + textPaint.setText(label) + textPaint.setX(x) + textPaint.setY(y) + return textPaint + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineChartRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineChartRenderer.ets new file mode 100644 index 0000000..966702f --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineChartRenderer.ets @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import YAxis,{AxisDependency} from '../components/YAxis'; +import EntryOhos from '../data/EntryOhos'; +import MyRect from '../data/Rect'; +import {LineDataSet,Mode} from '../data/LineDataSet'; +import MPPointD from '../utils/MPPointD'; +import { PathViewModel } from '../components/PathView'; +import {XAxis} from '../components/XAxis'; +import LineDataProvider from '../interfaces/dataprovider/LineDataProvider' +import LineData from '../data/LineData' +import Paint, { PathPaint, TextPaint, CirclePaint, PathFillPaint, LinePaint, ImagePaint} from '../data/Paint' +import {Rounding} from '../data/DataSet' +import Utils from '../utils/Utils' +import {FillStyle} from '../data/LineDataSet' +import LineScatterCandleRadarRenderer from '../renderer/LineScatterCandleRadarRenderer' + +export default class LineChartRenderer extends LineScatterCandleRadarRenderer { + protected mChart: LineDataProvider; + private min; max: number; + private range: number; + private phaseY: number = 1; + private phaseX: number = 1; + private isInverted: boolean = false; + private pathViewModel: PathViewModel; + private lineData: LineData; + private yleftAxis: YAxis; + private yRightAxis: YAxis; + private animateXIndex: number = Number.MAX_VALUE; + private animateYValue: number = 1; + + private mLineBuffer: number[] = new Array(4); + + private clickPaint: Paint[] = []; + private xStartPoint:number[] = []; + private xEndPoint:number[] = []; + private yStartPoint:number[] = []; + private yEndPoint:number[] = []; + + + constructor(pathViewModel: PathViewModel,yleftAxis: YAxis,yRightAxis: YAxis,isInverted: boolean) { + super(null, null); + this.pathViewModel = pathViewModel; + this.lineData = pathViewModel.getLineData(); + this.yleftAxis = yleftAxis; + this.yRightAxis = yRightAxis; + this.isInverted = isInverted; + } + + + public drawExtras():Paint[]{ + return []; + } + + + public drawHighlighted():Paint[]{ + + // 1、拿到点击的物理坐标,转换成图形坐标 + // 2、拿点击转换后的坐标和数据中所有的点作比较,取最近的点, + // 3、以最近的点为中心画十字架 + this.clickPaint = []; + + let x = this.pathViewModel.eventX; + let y = this.pathViewModel.eventY; + + if(x > 0 && x < this.pathViewModel.rect.right && y > 0 && y < this.pathViewModel.rect.bottom){ + let ccTemp: number = Number.MAX_VALUE; + let positionX: number; + let positionY: number; + let entryX: number; + let entryY: number; + let maxY: number; + let isLeftAxis; + let yAxis: YAxis; + let yScale: number; + let size = this.lineData.getDataSets().length(); + for (let i = 0;i < size; i++) { + let entryArray: EntryOhos[] = (this.lineData.getDataSetByIndex(i) as LineDataSet).getEntries().toArray(); + for(let j = 0; j < entryArray.length; j++){ + isLeftAxis = this.pathViewModel.lineData.getDataSetByIndex(i).getAxisDependency() == AxisDependency.LEFT; + yAxis = isLeftAxis ? this.pathViewModel.yleftAxis : this.pathViewModel.yRightAxis; + yScale = isLeftAxis ? this.pathViewModel.yLeftScale : this.pathViewModel.yRightScale; + + entryX = entryArray[j].getX() * this.pathViewModel.xScale * this.pathViewModel.scaleX + this.pathViewModel.moveX - this.pathViewModel.currentXSpace; + maxY = yAxis.getAxisMaximum(); + if(!this.pathViewModel.isInverted){ + entryY = (maxY - entryArray[j].getY()) * yScale; + } else { + entryY = (entryArray[j].getY()) * yScale; + } + + entryY = entryY * this.pathViewModel.scaleY + this.pathViewModel.moveY - this.pathViewModel.currentYSpace + let a = Math.abs(x - entryX); + let b = Math.abs(y - entryY); + let cc = Math.sqrt(a * a + b * b); + + if(ccTemp > cc){ + ccTemp = cc; + positionX = entryX; + positionY = entryY; + + } + } + } + + + let textPaint: TextPaint = new TextPaint(); + let value: string = Math.floor(maxY - positionY / yScale).toString(); + textPaint.setText(value); + textPaint.setColor(Color.White); + textPaint.setX(positionX - Utils.calcTextWidth(textPaint,value) / 2); + textPaint.setY(positionY - Utils.calcTextHeight(textPaint,value) - (Utils.calcTextHeight(textPaint,value) + 20) / 2); + + + let imagePaint: ImagePaint = new ImagePaint(); + imagePaint.setWidth(Utils.calcTextWidth(textPaint,value) + 20); + imagePaint.setHeight(Utils.calcTextHeight(textPaint,value) + 20); + imagePaint.setX(positionX - (Utils.calcTextWidth(textPaint,value) + 20) / 2); + imagePaint.setY(positionY - Utils.calcTextHeight(textPaint,value) - 20); + + + let yLinePaint: LinePaint = new LinePaint(); + yLinePaint.setStartPoint([positionX,0]); + yLinePaint.setEndPoint([positionX,this.pathViewModel.rect.bottom - this.pathViewModel.minOffset]); + yLinePaint.setColor(Color.Red); + yLinePaint.setStrokeWidth(0.5); + + + let xLinePaint: LinePaint = new LinePaint(); + xLinePaint.setStartPoint([0,positionY]); + xLinePaint.setEndPoint([this.pathViewModel.rect.right - this.pathViewModel.minOffset - Utils.calcTextWidth(textPaint,yAxis.getLongestLabel()),positionY]); + xLinePaint.setColor(Color.Red); + xLinePaint.setStrokeWidth(0.5); + + this.clickPaint.push(xLinePaint); + this.clickPaint.push(yLinePaint); + this.clickPaint.push(imagePaint); + this.clickPaint.push(textPaint); + } + return this.clickPaint; + } + + + public initBuffers(){ + + } + + public drawData():Paint[]{ + + let pathPaintArr: Paint[] = []; + + let firstPointX,lastPointX,minPointY, maxPointY: number; + + + let xScale: number = this.pathViewModel.xScale; + let yScale: number = 1; + let lineDataSet: LineDataSet; + for(let i = 0; i < this.lineData.getDataSetCount(); i++) { + lineDataSet = this.lineData.getDataSetByIndex(i) as LineDataSet; + if(lineDataSet.getEntries().size() <= 0){ + continue; + } + + this.animateXIndex = this.animateXIndex <= (lineDataSet.getEntries().length() - 1) ? this.animateXIndex : (lineDataSet.getEntries().length() - 1); + + firstPointX = Utils.convertDpToPixel(this.getXPosition(lineDataSet.getEntries().at(0).getX() * xScale * this.pathViewModel.scaleX)); + lastPointX = Utils.convertDpToPixel(this.getXPosition(lineDataSet.getEntries().at(this.animateXIndex).getX() * xScale)); + if(lineDataSet.getAxisDependency() == AxisDependency.LEFT){ + yScale = this.pathViewModel.yLeftScale; + minPointY = Utils.convertDpToPixel(this.getYPosition((this.yleftAxis.getAxisMaximum() - this.yleftAxis.getAxisMinimum() * this.animateYValue) * yScale)); + maxPointY = this.getYPosition(0); // 屏幕上Y值的0点在最上方,而图例的最大是在最上方,所以取0 + } else { + yScale = this.pathViewModel.yRightScale; + minPointY = Utils.convertDpToPixel(this.getYPosition((this.yRightAxis.getAxisMaximum() - this.yRightAxis.getAxisMinimum() * this.animateYValue) * yScale)); + maxPointY = this.getYPosition(0); + } + + let pathPaint: Paint[] = this.computePathDataSet(lineDataSet,xScale,yScale,firstPointX,lastPointX,minPointY, maxPointY); + pathPaintArr = pathPaintArr.concat(pathPaint); + } + + return pathPaintArr; + } + + protected computePathDataSet(dataSet: LineDataSet, xScale: number, yScale: number,firstPointX: number,lastPointX: number,minPointY: number,maxPointY: number):Paint[] { + if (this.lineData.getDataSetCount() < 1) return null; + + let chartMode: Mode = dataSet.getMode(); + switch (chartMode) { + case Mode.LINEAR: + case Mode.STEPPED: + return this.computeLinear(dataSet, xScale, yScale, firstPointX,lastPointX,minPointY, maxPointY); + break; + case Mode.CUBIC_BEZIER: + return this.computeCubicBezier(dataSet, xScale, yScale, firstPointX,lastPointX,minPointY, maxPointY); + break; + case Mode.HORIZONTAL_BEZIER: + return this.computeHorizontalBezier(dataSet, xScale, yScale, firstPointX,lastPointX,minPointY, maxPointY); + break; + + default: + break; + } + } + + protected computeCubicBezier(dataSet: LineDataSet, xScale: number, yScale: number,firstPointX: number,lastPointX: number,minY: number,maxY: number):Paint[] { + + let intensity = dataSet.getCubicIntensity(); + + let mXAxis = new XAxis() + let posForGetLowestVisibleX = MPPointD.getInstance(0, 0); + let posForGetHighestVisibleX = MPPointD.getInstance(0, 0); + + let low = Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.x); + let high = Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.x); + + let entryFrom = dataSet.getEntryForXValue(low, Number.NaN, Rounding.DOWN); + let entryTo = dataSet.getEntryForXValue(high, Number.NaN, Rounding.UP); + + this.min = entryFrom == null ? 0 : dataSet.getEntryIndexByEntry(entryFrom); + this.max = entryTo == null ? 0 : dataSet.getEntryIndexByEntry(entryTo); + + this.range = dataSet.getEntryCount() - 1; + + if (this.range >= 1) { + + let prevDx = 0; + let prevDy = 0; + let curDx = 0; + let curDy = 0; + + let firstIndex: number = this.min + 1; + let lastIndex: number = this.min + this.range; + + let prevPrev; + let prev = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); + let cur = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); + let next = cur; + let nextIndex = -1; + + if (cur == null) return; + + var commandsData: string = ''; + var commandsDataFill: string = ''; + + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsDataFill = 'M' + firstPointX + ' ' + minY; + } else { + commandsDataFill = 'M' + firstPointX + ' ' + minY; + } + + let startAddY: number; + let yAxis: YAxis = dataSet.getAxisDependency() == AxisDependency.LEFT ? this.yleftAxis : this.yRightAxis; + if (!this.isInverted) { + startAddY = Utils.convertDpToPixel(this.getYPosition((yAxis.getAxisMaximum() - cur.getY() * this.animateYValue) * yScale)); + } else { + startAddY = Utils.convertDpToPixel(this.getYPosition((cur.getY() * this.animateYValue) * yScale)); + } + + commandsDataFill += " L"+Utils.convertDpToPixel(this.getXPosition(cur.getX() * xScale)) + " " + startAddY + + commandsData = "M" + Utils.convertDpToPixel(this.getXPosition(cur.getX() * xScale)) + " " + startAddY; + + for (let j = this.min + 1; j <= this.range + this.min; j++) { + + if(j > this.animateXIndex){ + break; + } + prevPrev = prev; + prev = cur; + cur = nextIndex == j ? next : dataSet.getEntryForIndex(j); + + nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; + next = dataSet.getEntryForIndex(nextIndex); + + prevDx = (cur.getX() - prevPrev.getX()) * intensity; + prevDy = (yAxis.getAxisMaximum() - cur.getY() * this.animateYValue - (yAxis.getAxisMaximum() - prevPrev.getY() * this.animateYValue)) * intensity; + curDx = (next.getX() - prev.getX()) * intensity; + curDy = 0; + + let realY1: number = this.isInverted ? (prev.getY() * this.animateYValue + prevDy) : (yAxis.getAxisMaximum() - prev.getY() * this.animateYValue + prevDy); + let x1 = Utils.convertDpToPixel(this.getXPosition((prev.getX() + prevDx) * xScale)); + let y1 = Utils.convertDpToPixel(this.getYPosition(realY1 * yScale)); + + let realY2: number = this.isInverted ? cur.getY() * this.animateYValue : yAxis.getAxisMaximum() - cur.getY() * this.animateYValue; + let x2 = Utils.convertDpToPixel(this.getXPosition((cur.getX() - curDx) * xScale)); + let y2 = Utils.convertDpToPixel(this.getYPosition((realY2 - curDy) * yScale)); + + let realY3: number = this.isInverted ? cur.getY() * this.animateYValue : yAxis.getAxisMaximum() - cur.getY() * this.animateYValue; + let x3 = Utils.convertDpToPixel(this.getXPosition(cur.getX() * xScale)); + let y3 = Utils.convertDpToPixel(this.getYPosition(realY3 * yScale)); + + commandsData += " C" + x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3; + commandsDataFill += " C" + x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3; + } + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsDataFill += " L" + lastPointX + " " + minY; + } else { + commandsDataFill += " L" + lastPointX + " " + maxY; + } + } + + return this.creatPathPaintArr(dataSet,commandsData,commandsDataFill);; + } + + private getXPosition(x: number): number{ + return x * this.pathViewModel.scaleX + this.pathViewModel.moveX - this.pathViewModel.currentXSpace + } + + private getYPosition(y: number): number{ + return y * this.pathViewModel.scaleY + this.pathViewModel.moveY - this.pathViewModel.currentYSpace; + } + + private computeLinear(dataSet: LineDataSet,xScale: number, yScale: number,firstPointX: number,lastPointX: number,minY: number,maxY: number): Paint[]{ + + let x,y: number; + + var commandsData: string = ''; + var commandsFillData: string = ''; + + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsFillData = 'M' + firstPointX + ' ' + minY; + } else { + commandsFillData = 'M' + firstPointX + ' ' + maxY; + } + + let entryArray: Array = dataSet.getEntries().toArray(); + let entryOhosY: number; + for(let i = 0; i < entryArray.length;i++){ + if(i > this.animateXIndex){ + break; + } + if(!this.isInverted) { + if(dataSet.getAxisDependency() == AxisDependency.LEFT){ + entryOhosY = this.yleftAxis.getAxisMaximum() - entryArray[i].getY() * this.animateYValue; + } else { + entryOhosY = this.yRightAxis.getAxisMaximum() - entryArray[i].getY() * this.animateYValue; + } + } else { + entryOhosY = entryArray[i].getY() * this.animateYValue; + } + + x = Utils.convertDpToPixel(this.getXPosition(entryArray[i].getX() * xScale)); + y = Utils.convertDpToPixel(this.getYPosition(entryOhosY * yScale)); + if(i == 0){ + commandsData = "M" + x + " " + y; + } else { + commandsData += " L" + x + " " + y; + } + commandsFillData += " L" + x + " " + y; + } + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsFillData += " L" + lastPointX + " " + minY; + } else { + commandsFillData += " L" + lastPointX + " " + maxY; + } + + return this.creatPathPaintArr(dataSet,commandsData,commandsFillData); + } + + private computeHorizontalBezier(dataSet: LineDataSet,xScale: number, yScale: number,firstPointX: number,lastPointX: number,minY: number,maxY: number): Paint[]{ + + let mXAxis = new XAxis() + let posForGetLowestVisibleX = MPPointD.getInstance(0, 0); + let posForGetHighestVisibleX = MPPointD.getInstance(0, 0); + + let low = Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.x); + let high = Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.x); + + let entryFrom = dataSet.getEntryForXValue(low, Number.NaN, Rounding.DOWN); + let entryTo = dataSet.getEntryForXValue(high, Number.NaN, Rounding.UP); + + this.min = entryFrom == null ? 0 : dataSet.getEntryIndexByEntry(entryFrom); + this.max = entryTo == null ? 0 : dataSet.getEntryIndexByEntry(entryTo); + this.range = dataSet.getEntryCount() - 1; + + if (this.range >= 1) { + + let prev: EntryOhos = dataSet.getEntryForIndex(this.min); + let cur: EntryOhos = prev; + + var commandsData: string = ''; + var commandsDataFill: string = ''; + + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsDataFill = 'M' + firstPointX + ' ' + minY; + } else { + commandsDataFill = 'M' + firstPointX + ' ' + maxY; + } + + let startAddY: number; + let yAxis: YAxis = dataSet.getAxisDependency() == AxisDependency.LEFT ? this.yleftAxis : this.yRightAxis; + if (!this.isInverted) { + startAddY = Utils.convertDpToPixel(this.getYPosition((yAxis.getAxisMaximum() - cur.getY() * this.animateYValue) * yScale)); + } else { + startAddY = Utils.convertDpToPixel(this.getYPosition((cur.getY() * this.animateYValue) * yScale)); + } + + commandsDataFill += " L"+Utils.convertDpToPixel(this.getXPosition(cur.getX() * xScale)) + " " + startAddY + + commandsData = "M" + Utils.convertDpToPixel(this.getXPosition(cur.getX()) * xScale) + " " + startAddY; + + for (let j = this.min + 1; j <= this.range + this.min; j++) { + + if(j > this.animateXIndex){ + break; + } + + prev = cur; + cur = dataSet.getEntryForIndex(j); + + let realY1: number = this.isInverted ? (prev.getY() * this.animateYValue) : (yAxis.getAxisMaximum() - prev.getY() * this.animateYValue); + let x1 = Utils.convertDpToPixel(this.getXPosition(((prev.getX()) + (cur.getX() - prev.getX()) / 2) * xScale)); + let y1 = Utils.convertDpToPixel(this.getYPosition(realY1 * yScale)); + + let realY2: number = this.isInverted ? (cur.getY() * this.animateYValue) : (yAxis.getAxisMaximum() - cur.getY() * this.animateYValue); + let x2 = Utils.convertDpToPixel(this.getXPosition(((prev.getX()) + (cur.getX() - prev.getX()) / 2) * xScale)); + let y2 = Utils.convertDpToPixel(this.getYPosition(realY2 * yScale)); + + let realY3: number = this.isInverted ? (cur.getY() * this.animateYValue) : (yAxis.getAxisMaximum() - cur.getY() * this.animateYValue); + let x3 = Utils.convertDpToPixel(this.getXPosition(cur.getX() * xScale)); + let y3 = Utils.convertDpToPixel(this.getYPosition(realY3 * yScale)); + + commandsData += " C" + x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3; + commandsDataFill += " C" + x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3; + } + if(dataSet.getFillStyle() == FillStyle.MIN){ + commandsDataFill += " L" + lastPointX + " " + minY; + } else { + commandsDataFill += " L" + lastPointX + " " + maxY; + } + } + + return this.creatPathPaintArr(dataSet,commandsData,commandsDataFill); + } + + public drawCircle(): Paint[]{ + let circlePaints: Paint[] = new Array(); + + let xScale: number = this.pathViewModel.xScale; + let yScale: number = 1; + for (let index = 0; index < this.lineData.getDataSetCount(); index++) { + + let lineDataSet: LineDataSet = this.lineData.getDataSetByIndex(index) as LineDataSet; + if(!lineDataSet.isDrawCirclesEnabled()){ + continue; + } + + if(lineDataSet.getAxisDependency() == AxisDependency.LEFT){ + yScale = this.pathViewModel.yLeftScale; + } else { + yScale = this.pathViewModel.yRightScale; + } + + let entryOhosY: number; + for (let i = 0; i < lineDataSet.getEntries().length(); i++) { + if(i > this.animateXIndex){ + break; + } + let circlePaint: CirclePaint = new CirclePaint(); + + if(!this.isInverted) { + if(lineDataSet.getAxisDependency() == AxisDependency.LEFT){ + entryOhosY = this.yleftAxis.getAxisMaximum() - lineDataSet.getEntries().at(i).getY() * this.animateYValue; + } else { + entryOhosY = this.yRightAxis.getAxisMaximum() - lineDataSet.getEntries().at(i).getY() * this.animateYValue; + } + } else { + entryOhosY = lineDataSet.getEntries().at(i).getY() * this.animateYValue; + } + let xx: number = lineDataSet.getEntries().at(i).getX() * xScale; + let yy: number = entryOhosY * yScale; + circlePaint.setDrawCircles(lineDataSet.isDrawCirclesEnabled()) + circlePaint.setCirclesColor(lineDataSet.getCircleColor()) + circlePaint.setCircleRadius(lineDataSet.getCircleRadius()) + circlePaint.setDrawCircleHole(lineDataSet.isDrawCircleHoleEnabled()) + circlePaint.setCircleHoleRadius(lineDataSet.getCircleHoleRadius()) + circlePaint.setCircleHoleColor(lineDataSet.getCircleHoleColor()) + circlePaint.setX(xx * this.pathViewModel.scaleX + this.pathViewModel.moveX - this.pathViewModel.currentXSpace); + circlePaint.setY(yy * this.pathViewModel.scaleY + this.pathViewModel.moveY - this.pathViewModel.currentYSpace); + + circlePaints.push(circlePaint); + } + } + return circlePaints; + } + + public drawValues():Paint[]{ + let textPaints:Paint[] = new Array(); + + let xScale: number = this.pathViewModel.xScale; + let yScale: number = 1; + let entryArray: Array; + + for (let i = 0; i < this.lineData.getDataSetCount(); i++) { + let lineDataSet: LineDataSet = this.pathViewModel.getLineData().getDataSetByIndex(i) as LineDataSet; + if(!lineDataSet.isDrawValuesEnabled()){ + return []; + } + + entryArray = lineDataSet.getEntries().toArray(); + + if(lineDataSet.getAxisDependency() == AxisDependency.LEFT){ + yScale = this.pathViewModel.yLeftScale; + } else { + yScale = this.pathViewModel.yRightScale; + } + + let entryOhosY: number; + let xx: number; + let yy: number; + let value: string + for (let i = 0; i < entryArray.length; i++) { + + if(i > this.animateXIndex){ + break; + } + let textPaint:TextPaint = new TextPaint(); + if(!this.isInverted) { + if(lineDataSet.getAxisDependency() == AxisDependency.LEFT){ + entryOhosY = this.yleftAxis.getAxisMaximum() - entryArray[i].getY() * this.animateYValue; + } else { + entryOhosY = this.yRightAxis.getAxisMaximum() - entryArray[i].getY() * this.animateYValue; + } + } else { + entryOhosY = entryArray[i].getY() * this.animateYValue; + } + value = Math.floor(entryArray[i].getY()).toString(); + xx = entryArray[i].getX() * xScale * this.pathViewModel.scaleX - Utils.calcTextWidth(textPaint,value) / 2; + yy = entryOhosY * yScale * this.pathViewModel.scaleY - Utils.calcTextHeight(textPaint,value) - 2; + textPaint.setText(value); + textPaint.setX(xx + this.pathViewModel.moveX - this.pathViewModel.currentXSpace); + textPaint.setY(yy + this.pathViewModel.moveY - this.pathViewModel.currentYSpace); + textPaints.push(textPaint); + } + } + return textPaints; + } + + private creatPathPaintArr(dataSet: LineDataSet, commandsData: string,commandsDataFill ? : string): Paint[]{ + let pathPaints:Paint[] = new Array(); + + let pathPaint: PathPaint = new PathPaint(); + pathPaint.setCommands(commandsData); + pathPaint.setAxisDependency(dataSet.getAxisDependency()); + pathPaint.setStrokeWidth(dataSet.getLineWidth()); + pathPaint.setStroke(dataSet.getColor()) + pathPaint.setDashPathEffect(dataSet.getDashPathEffect()) + + if(dataSet.isDrawFilledEnabled()){ + let pathFillPaint: PathFillPaint = new PathFillPaint(); + pathFillPaint.setCommandsFill(commandsDataFill); + pathFillPaint.setDrawFilled(dataSet.isDrawFilledEnabled()); + pathFillPaint.setGradientFillColor(dataSet.getGradientFillColor()) + pathPaints.push(pathFillPaint); + } + + pathPaints.push(pathPaint); + + return pathPaints; + } + + public animateX(animateIndex: number){ + this.animateXIndex = animateIndex; + } + + public animateY(animateYValue: number){ + this.animateYValue = animateYValue; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineRadarRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineRadarRenderer.ets new file mode 100644 index 0000000..757492b --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineRadarRenderer.ets @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint, { ImagePaint , PathPaint} from '../data/Paint'; +import LineScatterCandleRadarRenderer from './LineScatterCandleRadarRenderer' +import Utils from '../utils/Utils' +import ChartAnimator from '../animation/ChartAnimator' +import ViewPortHandler from '../utils/ViewPortHandler' + +/** + * Created by Philipp Jahoda on 25/01/16. + */ +export default abstract class LineRadarRenderer extends LineScatterCandleRadarRenderer { + + constructor(animator:ChartAnimator, viewPortHandler:ViewPortHandler) { + super(animator, viewPortHandler); + } + +/** + * Draws the provided path in filled mode with the provided drawable. + * + * @param c + * @param filledPath + * @param drawable + */ + protected drawFilledPath(filledPath:string , icon?:ImagePaint,fillColor?:number, fillAlpha?:number):Paint[] { + if(icon != null){ + icon.setX(this.mViewPortHandler.contentLeft()); + icon.setY(this.mViewPortHandler.contentTop()); + icon.setWidth(this.mViewPortHandler.contentRight() - this.mViewPortHandler.contentLeft()); + icon.setHeight(this.mViewPortHandler.contentBottom() - this.mViewPortHandler.contentTop()); + return [icon] + }else{ + let color:number = (fillAlpha << 24) | (fillColor & 0xffffff); + let pathPaint:PathPaint = new PathPaint(); + pathPaint.setCommands(filledPath); + pathPaint.setFill(color); + return [pathPaint] + } + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineScatterCandleRadarRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineScatterCandleRadarRenderer.ets new file mode 100644 index 0000000..d4a6fcc --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/LineScatterCandleRadarRenderer.ets @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint from '../data/Paint'; +import {PathPaint} from '../data/Paint'; +import BarLineScatterCandleBubbleRenderer from './BarLineScatterCandleBubbleRenderer' +import ChartAnimator from '../animation/ChartAnimator' +import ViewPortHandler from '../utils/ViewPortHandler' +import ILineScatterCandleRadarDataSet from '../interfaces/datasets/ILineScatterCandleRadarDataSet' + +/** + * Created by Philipp Jahoda on 11/07/15. + */ +export default abstract class LineScatterCandleRadarRenderer extends BarLineScatterCandleBubbleRenderer { + +/** + * path that is used for drawing highlight-lines (drawLines(...) cannot be used because of dashes) + */ + private mHighlightLinePath: PathPaint = new PathPaint(); + + constructor(animator: ChartAnimator, viewPortHandler: ViewPortHandler) { + super(animator, viewPortHandler); + } + +/** + * Draws vertical & horizontal highlight-lines if enabled. + * + * @param c + * @param x x-position of the highlight line intersection + * @param y y-position of the highlight line intersection + * @param set the currently drawn dataset + */ + protected drawHighlightLines(x: number, y: number, dataSet: ILineScatterCandleRadarDataSet): Paint[] { + + let pathPaints: Paint[] = []; + + // set color and stroke-width + this.mHighlightPaint.setColor(dataSet.getHighLightColor()); + this.mHighlightPaint.setStrokeWidth(dataSet.getHighlightLineWidth()); + + // draw highlighted lines (if enabled) + this.mHighlightPaint.setDashPathEffect(dataSet.getDashPathEffectHighlight()); + + // draw vertical highlight lines + if (dataSet.isVerticalHighlightIndicatorEnabled()) { + + let pathPath = new PathPaint(); + pathPath.set(this.mHighlightPaint); + pathPath.setCommands("M"+x+" "+this.mViewPortHandler.contentTop()+" L"+x+" "+this.mViewPortHandler.contentBottom())+" Z" + // create vertical path + pathPaints.push(pathPath); + } + + // draw horizontal highlight lines + if (dataSet.isHorizontalHighlightIndicatorEnabled()) { + + let pathPath = new PathPaint(); + pathPath.set(this.mHighlightPaint); + pathPath.setCommands("M"+this.mViewPortHandler.contentLeft()+" "+y+" L"+this.mViewPortHandler.contentRight()+" "+y)+" Z" + // create horizontal path + pathPaints.push(pathPath) + } + return pathPaints; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/RadarChartRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/RadarChartRenderer.ets new file mode 100644 index 0000000..72b9091 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/RadarChartRenderer.ets @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint,{PathPaint,ImagePaint,Style, LinePaint,IndexPositionPaint, CirclePaint, TextPaint}from '../data/Paint'; +import ChartAnimator from '../animation/ChartAnimator' +import RadarData from '../data/RadarData' +import RadarEntry from '../data/RadarEntry' +import Highlight from '../highlight/Highlight' +import IRadarDataSet from '../interfaces/datasets/IRadarDataSet' +import ColorTemplate from '../utils/ColorTemplate' +import MPPointF from '../utils/MPPointF' +import Utils from '../utils/Utils' +import LineRadarRenderer from './LineRadarRenderer' +import MyRect from '../data/Rect' +import RadarChartMode from '../data/RadarChartMode' +export default class RadarChartRenderer extends LineRadarRenderer { + + public TYPE_POINT:number=1 + public TYPE_PATH:number=2 + public radarChartMode:RadarChartMode=new RadarChartMode() + /** + * paint for drawing the web + */ + public mWebPaint:Paint; + protected mHighlightCirclePaint:Paint; + + + constructor(radarChartMode:RadarChartMode) { + super(radarChartMode.mAnimator, radarChartMode.handler); + this.radarChartMode = radarChartMode; + + this.mHighlightPaint = new Paint(); + this.mHighlightPaint.setStrokeWidth(2); + this.mHighlightPaint.setColor(0xffbb73); + + this.mWebPaint = new LinePaint(); + this.mWebPaint.setStyle(Style.STROKE); + + this.mHighlightCirclePaint = new Paint(); + if(this.mAnimator==null||this.mAnimator==undefined){ + this.mAnimator = new ChartAnimator(); + } + } + + public getWebPaint():Paint { + return this.mWebPaint; + } + + public initBuffers():void{ + // TODO Auto-generated method stub + + } + + public drawData():Paint[]{ + return this.drawDataByType(this.TYPE_PATH) + } + public drawDataByType(byType:number):Paint[]{ + let paint:Paint[]=[] + let radarData:RadarData = this.radarChartMode.getData(); + + let mostEntries:number = radarData.getMaxEntryCountSet().getEntryCount(); + let i=0; + for (let set1 of radarData.getDataSets().dataSouce) { + if (set1.isVisible()) { + paint=paint.concat(this.drawDataSetByType(set1, mostEntries,byType,i)); + } + i++ + } + return paint + } + + protected mDrawDataSetSurfacePathBuffer:string = ""; + /** + * Draws the RadarDataSet + * + * @param c + * @param dataSet + * @param mostEntries the entry count of the dataset with the most entries + */ + protected drawDataSet( dataSet:IRadarDataSet, mostEntries:number):Paint[] { + return [] + } + protected drawDataSetByType( dataSet:IRadarDataSet, mostEntries:number,byType:number,parentIndex:number):Paint[] { + let phaseX:number = this.mAnimator.getPhaseX(); + let phaseY:number = this.mAnimator.getPhaseY(); + + let sliceangle:number = this.radarChartMode.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + let factor:number = this.radarChartMode.getFactor(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + let surface:string = this.mDrawDataSetSurfacePathBuffer; + surface=''; + let pointTypeArr:Paint[]=[] + + let hasMovedToPoint:boolean = false; + + for (let j = 0; j < dataSet.getEntryCount(); j++) { + let pointType:IndexPositionPaint=new IndexPositionPaint(); + pointType.setDataSetIndex(parentIndex) + pointType.setDataIndex(j) + this.mRenderPaint.setColor(dataSet.getColor(j)); + + let e:RadarEntry = dataSet.getEntryForIndex(j); + + Utils.getPosition( + center, + (e.getY() - this.radarChartMode.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + this.radarChartMode.getRotationAngle(), pOut); + // + if (Number.isNaN(pOut.x)) + continue; + + if (!hasMovedToPoint) { + surface="M"+Utils.convertDpToPixel(pOut.x)+" "+Utils.convertDpToPixel(pOut.y)+" "; + hasMovedToPoint = true; + } else { + surface += "L" + Utils.convertDpToPixel(pOut.x) + " " + Utils.convertDpToPixel(pOut.y) + " "; + } + pointType.x=pOut.x; + pointType.y=pOut.y; + pointTypeArr.push(pointType); + } + // + if (dataSet.getEntryCount() > mostEntries) { + // if this is not the largest set, draw a line to the center before closing + surface+="L"+Utils.convertDpToPixel(center.x)+" "+Utils.convertDpToPixel(center.y)+" "; + let pointType:IndexPositionPaint=new IndexPositionPaint(); + pointType.x=center.x; + pointType.y=center.y; + pointType.setDataSetIndex(-1) + pointType.setDataIndex(-1) + pointTypeArr.push(pointType); + } + // + // surface.close(); + // + // if (dataSet.isDrawFilledEnabled()) { + // + // final Drawable drawable = dataSet.getFillDrawable(); + // if (drawable != null) { + // + // drawFilledPath(c, surface, drawable); + // } else { + // + // drawFilledPath(c, surface, dataSet.getFillColor(), dataSet.getFillAlpha()); + // } + // } + // + this.mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); + this.mRenderPaint.setStyle(Style.STROKE); + // + // // draw the line (only if filled is disabled or alpha is below 255) + // if (!dataSet.isDrawFilledEnabled() || dataSet.getFillAlpha() < 255) + // c.drawPath(surface, mRenderPaint); + // + // MPPointF.recycleInstance(center); + // MPPointF.recycleInstance(pOut); + surface+=" Z" + let pathPaint:PathPaint=new PathPaint(); + pathPaint.set(this.mRenderPaint); + pathPaint.setCommands(surface) + if(dataSet.isDrawFilledEnabled()){ + pathPaint.setFill(dataSet.getFillColor()) + }else{ + pathPaint.setFill(0x01ffffff) + } + return byType==this.TYPE_PATH?[pathPaint]:pointTypeArr + } + + public drawValues():Paint[] { + let paints: Paint[] = []; + + let phaseX:number = this.mAnimator.getPhaseX(); + let phaseY:number = this.mAnimator.getPhaseY(); + + let sliceangle:number = this.radarChartMode.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + let factor:number = this.radarChartMode.getFactor(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + let pIcon:MPPointF = MPPointF.getInstance(0,0); + + let yoffset:number = Utils.convertDpToPixel(5); + let extraWidth=Utils.calcTextWidth(this.mValuePaint,this.radarChartMode.getYChartMax()+""); + for (let i = 0; i < this.radarChartMode.getData().getDataSetCount(); i++) { + + let dataSet:IRadarDataSet = this.radarChartMode.getData().getDataSetByIndex(i); + + // apply the text-styling defined by the DataSet + this.applyValueTextStyle(dataSet); + let iconsOffset:MPPointF = MPPointF.getInstance(null,null,dataSet.getIconsOffset()); + + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (let j = 0; j < dataSet.getEntryCount(); j++) { + + + let entry:RadarEntry = dataSet.getEntryForIndex(j); + Utils.getPosition( + center, + (entry.getY() - this.radarChartMode.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + this.radarChartMode.getRotationAngle(), + pOut); + + if ( this.radarChartMode.getData().getDataSets().get(i).isDrawValuesEnabled()) { + paints=paints.concat(this.drawValue( + dataSet.getValueFormatter(), + entry.getY(), + entry, + i, + pOut.x-extraWidth/2, + pOut.y - yoffset, + dataSet.getValueTextColor + (j))); + }else{ + paints=paints.concat(new TextPaint()) + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + // + // Drawable icon = entry.getIcon(); + // + // Utils.getPosition( + // center, + // (entry.getY()) * factor * phaseY + iconsOffset.y, + // sliceangle * j * phaseX + mChart.getRotationAngle(), + // pIcon); + // + // //noinspection SuspiciousNameCombination + // pIcon.y += iconsOffset.x; + // + // Utils.drawImage( + // c, + // icon, + // (int)pIcon.x, + // (int)pIcon.y, + // icon.getIntrinsicWidth(), + // icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(pIcon); + return paints + } + + public drawExtras():Paint[] { + return this.drawWeb(); + } + + protected drawWeb():Paint[] { + + let sliceangle:number =this.radarChartMode.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + let factor:number = this.radarChartMode.getFactor(); + let rotationangle = this.radarChartMode.getRotationAngle(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + + // draw the web lines that come from the center + this.mWebPaint.setStrokeWidth(this.radarChartMode.getWebLineWidth()); + this.mWebPaint.setColor(this.radarChartMode.getWebColor()); + this.mWebPaint.setAlpha(this.radarChartMode.getWebAlpha()); + + let linePaintArr:Paint[]=new Array(); + + const xIncrements :number= 1 + this.radarChartMode.getSkipWebLineCount(); + let maxEntryCount:number = this.radarChartMode.getData().getMaxEntryCountSet().getEntryCount(); + + let p:MPPointF = MPPointF.getInstance(0,0); + for (let i = 0; i < maxEntryCount; i += xIncrements) { + + Utils.getPosition( + center, + this.radarChartMode.getYRange() * factor, + sliceangle * i + rotationangle, + p); + let linePaint:LinePaint=new LinePaint(this.mWebPaint as LinePaint); + linePaint.setStartPoint([center.x,center.y]) + linePaint.setEndPoint([p.x, p.y]) + linePaintArr.push(linePaint); + } + MPPointF.recycleInstance(p); + + // draw the inner-web + this.mWebPaint.setStrokeWidth(this.radarChartMode.getWebLineWidthInner()); + this.mWebPaint.setColor(this.radarChartMode.getWebColorInner()); + this.mWebPaint.setAlpha(this.radarChartMode.getWebAlpha()); + + let labelCount = this.radarChartMode.getYAxis().mEntryCount; + + let p1out:MPPointF= MPPointF.getInstance(0,0); + let p2out:MPPointF = MPPointF.getInstance(0,0); + for (let j = 0; j < labelCount; j++) { + for (let i = 0; i < this.radarChartMode.getData().getEntryCount(); i++) { + + let r:number = (this.radarChartMode.getYAxis().mEntries[j] - this.radarChartMode.getYChartMin()) * factor; + + Utils.getPosition(center, r, sliceangle * i + rotationangle, p1out); + Utils.getPosition(center, r, sliceangle * (i + 1) + rotationangle, p2out); + + let linePaint:LinePaint=new LinePaint(this.mWebPaint as LinePaint); + linePaint.setStartPoint([p1out.x,p1out.y]) + linePaint.setEndPoint([p2out.x, p2out.y]) + linePaintArr.push(linePaint); + } + } + MPPointF.recycleInstance(p1out); + MPPointF.recycleInstance(p2out); + return linePaintArr; + } + + + public drawHighlighted(indices:Highlight[]):Paint[]{ + let paintData:Paint[]=[] + + let sliceangle:number = this.radarChartMode.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + let factor:number = this.radarChartMode.getFactor(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + + let radarData:RadarData = this.radarChartMode.getData(); + + for (let high of indices) { + let dataSet:IRadarDataSet= radarData.getDataSetByIndex(high.getDataSetIndex()); + + if (dataSet == null || !dataSet.isHighlightEnabled()) + continue; + + let e:RadarEntry = dataSet.getEntryForIndex(high.getDataIndex()); + if (!this.isInBoundsX(e, dataSet)) + continue; + + let y:number = (e.getY() - this.radarChartMode.getYChartMin()); + + Utils.getPosition(center, + y * factor * this.mAnimator.getPhaseY(), + sliceangle * high.getDataIndex() * this.mAnimator.getPhaseX() + this.radarChartMode.getRotationAngle(), + pOut); + + let content:string= Math.round(Number(e.getY().toFixed(1)))+"%"; + let textPaint:TextPaint=new TextPaint(); + let textWidth=Utils.calcTextWidth(textPaint,content)*2+10; + + textPaint.setTextSize(10) + textPaint.setText(content) + textPaint.setColor(Color.White) + textPaint.setWidth(textWidth+10) + textPaint.setHeight(10) + textPaint.setX(pOut.x-textWidth/2-5) + textPaint.setY(pOut.y-textWidth+2) + textPaint.setTextAlign(TextAlign.Center) + paintData.push(textPaint); + + let imagePaint:ImagePaint=new ImagePaint(); + imagePaint.setX(pOut.x-textWidth/2-5) + imagePaint.setY(pOut.y-textWidth-5) + imagePaint.setWidth(textWidth+10) + imagePaint.setHeight(textWidth) + paintData.push(imagePaint); + //high.setDraw(pOut.x, pOut.y); + + // draw the lines + //this.drawHighlightLines( pOut.x, pOut.y, dataSet); + + if (dataSet.isDrawHighlightCircleEnabled()) { + + if (!Number.isNaN(pOut.x) && !Number.isNaN(pOut.y)) { + + let strokeColor:number = dataSet.getHighlightCircleStrokeColor(); +// if (strokeColor == ColorTemplate.COLOR_NONE) { +// strokeColor = dataSet.getColor(0); +// } +// +// if (dataSet.getHighlightCircleStrokeAlpha() < 255) { +// strokeColor = ColorTemplate.colorWithAlpha(strokeColor, dataSet.getHighlightCircleStrokeAlpha()); +// } + + paintData=paintData.concat( this.drawHighlightCircle( + pOut, + px2vp(dataSet.getHighlightCircleInnerRadius()), + px2vp(dataSet.getHighlightCircleOuterRadius()), + dataSet.getHighlightCircleFillColor(), + strokeColor, + px2vp(dataSet.getHighlightCircleStrokeWidth()))); + } + } + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + return paintData + } + + protected mDrawHighlightCirclePathBuffer:string= ""; + public drawHighlightCircle( point:MPPointF, + innerRadius:number, + outerRadius:number, + fillColor:number, + strokeColor:number, + strokeWidth:number):Paint[]{ + + outerRadius = Utils.convertDpToPixel(outerRadius); + innerRadius = Utils.convertDpToPixel(innerRadius); + let paint:Paint[]=[] + if (fillColor != ColorTemplate.COLOR_NONE) { + let p:string = this.mDrawHighlightCirclePathBuffer; + let outerPaintCircle:CirclePaint=new CirclePaint() + outerPaintCircle.setX(point.x-outerRadius) + outerPaintCircle.setY(point.y-outerRadius) + outerPaintCircle.setWidth(outerRadius*2) + outerPaintCircle.setHeight(outerRadius*2) + //paint.push(outerPaintCircle) + if (innerRadius > 0) { + let innerPaintCir:CirclePaint=new CirclePaint() + innerPaintCir.setX(point.x-innerRadius) + innerPaintCir.setY(point.y-innerRadius) + innerPaintCir.setWidth(innerRadius*2) + innerPaintCir.setHeight(innerRadius*2) + // paint.push(innerPaintCir) + } + this.mHighlightCirclePaint.setColor(fillColor); + this.mHighlightCirclePaint.setStyle(Style.FILL); + } + + if (strokeColor != ColorTemplate.COLOR_NONE) { + + this.mHighlightCirclePaint.setFill(fillColor) + this.mHighlightCirclePaint.setStroke(strokeColor); + this.mHighlightCirclePaint.setStrokeWidth(strokeWidth); + + let strokePaintCir:CirclePaint=new CirclePaint(this.mHighlightCirclePaint) + strokePaintCir.setX(point.x-outerRadius) + strokePaintCir.setY(point.y-outerRadius) + strokePaintCir.setWidth(outerRadius*2) + strokePaintCir.setHeight(outerRadius*2) + paint.push(strokePaintCir) + } + + return paint + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/Renderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/Renderer.ets new file mode 100644 index 0000000..cd02855 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/Renderer.ets @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ViewPortHandler from '../utils/ViewPortHandler' +/** + * Abstract baseclass of all Renderers. + * + * @author Philipp Jahoda + */ +export default abstract class Renderer { + +/** + * the component that handles the drawing area of the chart and it's offsets + */ + protected mViewPortHandler: ViewPortHandler; + + constructor(viewPortHandler: ViewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRenderer.ets new file mode 100644 index 0000000..9f8b646 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRenderer.ets @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AxisRenderer from './AxisRenderer' +import {XAxis, XAxisPosition} from '../components/XAxis' +import ViewPortHandler from '../utils/ViewPortHandler' +import Transformer from '../utils/Transformer' +import Utils from '../utils/Utils' +import FSize from '../utils/FSize' +import MPPointF from '../utils/MPPointF' +import MPPointD from '../utils/MPPointD' +import LimitLine,{LimitLabelPosition}from '../components/LimitLine' +import {JArrayList} from '../utils/JArrayList'; +import MyRect from '../data/Rect'; +import Paint, {Style,LinePaint,TextPaint,PathPaint} from '../data/Paint' + +export default class XAxisRenderer extends AxisRenderer { + protected mXAxis: XAxis; + public yLeftLongestLabel:string="AAA"; + public yRightLongestLabel:string="AAA"; + constructor(viewPortHandler: ViewPortHandler, xAxis: XAxis, trans: Transformer) { + super(viewPortHandler, trans, xAxis); + this.mXAxis = xAxis; + this.mAxisLabelPaint.setColor(Color.Black); + this.mAxisLabelPaint.setTextAlign(TextAlign.Center); + this.mAxisLabelPaint.setTextSize(10); + } + + protected setupGridPaint() { + this.mGridPaint.setColor(this.mXAxis.getGridColor()); + this.mGridPaint.setStrokeWidth(this.mXAxis.getGridLineWidth()); + this.mGridPaint.setDashPathEffect(this.mXAxis.getGridDashPathEffect()); + } + + public computeAxis(min: number, max: number, inverted: boolean) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (this.mViewPortHandler.contentWidth() > 10 && !this.mViewPortHandler.isFullyZoomedOutX()) { + + var p1:MPPointD = this.mTrans.getValuesByTouchPoint(this.mViewPortHandler.contentLeft(), this.mViewPortHandler.contentTop()); + var p2:MPPointD = this.mTrans.getValuesByTouchPoint(this.mViewPortHandler.contentRight(),this.mViewPortHandler.contentTop()); + + if (inverted) { + + min =p2.x; + max =p1.x; + } else { + + min = p1.x; + max = p2.x; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + this.computeAxisValues(min, max); + } + + // @Override + protected computeAxisValues(min: number, max: number) { + super.computeAxisValues(min, max); + this.computeSize(); + } + + protected computeSize() { + + let longest = this.mXAxis.getLongestLabel(); + + this.mAxisLabelPaint.setTypeface(this.mXAxis.getTypeface()); + this.mAxisLabelPaint.setTextSize(this.mXAxis.getTextSize()); + + let labelSize: FSize = Utils.calcTextSize(this.mAxisLabelPaint, longest); + + let labelWidth = labelSize.width; + let labelHeight = Utils.calcTextHeight(this.mAxisLabelPaint, "Q"); + + let labelRotatedSize: FSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelWidth, + labelHeight, + this.mXAxis.getLabelRotationAngle()); + + + this.mXAxis.mLabelWidth = Math.round(labelWidth); + this.mXAxis.mLabelHeight = Math.round(labelHeight); + this.mXAxis.mLabelRotatedWidth = Math.round(labelRotatedSize.width); + this.mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); + + FSize.recycleInstance(labelRotatedSize); + FSize.recycleInstance(labelSize); + } + + public renderAxisLabels():Paint[] { + + if (!this.mXAxis.isEnabled() || !this.mXAxis.isDrawLabelsEnabled()) + return []; + + let yoffset = this.mXAxis.getYOffset(); + + this.mAxisLabelPaint.setTypeface(this.mXAxis.getTypeface()); + this.mAxisLabelPaint.setTextSize(this.mXAxis.getTextSize()); + this.mAxisLabelPaint.setColor(this.mXAxis.getTextColor()); + + let pointF: MPPointF = MPPointF.getInstance(0, 0); + if (this.mXAxis.getPosition() == XAxisPosition.TOP) { + pointF.x = 0.5; + pointF.y = 1.0; + return this.drawLabels(this.mViewPortHandler.contentTop() - yoffset, pointF); + + } else if (this.mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { + pointF.x = 0.5; + pointF.y = 1.0; + return this.drawLabels(this.mViewPortHandler.contentTop() + yoffset + this.mXAxis.mLabelRotatedHeight, pointF); + + } else if (this.mXAxis.getPosition() == XAxisPosition.BOTTOM) { + pointF.x = 0.5; + pointF.y = 0.0; + return this.drawLabels(this.mViewPortHandler.contentBottom() + yoffset, pointF); + + } else if (this.mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { + pointF.x = 0.5; + pointF.y = 0.0; + return this.drawLabels(this.mViewPortHandler.contentBottom() - yoffset - this.mXAxis.mLabelRotatedHeight, pointF); + + } else { // BOTH SIDED + let paintArr:Paint[]=new Array(); + pointF.x = 0.5; + pointF.y = 1.0; + paintArr=paintArr.concat(this.drawLabels(this.mViewPortHandler.contentTop() - yoffset, pointF)); + pointF.x = 0.5; + pointF.y = 0.0; + paintArr=paintArr.concat(this.drawLabels(this.mViewPortHandler.contentBottom() + yoffset, pointF)); + return paintArr; + } + MPPointF.recycleInstance(pointF); + } + public renderAxisLine():Paint[] { + + if (!this.mXAxis.isDrawAxisLineEnabled() || !this.mXAxis.isEnabled()) + return []; + + this.mAxisLinePaint.setColor(this.mXAxis.getAxisLineColor()); + this.mAxisLinePaint.setStrokeWidth(this.mXAxis.getAxisLineWidth()); + this.mAxisLinePaint.setDashPathEffect(this.mXAxis.getAxisLineDashPathEffect()); + let linePaint:LinePaint= this.mAxisLinePaint as LinePaint; + let leftTextWidth=this.getLeftYTextWidth(); + let leftRightWidth=this.getRightYTextWidth(); + if (this.mXAxis.getPosition() == XAxisPosition.TOP + || this.mXAxis.getPosition() == XAxisPosition.TOP_INSIDE + || this.mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + console.log("mViewPortHandler:"+this.mViewPortHandler.contentLeft()+" leftTextWidth:"+leftTextWidth); + console.log("mViewPortHandler:"+this.mViewPortHandler.contentRight()+" leftTextWidth:"+leftRightWidth); + + linePaint.setStartPoint([this.mViewPortHandler.contentLeft()+leftTextWidth,this.mViewPortHandler.contentTop()]) + linePaint.setEndPoint([this.mViewPortHandler.contentRight()-leftRightWidth,this.mViewPortHandler.contentTop()]) + } + + if (this.mXAxis.getPosition() == XAxisPosition.BOTTOM + || this.mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE + || this.mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + linePaint.setStartPoint([this.mViewPortHandler.contentLeft()+leftTextWidth,this.mViewPortHandler.contentBottom()]) + linePaint.setEndPoint([this.mViewPortHandler.contentRight()-leftRightWidth,this.mViewPortHandler.contentBottom()]) + linePaint.setStrokeWidth(1.5) + } + return [linePaint] + } + + /** + * draws the x-labels on the specified y-position + * + * @param pos + */ + protected drawLabels(pos: number, anchor: MPPointF):Paint[] { + + let labelRotationAngleDegrees = this.mXAxis.getLabelRotationAngle(); + let centeringEnabled = this.mXAxis.isCenterAxisLabelsEnabled(); + + let positions: number[] = new Array(this.mXAxis.mEntryCount * 2); + + for (var i = 0; i < positions.length; i += 2) { + + // only fill x values + if (centeringEnabled) { + positions[i] = this.mXAxis.mCenteredEntries[i / 2]; + } else { + positions[i] = this.mXAxis.mEntries[i / 2]; + } + } + + this.mTrans.pointValuesToPixel(positions); + let labelPaint:Paint[]=new Array(); + for (let i = 0; i 1) { + let width = Utils.calcTextWidth(this.mAxisLabelPaint, label); + + if (width > this.mViewPortHandler.offsetRight() * 2 + && x + width > this.mViewPortHandler.getChartWidth()) + x -= width / 2; + + // avoid clipping of the first + } else if (i == 0) { + + let width = Utils.calcTextWidth(this.mAxisLabelPaint, label); + x += width / 2; + } + } + + labelPaint.push(this.drawLabel(label, this.getXRelativeValue(x), pos, anchor, labelRotationAngleDegrees)); + } + if(x-this.mAxis.getAxisMinimum()==0){ + let label = this.mXAxis.getValueFormatter().getFormattedValue(this.mXAxis.mEntries[i / 2], this.mXAxis); + labelPaint.push(this.drawLabel(label, this.getXRelativeValue(x), pos, anchor, labelRotationAngleDegrees)); + } + } + return labelPaint; + } + + protected drawLabel(formattedLabel: string, x: number, y: number, anchor: MPPointF, angleDegrees: number):Paint { + var xResult=this.calcXLeftOffset(x); + var labelPaint=new TextPaint(this.mAxisLabelPaint as TextPaint); + return Utils.drawXAxisValue(formattedLabel, xResult, y,labelPaint , anchor, angleDegrees); + } + + protected mRenderGridLinesPath:string = ""; + protected mRenderGridLinesBuffer = new Array(2); + + public renderGridLines():Paint[] { + + if (!this.mXAxis.isDrawGridLinesEnabled() || !this.mXAxis.isEnabled()) + return []; + this.mGridPaint.setColor(this.mXAxis.getGridColor()); + this.mGridPaint.setStrokeWidth(this.mXAxis.getGridLineWidth()); + this.mGridPaint.setDashPathEffect(this.mXAxis.getGridDashPathEffect()); + + if (this.mRenderGridLinesBuffer.length != this.mAxis.mEntryCount * 2) { + this.mRenderGridLinesBuffer = new Array(this.mXAxis.mEntryCount * 2); + } + let positions = this.mRenderGridLinesBuffer; + + for (var i = 0; i < positions.length; i += 2) { + positions[i] = this.mXAxis.mEntries[i / 2]; + positions[i + 1] = this.mXAxis.mEntries[i / 2]; + } + + this.mTrans.pointValuesToPixel(positions); + + this.setupGridPaint(); + + var gridLinePath:string = this.mRenderGridLinesPath; + gridLinePath=""; + var girdLinePaint:Paint[]=new Array(); + + for (var i = 0; i < positions.length; i += 2) { + let x = positions[i]; + if (this.mViewPortHandler.isInBoundsX(this.getXRelativeValue(x)/this.getAxisPercent())) { + if((this.getXRelativeValue(x)/this.getAxisPercent()) < (this.mViewPortHandler.contentRight()-this.getRightYTextWidth())){ + var pathPaintF:Paint= this.drawGridLine(this.getXRelativeValue(x), positions[i + 1], gridLinePath); + girdLinePaint.push( pathPaintF); + } + } + } + return girdLinePaint; + } + + protected mGridClippingRect:MyRect = new MyRect(); + + public getGridClippingRect(): MyRect{ + this.mGridClippingRect.set(this.mViewPortHandler.getContentRect().left,this.mViewPortHandler.getContentRect().top + ,this.mViewPortHandler.getContentRect().right,this.mViewPortHandler.getContentRect().bottom); + this.mGridClippingRect.inset(-this.mAxis.getGridLineWidth(), 0, -this.mAxis.getGridLineWidth(),0,); + return this.mGridClippingRect; + } + + /** + * Draws the grid line at the specified position using the provided path. + * + * @param c + * @param x + * @param y + * @param gridLinePath + */ + protected drawGridLine(x: number, y: number,path:string):Paint { + let xResult:number=this.calcXLeftOffset(x) + path="M"+Utils.convertDpToPixel(xResult)+" "+Utils.convertDpToPixel(this.mViewPortHandler.contentBottom())+"L"+Utils.convertDpToPixel(xResult)+" "+Utils.convertDpToPixel(this.mViewPortHandler.contentTop()) + var pathPaint:PathPaint=new PathPaint(this.mGridPaint as PathPaint); + pathPaint.setCommands(path); + return pathPaint + + } + + protected mRenderLimitLinesBuffer = new Array(2); + protected mLimitLineClippingRect = new MyRect(); + + /** + * retrurn the LimitLines draw data. + * + * @param c + */ + public renderLimitLines():Paint[]{ + + let limitLines: JArrayList = this.mXAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0||!this.mAxis.isDrawLimitLinesBehindDataEnabled()) + return []; + + var limitPaint:Paint[]=new Array(); + + let position = this.mRenderLimitLinesBuffer; + position[0] = 0; + position[1] = 0; + + for (var i = 0; i < limitLines.size(); i++) { + + let l: LimitLine = limitLines.get(i); + if (!l.isEnabled()) + continue; + + this.mLimitLineClippingRect.set(this.mViewPortHandler.getContentRect().left,this.mViewPortHandler.getContentRect().top + ,this.mViewPortHandler.getContentRect().right,this.mViewPortHandler.getContentRect().bottom); + this.mGridClippingRect.inset(-this.mAxis.getGridLineWidth(), 0, -this.mAxis.getGridLineWidth(),0,); + + position[0] = this.getXRelativeValue(l.getLimit()); + position[1] = 0; + + this.mTrans.pointValuesToPixel(position); + limitPaint.push( this.renderLimitLineLine( l, position)); + limitPaint.push(this.renderLimitLineLabel(l, position, 2 + l.getYOffset())); + } + return limitPaint; + } + + mLimitLineSegmentsBuffer = new Array(4); + private mLimitLinePath = new Paint(); + + public renderLimitLineLine(limitLine: LimitLine, position: number[]):Paint { + let leftXResult=this.calcXLeftOffset(position[0]) + this.mLimitLineSegmentsBuffer[0] = leftXResult; + this.mLimitLineSegmentsBuffer[1] = this.mViewPortHandler.contentTop(); + this.mLimitLineSegmentsBuffer[2] = leftXResult; + this.mLimitLineSegmentsBuffer[3] = this.mViewPortHandler.contentBottom(); + + this.mLimitLinePaint.setStyle(Style.STROKE); + this.mLimitLinePaint.setColor(limitLine.getLineColor()); + this.mLimitLinePaint.setStrokeWidth(limitLine.getLineWidth()); + this.mLimitLinePaint.setDashPathEffect(limitLine.getDashPathEffect()); + let path:string="M"+Utils.convertDpToPixel(this.mLimitLineSegmentsBuffer[0])+" "+Utils.convertDpToPixel(this.mLimitLineSegmentsBuffer[1])+"L"+Utils.convertDpToPixel(this.mLimitLineSegmentsBuffer[2])+" "+Utils.convertDpToPixel(this.mLimitLineSegmentsBuffer[3]) + + let pathPaint:PathPaint=new PathPaint(this.mLimitLinePaint as PathPaint); + pathPaint.setCommands(path); + return pathPaint; + } + public renderLimitLineLabel(limitLine: LimitLine, position: number[], yOffset: number):Paint{ + + let label = limitLine.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && label.length > 0 ){ + this.mLimitLinePaint.setStyle(limitLine.getTextStyle()); + this.mLimitLinePaint.setDashPathEffect(null); + this.mLimitLinePaint.setColor(limitLine.getTextColor()); + this.mLimitLinePaint.setStrokeWidth(0.5); + this.mLimitLinePaint.setTextSize(limitLine.getTextSize()); + + let textPaint:TextPaint=new TextPaint(); + textPaint.setTextSize(limitLine.getTextSize()); + textPaint.setStyle(limitLine.getTextStyle()); + textPaint.setColor(limitLine.getTextColor()); + textPaint.setText(label); + + let xOffset = limitLine.getLineWidth() + limitLine.getXOffset(); + + let labelPosition: LimitLabelPosition = limitLine.getLabelPosition(); + + if (labelPosition == LimitLabelPosition.RIGHT_TOP) { + textPaint.setTextAlign(TextAlign.Start); + textPaint.x=this.calcXLeftOffset(position[0])+xOffset; + textPaint.y=this.mViewPortHandler.contentTop()+yOffset + } else if (labelPosition == LimitLabelPosition.RIGHT_BOTTOM) { + let labelLineHeight = Utils.calcTextHeight(this.mLimitLinePaint, label); + textPaint.setTextAlign(TextAlign.Start); + textPaint.x=this.calcXLeftOffset(position[0])+xOffset; + textPaint.y=this.mViewPortHandler.contentBottom()-yOffset-labelLineHeight; + } else if (labelPosition == LimitLabelPosition.LEFT_TOP) { + + textPaint.setTextAlign(TextAlign.End); + let labelLineWidth = Utils.calcTextWidth(this.mLimitLinePaint, label); + textPaint.x=this.calcXLeftOffset(position[0])-xOffset-labelLineWidth; + textPaint.y=this.mViewPortHandler.contentTop()+yOffset; + } else { + textPaint.setTextAlign(TextAlign.End); + let labelLineWidth = Utils.calcTextWidth(this.mLimitLinePaint, label); + let labelLineHeight = Utils.calcTextHeight(this.mLimitLinePaint, label); + textPaint.x=this.calcXLeftOffset(position[0])-xOffset-labelLineWidth; + textPaint.y=this.mViewPortHandler.contentBottom()-yOffset-labelLineHeight; + // c.drawText(label, position[0] - xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } + return textPaint; + } + } + public calcXLeftOffset(xVlaus:number):number{ + var xResult=xVlaus/this.getAxisPercent()+this.mViewPortHandler.contentLeft()+this.getLeftYTextWidth() + return xResult; + } + public getAxisPercent():number{ + return (this.mAxis.getAxisMaximum()-(this.mAxis.getAxisMinimum()>=0?0:this.mAxis.getAxisMinimum()))/this.getLineRange() + } + public getLeftYTextWidth():number{ + return Utils.calcTextWidth(this.mAxisLabelPaint,this.yLeftLongestLabel); + } + public getRightYTextWidth():number{ + return Utils.calcTextWidth(this.mAxisLabelPaint,this.yRightLongestLabel); + } + public getLineRange():number{ + return this.mViewPortHandler.contentRight()-this.mViewPortHandler.contentLeft()-this.getLeftYTextWidth()-this.getRightYTextWidth(); + } + public getXRelativeValue(x:number):number{ + return x-(this.mAxis.getAxisMinimum()>=0?0:this.mAxis.getAxisMinimum()); + } +} + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRendererRadarChart.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRendererRadarChart.ets new file mode 100644 index 0000000..d024089 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/XAxisRendererRadarChart.ets @@ -0,0 +1,83 @@ +import MyRect from '../data/Rect'; +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {XAxis} from '../components/XAxis'; +import MPPointF from '../utils/MPPointF'; +import Utils from '../utils/Utils'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import XAxisRenderer from './XAxisRenderer'; +import Paint, { TextPaint } from '../data/Paint'; +import RadarData from '../data/RadarData'; +import RadarChartMode from '../data/RadarChartMode' +export default class XAxisRendererRadarChart extends XAxisRenderer { + + public radarChartMode:RadarChartMode=new RadarChartMode() + constructor(radarChartMode:RadarChartMode) { + super(radarChartMode.handler, radarChartMode.xAxis, null); + this.radarChartMode = radarChartMode; + } + + public renderAxisLabels():Paint[] { + let paintArr:Paint[]=new Array(); + /* if (!this.mXAxis.isEnabled() || !this.mXAxis.isDrawLabelsEnabled()) + return;*/ + + let labelRotationAngleDegrees:number = this.mXAxis.getLabelRotationAngle(); + let drawLabelAnchor:MPPointF = MPPointF.getInstance(0.5, 0.25); + + this.mAxisLabelPaint.setTypeface(this.mXAxis.getTypeface()); + this.mAxisLabelPaint.setTextSize(this.mXAxis.getTextSize()); + this.mAxisLabelPaint.setColor(this.mXAxis.getTextColor()); + + let sliceangle:number = this.radarChartMode.getSliceAngle(); + + let factor:number = this.radarChartMode.getFactor(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + let extraWidth=Utils.calcTextWidth(this.mAxisLabelPaint,this.mXAxis.longest); + let r:number = this.radarChartMode.getYRange() * factor+extraWidth/2+3; + for (let i = 0; i < this.radarChartMode.getData().getMaxEntryCountSet().getEntryCount(); i++) { + + let label:string = this.mXAxis.getValueFormatter().getFormattedValue(i, this.mXAxis); + let angle:number = (sliceangle * i + this.radarChartMode.getRotationAngle()) % 360; + Utils.getPosition(center, r, angle, pOut); + if (!this.mXAxis.isEnabled() || !this.mXAxis.isDrawLabelsEnabled()) { + paintArr.push(new TextPaint()); + }else{ + var labelPaint = new TextPaint(this.mAxisLabelPaint as TextPaint); + paintArr.push(Utils.drawXAxisValue(label, pOut.x, (pOut.y - this.mXAxis.mLabelRotatedHeight / 2), labelPaint, + drawLabelAnchor, labelRotationAngleDegrees)); + } + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(drawLabelAnchor); + return paintArr; + } + + /** + * XAxis LimitLines on RadarChart not yet supported. + * + * @param c + */ + public renderLimitLines():Paint[] { + // this space intentionally left blank + return []; + } + +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRenderer.ets new file mode 100644 index 0000000..e32ad60 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRenderer.ets @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MyRect from '../data/Rect'; +import YAxis, {AxisDependency, YAxisLabelPosition} from '../components/YAxis' +import Paint, {Style, LinePaint,PathPaint,TextPaint} from '../data/Paint' +import Utils from '../utils/Utils' +import AxisRenderer from '../renderer/AxisRenderer' +import LimitLine, {LimitLabelPosition} from '../components/LimitLine' +import {JArrayList} from '../utils/JArrayList'; +import ViewPortHandler from '../utils/ViewPortHandler' +import Transformer from '../utils/Transformer' +import MPPointD from '../utils/MPPointD' + +export default class YAxisRenderer extends AxisRenderer { + protected mYAxis: YAxis; + protected mZeroLinePaint: Paint; + + constructor(viewPortHandler: ViewPortHandler, yAxis: YAxis, trans: Transformer) { + super(viewPortHandler, trans, yAxis) + this.mYAxis = yAxis; + + if (viewPortHandler != null) { + this.mAxisLabelPaint.setColor(Color.Black); + this.mAxisLabelPaint.setTextSize(10); + + this.mZeroLinePaint = new PathPaint(); + this.mZeroLinePaint.setColor(Color.Gray); + this.mZeroLinePaint.setStrokeWidth(1); + this.mZeroLinePaint.setStyle(Style.STROKE); + } + } + + /** + * draws the y-axis labels to the screen + */ + public renderAxisLabels(): Paint[] { + + if (!this.mYAxis.isEnabled() || !this.mYAxis.isDrawLabelsEnabled()) + return []; + + var positions: number[] = this.getTransformedPositions(); + + this.mAxisLabelPaint.setTypeface(this.mYAxis.getTypeface()); + this.mAxisLabelPaint.setTextSize(this.mYAxis.getTextSize()); + this.mAxisLabelPaint.setColor(this.mYAxis.getTextColor()); + + var xOffset = this.mYAxis.getXOffset(); + var yOffset = Utils.calcTextHeight(this.mAxisLabelPaint, "A") + this.mYAxis.getYOffset(); + + var dependency: AxisDependency = this.mYAxis.getAxisDependency(); + var labelPosition: YAxisLabelPosition = this.mYAxis.getLabelPosition(); + + var xPos: number = 0; + + if (dependency == AxisDependency.LEFT) { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + this.mAxisLabelPaint.setTextAlign(TextAlign.End); + xPos = this.mViewPortHandler.offsetLeft() - xOffset; + } else { + this.mAxisLabelPaint.setTextAlign(TextAlign.Start); + xPos = this.mViewPortHandler.offsetLeft() + xOffset; + } + } else { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + this.mAxisLabelPaint.setTextAlign(TextAlign.Start); + xPos = this.mViewPortHandler.contentRight(); + } else { + this.mAxisLabelPaint.setTextAlign(TextAlign.End); + xPos = this.mViewPortHandler.contentRight() - xOffset; + } + } + + return this.drawYLabels(xPos, positions, yOffset); + } + + public renderAxisLine(): Paint[] { + + if (!this.mYAxis.isEnabled() || !this.mYAxis.isDrawAxisLineEnabled()) + return []; + + this.mAxisLinePaint.setColor(this.mYAxis.getAxisLineColor()); + this.mAxisLinePaint.setStrokeWidth(this.mYAxis.getAxisLineWidth()); + + this.mViewPortHandler.getContentRect().right = this.mViewPortHandler.contentRight() - Utils.calcTextWidth(this.mAxisLabelPaint,this.mAxis.getLongestLabel()); + if (this.mYAxis.getAxisDependency() == AxisDependency.LEFT) { + (this.mAxisLinePaint as LinePaint).setStartPoint([this.mViewPortHandler.contentLeft()+Utils.calcTextWidth(this.mAxisLabelPaint,this.mAxis.getLongestLabel()), this.mViewPortHandler.contentTop()]); + (this.mAxisLinePaint as LinePaint).setEndPoint([this.mViewPortHandler.contentLeft()+Utils.calcTextWidth(this.mAxisLabelPaint,this.mAxis.getLongestLabel()), this.mViewPortHandler.contentBottom()]); + } else { + (this.mAxisLinePaint as LinePaint).setStartPoint([this.mViewPortHandler.contentRight(), this.mViewPortHandler.contentTop()]); + (this.mAxisLinePaint as LinePaint).setEndPoint([this.mViewPortHandler.contentRight(), this.mViewPortHandler.contentBottom()]); + } + + return [this.mAxisLinePaint]; + } + + + /** + * draws the y-labels on the specified x-position + * + * @param fixedPosition + * @param positions + */ + protected drawYLabels(fixedPosition: number, positions: number[], offset: number): Paint[] { + var paints = []; + const fromIndex = this.mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + const to = this.mYAxis.isDrawTopYLabelEntryEnabled() + ? this.mYAxis.mEntryCount + : (this.mYAxis.mEntryCount - 1); + + var xOffset: number = this.mYAxis.getLabelXOffset(); + + // draw + for (var i = fromIndex; i < to; i++) { + var newLabelPaint = new TextPaint(this.mAxisLabelPaint as TextPaint); + var text: string = this.mYAxis.getFormattedLabel(i); + newLabelPaint.setText(text); + newLabelPaint.setX(fixedPosition + xOffset); + let interval = ((this.mAxisLinePaint as LinePaint).endPoint[1] - (this.mAxisLinePaint as LinePaint).startPoint[1]) / (to - 1); + let lastNumber = this.mAxis.mEntries[this.mAxis.mEntries.length - 1]; + let topOffset = ((this.mAxis.getAxisMaximum() - lastNumber) / (lastNumber - this.mAxis.mEntries[this.mAxis.mEntries.length - 2]) * interval) + let bottomOffset = ((this.mAxis.mEntries[0] - this.mAxis.mAxisMinimum) / (this.mAxis.mEntries[1] - this.mAxis.mEntries[0]) * interval) + interval = ((this.mAxisLinePaint as LinePaint).endPoint[1] - (this.mAxisLinePaint as LinePaint).startPoint[1] - topOffset - bottomOffset) / (to - 1) + let nowOffset = offset; + if(this.mYAxis.isInverted()){ + nowOffset += bottomOffset; + if(!this.mYAxis.isDrawBottomYLabelEntryEnabled()){ + newLabelPaint.setY(interval * (to - i - 1) + nowOffset); + }else{ + newLabelPaint.setY(interval * i + nowOffset); + } + }else{ + nowOffset += topOffset; + if(!this.mYAxis.isDrawBottomYLabelEntryEnabled()){ + newLabelPaint.setY(interval * i + nowOffset); + }else{ + newLabelPaint.setY(interval * (to - i - 1) + nowOffset); + } + } + if(newLabelPaint.textAlign == TextAlign.End){ + newLabelPaint.setWidth((this.mAxisLinePaint as LinePaint).startPoint[0]); + } + paints.push(newLabelPaint) + } + return paints; + } + + protected mRenderGridLinesPath: string = ""; + + public renderGridLines():Paint[] { + + var paints = [] + const fromIndex = this.mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + const to = this.mYAxis.isDrawTopYLabelEntryEnabled() + ? this.mYAxis.mEntryCount + : (this.mYAxis.mEntryCount - 1); + + if (!this.mYAxis.isEnabled()) + return []; + + if (this.mYAxis.isDrawGridLinesEnabled()) { + var positions: number[] = this.getTransformedPositions(); + + this.mGridPaint.setColor(this.mYAxis.getGridColor()); + this.mGridPaint.setStrokeWidth(this.mYAxis.getGridLineWidth()); + this.mGridPaint.setDashPathEffect(this.mYAxis.getGridDashPathEffect()); + + var gridLinePath: string = this.mRenderGridLinesPath; + gridLinePath = ""; + + // var yOffset = Utils.calcTextHeight(this.mAxisLabelPaint, "A") / 2.5 + this.mYAxis.getYOffset() + // draw the grid + for (var i = 0; i < positions.length; i += 2) { + var newGridPaint = new PathPaint(this.mGridPaint as PathPaint); + gridLinePath = ""; + var interval = ((this.mAxisLinePaint as LinePaint).endPoint[1] - (this.mAxisLinePaint as LinePaint).startPoint[1]) + / (to - 1); + let lastNumber = this.mAxis.mEntries[this.mAxis.mEntries.length - 1]; + let topOffset = ((this.mAxis.getAxisMaximum() - lastNumber) / (lastNumber - this.mAxis.mEntries[this.mAxis.mEntries.length - 2]) * interval) + let bottomOffset = ((this.mAxis.mEntries[0] - this.mAxis.mAxisMinimum) / (this.mAxis.mEntries[1] - this.mAxis.mEntries[0]) * interval) + interval = ((this.mAxisLinePaint as LinePaint).endPoint[1] - (this.mAxisLinePaint as LinePaint).startPoint[1] - topOffset - bottomOffset) / (to - 1) + positions[ i + 1] = interval * (Math.floor(i / 2)) + this.mViewPortHandler.offsetTop() + topOffset; + // + newGridPaint.setCommands(this.linePath(gridLinePath, i, positions)); + if(this.mYAxis.getGridDashPathEffect() != null){ + // @ts-ignore + newGridPaint.setStrokeDashArray(this.mYAxis.getGridDashPathEffect().dash) + // @ts-ignore + newGridPaint.setStrokeDashOffset(this.mYAxis.getGridDashPathEffect().offset) + } + paints.push(newGridPaint) + } + + } + + if (this.mYAxis.isDrawZeroLineEnabled()) { + paints.push(this.drawZeroLine()); + } + + return paints; + + } + + protected mGridClippingRect: MyRect = new MyRect(); + + public getGridClippingRect(): MyRect { + this.mGridClippingRect.set(this.mViewPortHandler.getContentRect().left, this.mViewPortHandler.getContentRect().top + , this.mViewPortHandler.getContentRect().right, this.mViewPortHandler.getContentRect().bottom); + + this.mGridClippingRect.inset(0, -this.mAxis.getGridLineWidth(), 0, -this.mAxis.getGridLineWidth()); + return this.mGridClippingRect; + } + + /** + * Calculates the path for a grid line. + * + * @param p + * @param i + * @param positions + * @return + */ + protected linePath(p: string, i: number, positions: number[]): string { + p = "M" + Utils.convertDpToPixel((this.mAxisLinePaint as LinePaint).startPoint[0]) + " " + Utils.convertDpToPixel(positions[i + 1]) + " L" + Utils.convertDpToPixel(this.mViewPortHandler.contentRight()) + " " + Utils.convertDpToPixel(positions[i + 1]) + " Z"; + return p; + } + + protected mGetTransformedPositionsBuffer: number[] = new Array(2); + /** + * Transforms the values contained in the axis entries to screen pixels and returns them in form of a float array + * of x- and y-coordinates. + * + * @return + */ + protected getTransformedPositions(): number[] { + + if (this.mGetTransformedPositionsBuffer.length != this.mYAxis.mEntryCount * 2) { + this.mGetTransformedPositionsBuffer = new Array(this.mYAxis.mEntryCount * 2); + } + var positions: number[] = this.mGetTransformedPositionsBuffer; + + for (var i = 0; i < positions.length; i += 2) { + // only fill y values, x values are not needed for y-labels + positions[i + 1] = this.mYAxis.mEntries[i / 2]; + } + + this.mTrans.pointValuesToPixel(positions); + return positions; + } + + protected mDrawZeroLinePath: string = ""; + protected mZeroLineClippingRect: MyRect = new MyRect(); + + /** + * Draws the zero line. + */ + protected drawZeroLine(): Paint { + + this.mZeroLineClippingRect.set(this.mViewPortHandler.getContentRect().left,this.mViewPortHandler.getContentRect().top + ,this.mViewPortHandler.getContentRect().right,this.mViewPortHandler.getContentRect().bottom); + this.mZeroLineClippingRect.inset(0, -this.mYAxis.getZeroLineWidth(), 0, -this.mYAxis.getZeroLineWidth()); + + // draw zero line + var pos:MPPointD = this.mTrans.getPixelForValues(0, 0); + + this.mZeroLinePaint.setColor(this.mYAxis.getZeroLineColor()); + this.mZeroLinePaint.setStrokeWidth(this.mYAxis.getZeroLineWidth()); + + var zeroLinePath: string = this.mDrawZeroLinePath; + zeroLinePath = "M"+this.mViewPortHandler.contentLeft()+" "+pos.y+" L"+this.mViewPortHandler.contentRight()+" "+pos.y; + (this.mZeroLinePaint as PathPaint).setCommands(zeroLinePath) + return this.mZeroLinePaint; + } + + protected mRenderLimitLines: string = ""; + protected mRenderLimitLinesBuffer: number[] = new Array(2); + protected mLimitLineClippingRect: MyRect = new MyRect(); + + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + public renderLimitLines():Paint[] { + + var limitLines: JArrayList = this.mYAxis.getLimitLines(); + var paints = [] + + if (limitLines == null || limitLines.size() <= 0) + return []; + + var pts: number[] = this.mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + var limitLinePath: string = this.mRenderLimitLines; + limitLinePath = ""; + + for (var i = 0; i < limitLines.size(); i++) { + + var l: LimitLine = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + this.mLimitLineClippingRect.set(this.mViewPortHandler.getContentRect().left,this.mViewPortHandler.getContentRect().top, + this.mViewPortHandler.getContentRect().right,this.mViewPortHandler.getContentRect().bottom); + this.mLimitLineClippingRect.inset(0, -l.getLineWidth(), 0, -l.getLineWidth()); + + var newPathLine = new PathPaint(); + newPathLine.set(this.mLimitLinePaint) + newPathLine.setStyle(Style.STROKE); + newPathLine.setColor(l.getLineColor()); + newPathLine.setStrokeWidth(l.getLineWidth()); + newPathLine.setDashPathEffect(l.getDashPathEffect()); + + pts[1] = l.getLimit(); + + this.mTrans.pointValuesToPixel(pts); + + var interval = ((this.mAxisLinePaint as LinePaint).endPoint[1] - (this.mAxisLinePaint as LinePaint).startPoint[1]); + let offset = interval - interval * ((l.getLimit()- this.mAxis.mAxisMinimum) / (this.mAxis.mAxisMaximum - this.mAxis.mAxisMinimum)); + pts[1] = offset + this.mViewPortHandler.offsetTop(); + + limitLinePath = "M"+Utils.convertDpToPixel((this.mAxisLinePaint as LinePaint).startPoint[0])+" "+Utils.convertDpToPixel(pts[1])+" L"+Utils.convertDpToPixel(this.mViewPortHandler.contentRight())+" "+Utils.convertDpToPixel(pts[1]); + + (newPathLine as PathPaint).setCommands(limitLinePath) + limitLinePath = ""; + paints.push(newPathLine) + + var label: string = l.getLabel(); + // + // if drawing the limit-value label is enabled + if (label != null && label != "") { + + let textPaint = new TextPaint(); + textPaint.set(this.mLimitLinePaint); + textPaint.setStyle(l.getTextStyle()); + textPaint.setDashPathEffect(null); + textPaint.setColor(l.getTextColor()); + textPaint.setTypeface(l.getTypeface()); + textPaint.setStrokeWidth(0.5); + textPaint.setTextSize(l.getTextSize()); + + const labelLineHeight: number = Utils.calcTextHeight(textPaint, label); + var xOffset: number = Utils.calcTextWidth(textPaint,l.getLabel()) + l.getXOffset() + 4; + var yOffset: number = l.getLineWidth() + labelLineHeight + l.getYOffset(); + + const position: LimitLabelPosition = l.getLabelPosition(); + + if (position == LimitLabelPosition.RIGHT_TOP) { + + textPaint.setTextAlign(TextAlign.End); + textPaint.setText(label); + textPaint.setX(this.mViewPortHandler.contentRight() - xOffset); + textPaint.setY(pts[1] - yOffset); + paints.push(textPaint); + + } else if (position == LimitLabelPosition.RIGHT_BOTTOM) { + + textPaint.setTextAlign(TextAlign.End); + textPaint.setText(label); + textPaint.setX(this.mViewPortHandler.contentRight() - xOffset); + textPaint.setY(pts[1] + l.getYOffset()); + paints.push(textPaint); + + } else if (position == LimitLabelPosition.LEFT_TOP) { + + textPaint.setTextAlign(TextAlign.Start); + textPaint.setText(label); + textPaint.setX(this.mViewPortHandler.contentLeft() + xOffset); + textPaint.setY(pts[1] - yOffset); + paints.push(textPaint); + + } else { + + textPaint.setTextAlign(TextAlign.Start); + textPaint.setText(label); + textPaint.setX(this.mViewPortHandler.offsetLeft() + xOffset); + textPaint.setY(pts[1] + l.getYOffset()); + paints.push(textPaint); + } + } + } + + return paints; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRendererRadarChart.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRendererRadarChart.ets new file mode 100644 index 0000000..666f214 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/YAxisRendererRadarChart.ets @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint from '../data/Paint'; +import RadarChartMode from '../data/RadarChartMode' +import LimitLine from '../components/LimitLine'; +import YAxis from '../components/YAxis'; +import MPPointF from '../utils/MPPointF'; +import Utils from '../utils/Utils'; +import ViewPortHandler from '../utils/ViewPortHandler'; +import YAxisRenderer from '../renderer/YAxisRenderer'; +import {JArrayList} from '../utils/JArrayList'; +import RadarData from '../data/RadarData'; +import MyRect from '../data/Rect'; + +export default class YAxisRendererRadarChart extends YAxisRenderer { + + public radarChartMode:RadarChartMode=new RadarChartMode() + constructor(radarChartMode:RadarChartMode) { + super(radarChartMode.handler, radarChartMode.yAxis, null); + this.radarChartMode=radarChartMode; + } + + protected computeAxisValues( min:number, max:number):void{ + + let yMin:number = min; + let yMax:number = max; + + let labelCount:number = this.mAxis.getLabelCount(); + let range:number = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || range== Infinity) { + this.mAxis.mEntries = []; + this.mAxis.mCenteredEntries = []; + this.mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + let rawInterval:number = range / labelCount; + let interval:number = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (this.mAxis.isGranularityEnabled()) + interval = interval < this.mAxis.getGranularity() ? this.mAxis.getGranularity() : interval; + + // Normalize interval + let intervalMagnitude:number = Utils.roundToNextSignificant(Math.pow(10, Math.log10(interval))); + let intervalSigDigit = interval / intervalMagnitude; + if (intervalSigDigit > 5) { + // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + } + + let centeringEnabled:boolean = this.mAxis.isCenterAxisLabelsEnabled(); + let n = centeringEnabled ? 1 : 0; + + // force label count + if (this.mAxis.isForceLabelsEnabled()) { + + let step:number = range / (labelCount - 1); + this.mAxis.mEntryCount = labelCount; + + if (this.mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + this.mAxis.mEntries = new Array(labelCount); + } + + let v:number = min; + + for (let i = 0; i < labelCount; i++) { + this.mAxis.mEntries[i] = v; + v += step; + } + + n = labelCount; + + // no forced count + } else { + + let first:number = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if (centeringEnabled) { + first -= interval; + } + + let last:number = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + let f:number; + let i:number; + + if (interval != 0.0) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + + n++; + + this.mAxis.mEntryCount = n; + + if (this.mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + this.mAxis.mEntries = new Array(n); + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + this.mAxis.mEntries[i] = f; + } + } + + // set decimals + if (interval < 1) { + this.mAxis.mDecimals = Math.ceil(-Math.log10(interval)); + } else { + this.mAxis.mDecimals = 0; + } + + if (centeringEnabled) { + + if (this.mAxis.mCenteredEntries.length < n) { + this.mAxis.mCenteredEntries = new Array(n); + } + + let offset:number = (this.mAxis.mEntries[1] - this.mAxis.mEntries[0]) / 2; + + for (let i = 0; i < n; i++) { + this.mAxis.mCenteredEntries[i] = this.mAxis.mEntries[i] + offset; + } + } + this.mAxis.mAxisMinimum = this.mAxis.mEntries[0]; + this.mAxis.mAxisMaximum = this.mAxis.mEntries[n-1]; + this.mAxis.mAxisRange = Math.abs(this.mAxis.mAxisMaximum - this.mAxis.mAxisMinimum); + } + + public renderAxisLabels():Paint[] { + + if (!this.mYAxis.isEnabled() || !this.mYAxis.isDrawLabelsEnabled()) + return; + + this.mAxisLabelPaint.setTypeface(this.mYAxis.getTypeface()); + this.mAxisLabelPaint.setTextSize(this.mYAxis.getTextSize()); + this.mAxisLabelPaint.setColor(this.mYAxis.getTextColor()); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + let factor:number = this.radarChartMode.getFactor(); + + const myFrom:number = this.mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + const to = this.mYAxis.isDrawTopYLabelEntryEnabled() + ? this.mYAxis.mEntryCount + : (this.mYAxis.mEntryCount - 1); + + const xOffset = this.mYAxis.getLabelXOffset(); + + for (let j = myFrom; j < to; j++) { + + let r:number = (this.mYAxis.mEntries[j] - this.mYAxis.mAxisMinimum) * factor; + + Utils.getPosition(center, r, this.radarChartMode.getRotationAngle(), pOut); + + let label:string =this.mYAxis.getFormattedLabel(j); + + //c.drawText(label, pOut.x + xOffset, pOut.y, mAxisLabelPaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + return + } + + private mRenderLimitLinesPathBuffer:string = '' + public renderLimitLines():Paint[]{ + + let limitLines:JArrayList = this.mYAxis.getLimitLines(); + + if (limitLines == null) + return; + + let sliceangle:number = this.radarChartMode.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + let factor:number = this.radarChartMode.getFactor(); + + let center:MPPointF = this.radarChartMode.getCenterOffsets(); + let pOut:MPPointF = MPPointF.getInstance(0,0); + for (let i = 0; i < limitLines.size(); i++) { + + let l:LimitLine = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + this.mLimitLinePaint.setColor(l.getLineColor()); + this.mLimitLinePaint.setDashPathEffect(l.getDashPathEffect()); + this.mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + + let r:number = (l.getLimit() - this.radarChartMode.getYChartMin()) * factor; + + let limitPath:string = this.mRenderLimitLinesPathBuffer; + //limitPath.reset(); + + + for (let j = 0; j < this.radarChartMode.getData().getMaxEntryCountSet().getEntryCount(); j++) { + + Utils.getPosition(center, r, sliceangle * j + this.radarChartMode.getRotationAngle(), pOut); + +// if (j == 0) +// limitPath.moveTo(pOut.x, pOut.y); +// else +// limitPath.lineTo(pOut.x, pOut.y); + } + + //c.drawPath(limitPath, mLimitLinePaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + return + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/scatter/IShapeRenderer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/scatter/IShapeRenderer.ets new file mode 100644 index 0000000..cd57805 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/renderer/scatter/IShapeRenderer.ets @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint from '../../data/Paint'; +import IScatterDataSet from '../../interfaces/datasets/IScatterDataSet'; +import ViewPortHandler from '../../utils/ViewPortHandler'; + +export default interface IShapeRenderer +{ + /** + * Renders the provided ScatterDataSet with a shape. + * + * @param c Canvas object for drawing the shape + * @param dataSet The DataSet to be drawn + * @param viewPortHandler Contains information about the current state of the view + * @param posX Position to draw the shape at + * @param posY Position to draw the shape at + * @param renderPaint Paint object used for styling and drawing + */ + renderShape(paints:Paint[], dataSet:IScatterDataSet, viewPortHandler:ViewPortHandler, + posX:number, posY:number,renderPaint:Paint):void; +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ArrayUtils.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ArrayUtils.ets new file mode 100644 index 0000000..1377bef --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ArrayUtils.ets @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class ArrayUtils{ + static copyOfRange(arr:number[],fromIndex:number,toIndex:number):number[]{ + + //需将copya的类型指定为any,这样才能够使其能够任意添加属性 + var copya:number[]; + //最关键的是注意 下面 当a中有方法时,json.parse(json.stringify(xx))是无效的 + for(var i=0;i>24) & 0xff; + } + + public static red(color: number): number { + return (color>>16) & 0xff; + } + + public static green(color: number): number { + return (color>>8) & 0xff; + } + + public static blue(color: number): number { + return (color) & 0xff; + } + + public static RGBToHSV(r,g,b,hsv:number[]){ + var h = 0, s = 0, v = 0; + let arr = [r,g,b]; + arr.sort(function (a, b) { + return a - b; + }) + var max = arr[2] + var min = arr[0]; + v = max / 255; + if (max === 0) { + s = 0; + } else { + s = 1 - (min / max); + } + if (max === min) { + h = 0;//事实上,max===min的时候,h无论为多少都无所谓 + } else if (max === r && g >= b) { + h = 60 * ((g - b) / (max - min)) + 0; + } else if (max === r && g < b) { + h = 60 * ((g - b) / (max - min)) + 360 + } else if (max === g) { + h = 60 * ((b - r) / (max - min)) + 120 + } else if (max === b) { + h = 60 * ((r - g) / (max - min)) + 240 + } + h = Math.floor(h); + s = Math.floor(s * 100); + v = Math.floor(v * 100); + hsv.splice(0,hsv.length); + hsv.push(h,s,v) + } + + public static HSVToColor(alpha: number,arr:number[]): number{ + var h = arr[0], s = arr[1], v = arr[2]; + s = s / 100; + v = v / 100; + var r = 0, g = 0, b = 0; + var i = Math.floor((h / 60) % 6); + var f = h / 60 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + switch (i) { + case 0: + r = v; g = t; b = p; + break; + case 1: + r = q; g = v; b = p; + break; + case 2: + r = p; g = v; b = t; + break; + case 3: + r = p; g = q; b = v; + break; + case 4: + r = t; g = p; b = v; + break; + case 5: + r = v; g = p; b = q; + break; + default: + break; + } + r = Math.floor(r * alpha) + g = Math.floor(g * alpha) + b = Math.floor(b * alpha) + return Color.rgb(r,g,b) + } + +} + +/** + * Class that holds predefined color integer arrays (e.g. + * ColorTemplate.VORDIPLOM_COLORS) and convenience methods for loading colors + * from resources. + * + * @author Philipp Jahoda + */ +class ColorTemplate { + build(){} + /** + * an "invalid" color that indicates that no color is set + */ + public static COLOR_NONE: number = 0x00112233; + + /** + * this "color" is used for the Legend creation and indicates that the next + * form should be skipped + */ + public static COLOR_SKIP: number = 0x00112234; + + /** + * THE COLOR THEMES ARE PREDEFINED (predefined color integer arrays), FEEL + * FREE TO CREATE YOUR OWN WITH AS MANY DIFFERENT COLORS AS YOU WANT + */ + public static LIBERTY_COLORS: number[] = [ + Color.rgb(207, 248, 246), Color.rgb(148, 212, 212), Color.rgb(136, 180, 187), + Color.rgb(118, 174, 175), Color.rgb(42, 109, 130) + ]; + public static JOYFUL_COLORS: number[] = [ + Color.rgb(217, 80, 138), Color.rgb(254, 149, 7), Color.rgb(254, 247, 120), + Color.rgb(106, 167, 134), Color.rgb(53, 194, 209) + ]; + public static PASTEL_COLORS: number[] = [ + Color.rgb(64, 89, 128), Color.rgb(149, 165, 124), Color.rgb(217, 184, 162), + Color.rgb(191, 134, 134), Color.rgb(179, 48, 80) + ]; + public static COLORFUL_COLORS: number[] = [ + Color.rgb(193, 37, 82), Color.rgb(255, 102, 0), Color.rgb(245, 199, 0), + Color.rgb(106, 150, 31), Color.rgb(179, 100, 53) + ]; + public static VORDIPLOM_COLORS: number[] = [ + Color.rgb(192, 255, 140), Color.rgb(255, 247, 140), Color.rgb(255, 208, 140), + Color.rgb(140, 234, 255), Color.rgb(255, 140, 157) + ]; + public static MATERIAL_COLORS: number[] = [ + 0x2ecc71, 0xf1c40f, 0xe74c3c, 0x3498db + ]; + + /** + * Converts the given hex-color-string to rgb. + * + * @param hex + * @return + */ + public static rgb(hex: string): number { + var color: number = Number(hex.replace("#", "")); + var r: number = (color >> 16) & 0xFF; + var g: number = (color >> 8) & 0xFF; + var b: number = (color >> 0) & 0xFF; + return Color.rgb(r, g, b); + } + + /** + * + * @return + */ + public static getHoloBlue(): number { + return Color.rgb(51,181,229) + } + + /** + * Sets the alpha component of the given color. + * + * @param color + * @param alpha 0 - 255 + * @return + */ + public static colorWithAlpha(color: number, alpha: number): number{ + return (color & 0xffffff) | ((alpha & 0xff) << 24); + } + + /** + * turn an array of resource-colors (contains resource-id integers) into an + * array list of actual color integers + * + * @param r + * @param colors an integer array of resource id's of colors + * @return + */ + public static createColors(colors?: number[]): JArrayList { + var result: JArrayList = new JArrayList(); + for (var i = 0;i < colors.length; i++) { + result.add(colors[i]); + } + return result; + } + + /** + * from Color.rgb() + * @param r + * @param g + * @param b + */ + public static colorRgb(r: number, g: number, b: number): number{ + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + /** + * from Color.argb() + * @param alpha + * @param red + * @param green + * @param blue + */ + public static argb(alpha:number,red:number,green:number,blue:number):number{ + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + + /** + * from Color red() + * @param color + */ + public static red(color:number):number{ + return (color >> 16) & 0xFF; + } + + /** + * from Color green() + * @param color + */ + public static green(color:number):number{ + return (color >> 8) & 0xFF; + } + + /** + * from Color blue() + * @param color + */ + public static blue(color:number):number{ + return color & 0xFF; + } + + +} + +export { Color, ColorTemplate }; +export default ColorTemplate \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/FSize.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/FSize.ets new file mode 100644 index 0000000..29268c9 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/FSize.ets @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ObjectPool} from './ObjectPool' +import {Poolable} from './Poolable' +/** + * Class for describing width and height dimensions in some arbitrary + */ +export default class FSize extends Poolable { + + // TODO : Encapsulate width & height + + public width: number; + public height: number; + public static pool: ObjectPool =ObjectPool.create(256, new FSize(0,0)).setReplenishPercentage(0.5); + + instantiate(): Poolable{ + return new FSize(0, 0); + } + + public static getInstance(width: number, height: number): FSize{ + let result: FSize = FSize.pool.get(); + result.width = width; + result.height = height; + return result; + } + + public static recycleInstance(instance: FSize) { + this.pool.recycle(instance); + } + + public static recycleInstances(instances: Array) { + this.pool.recycleArray(instances); + } + + public constructor(width: number, height: number) { + super() + this.width = width; + this.height = height; + } + + public equals( obj:Object):boolean { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof FSize) { + var other:FSize= obj as FSize; + return this.width == other.width && this.height == other.height; + } + return false; + } + + public toString():string { + return this.width + "x" + this.height; + } + + /** + * {@inheritDoc} + */ + /* + @Override + public int hashCode() { + return Float.floatToIntBits(width) ^ Float.floatToIntBits(height); + }*/ +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Fill.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Fill.ets new file mode 100644 index 0000000..45e90db --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Fill.ets @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint,{Style,ImagePaint,RectPaint,BackGroundPaint,PathPaint} from '../data/Paint'; +import MyRect from '../data/Rect'; +import Utils from '../utils/Utils'; +import { ColorStop } from '../data/LineDataSet'; + +export enum Type{ + EMPTY, COLOR, LINEAR_GRADIENT, DRAWABLE +} + +export enum MyDirection{ + DOWN, UP, RIGHT, LEFT +} +export default class Fill{ + + /** + * the type of fill + */ + private mType:Type = Type.EMPTY; + + /** + * the color that is used for filling + */ + private mColor:number = null; + + private mFinalColor:number= null; + + /** + * the drawable to be used for filling + */ + protected mDrawable:ImagePaint/*mDrawable*/; + + private mGradientColors:number[]; + + private mGradientPositions:number[]; + + /** + * transparency used for filling + */ + private mAlpha:number = 255; + + constructor (color?:number,startColor?:number,endColor?:number,gradientColors?:number[],gradientPositions?:number[],drawable?:ImagePaint){ + if(color!=null&&color!=undefined){ + this.mType = Type.COLOR; + this.mColor = color; + this.calculateFinalColor(); + return + } + if(startColor!=null&&startColor!=undefined&&endColor!=null&&endColor!=undefined){ + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = [startColor, endColor]; + this.mGradientPositions = [0.0, 1.0]; + return + } + if(gradientColors!=null&&gradientColors!=undefined){ + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = gradientColors; + this.mGradientPositions = gradientPositions; + return + } + if(drawable!=null&&drawable!=undefined){ + this.mType = Type.DRAWABLE; + this.mDrawable = drawable; + return + } + } + + public getType():Type{ + return this.mType; + } + + public setType(type:Type):void{ + this.mType = type; + } + + public getColor():number{ + return this.mColor; + } + + public setColor(color:number):void{ + this.mColor = color; + this.calculateFinalColor(); + } + + public getGradientColors():number[] { + return this.mGradientColors; + } + public getGradientPositions():number[] { + return this.mGradientPositions; + } + + public setGradientPositions(positions:number[]):void{ + this.mGradientPositions = positions; + } + + public setGradientColors( colors?:number[],startColor?:number,endColor?:number):void{ + if(colors!=null&&colors!=undefined){ + this.mGradientColors = colors; + return + } + this.mGradientColors =[startColor, endColor]; + } + + public getAlpha():number{ + return this.mAlpha; + } + + public setAlpha(alpha:number):void{ + this.mAlpha = alpha; + this.calculateFinalColor(); + } + + private calculateFinalColor():void{ + + if (this.mColor == null){ + this.mFinalColor = null; + } else{ + + let alpha:number= Math.floor(((this.mColor >> 24) / 255.0) * (this.mAlpha / 255.0) * 255.0); + this.mFinalColor = (alpha << 24) | (this.mColor & 0xffffff); + } + } + + public fillRect(paint:RectPaint,left:number, top:number, right:number, bottom:number, + gradientDirection:MyDirection):Paint{ + switch (this.mType){ + + case Type.EMPTY: + return; + case Type.COLOR: + if (this.mFinalColor == null) return; + if (this.isClipPathSupported()){ + let rectB:BackGroundPaint=new BackGroundPaint(); + rectB.setBackgroundColor(this.mFinalColor); + return rectB; + }else { + // save + let previous:Style= paint.getStyle(); + let previousColor:number = (paint.getColor() as number); + + // set + paint.setStyle(Style.FILL); + paint.setColor(this.mFinalColor); + let rectP:RectPaint=new RectPaint(paint); + rectP.setStartPoint([left,top]); + rectP.setWidth(right-left); + rectP.setHeight(bottom-top); + + // restore + paint.setColor(previousColor); + paint.setStyle(previous); + return rectP; + } + + case Type.LINEAR_GRADIENT: + if (this.mGradientColors == null) return; + let gradient:RectPaint=new RectPaint(); +// let leftResult:number=(gradientDirection == MyDirection.RIGHT? right: gradientDirection == MyDirection.LEFT ? left: left) + let leftResult:number = left; +// let topResult:number=(gradientDirection == MyDirection.UP? bottom: gradientDirection == MyDirection.DOWN? top: top) + let topResult = top; +// let rightResult:number=(gradientDirection == MyDirection.RIGHT ? left : gradientDirection == MyDirection.LEFT? right : left) + let rightResult:number=right; +// let bottomResult:number=(gradientDirection == MyDirection.UP? top: gradientDirection == MyDirection.DOWN ? bottom: top) + let bottomResult = bottom; + gradient.setX(leftResult) + gradient.setY(topResult) + gradient.setWidth(rightResult-leftResult); + gradient.setHeight(bottomResult - topResult); + gradient.setStyle(paint.getStyle()); + gradient.setColor(paint.getColor()); + let colorArr:Array =new Array() + + for(let i=0;i =new Array() + for(let i=0;i= 8; + } + + private ensureClipPathSupported():void{ + if (Utils.getSDKInt() < 8) + { + throw new Error("Fill-drawables not (yet) supported below API level 18, " + + "this code was run on API level " + Utils.getSDKInt() + "."); + } + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JArrayList.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JArrayList.ets new file mode 100644 index 0000000..a255c94 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JArrayList.ets @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 说明: 在 JAVA中 List是一套interface接口,继承自Collection +// AbstractList 实现了List接口 , +// 最终ArrayList/ArrayQueue等,继承自AbstractList +// 因此这里可能需要进行修改 +import { JList } from './JList' + +export class JArrayList implements JList { + + dataSouce:Array; + listSize:number; // 列表的大小 + pos:number; // 列表中当前的位置 + + constructor() { + this.dataSouce = []; + this.listSize = 0; // 列表的大小 + this.pos = 0; // 列表中当前的位置 + } + + /** + * 在列表的末尾添加新元素 + * @param {*} element 要添加的元素 + */ + append(element:T) { + this.dataSouce[this.listSize++] = element; + } + + add(element:T) { + this.append(element) + return this + } + + addAll(newArray:JList) { + let len = newArray.length() + for (let i = 0; i < len; i++) { + this.append(newArray.get(i)) + } + } + + /** + * 在列表中插入一个元素 + * @param {*} element + * @param {*} after + */ + insert(element:T) { + this.dataSouce.push(element); + this.listSize++; + } + + /** + * 在列表中移除一个元素 + * @param {*} element 要删除的元素 + */ + remove(element:T) { + // 查找当前元素的索引 + const index = this.dataSouce.indexOf(element); + if (index >= 0) { + this.dataSouce.splice(index, 1); + this.listSize--; + return true; + } + return false; + } + + /** + * 判断给定的值是否在列表中 + */ + contains(element:T) { + return this.dataSouce.indexOf(element) > -1; + } + indexOf(element:T):number { + return this.dataSouce.indexOf(element); + } + + /** + * 将列表的当前位置设移动到第一个元素 + */ + front() { + this.pos = 0; + } + + /** + * 将列表的当前位置移动到最后一个元素 + */ + end() { + this.pos = this.listSize - 1; + } + + /** + * 将当前位置前移一位 + */ + prev() { + if (this.pos > 0) { + --this.pos; + } + } + + /** + * 将当前位置向后移一位 + */ + next() { + if (this.pos <= (this.listSize - 1)) { + ++this.pos; + } + } + + /** + * 返回列表的当前位置 + */ + currPos() { + return this.pos; + } + + /** + * 将当前位置移动到指定位置 + * @param {*} position + */ + moveTo(position) { + this.pos = position; + } + + /** + * 返回当前位置的元素 + */ + getElement() { + return this.dataSouce[this.pos]; + } + /** + * 返回指定位置的元素 + */ + get(pos:number) { + return this.dataSouce[pos]; + } + at(pos:number):T { + return this.get(pos); + } + + /** + * 清楚列表中的元素 + */ + clear() { + delete this.dataSouce; + this.dataSouce = []; + this.listSize = 0; + this.pos = 0; + } + + /** + * 列表的长度 + */ + length():number { + return this.listSize; + } + size():number { + return this.listSize; + } + isEmpty():boolean { + return this.listSize == 0; + } + + /** + * 显示当前列表的元素 + */ + toArray(a?:T[]):T[] { + if (a == null) { + return this.dataSouce + } + if (a.length < this.length()) { + return new Array(); + } + } + /** + * 显示当前列表的元素 + */ + toString(interval?:string):string { + if (interval == null) { + return this.dataSouce.join(''); + } + return this.dataSouce.join(interval); + } +} + diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JList.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JList.ets new file mode 100644 index 0000000..7089559 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/JList.ets @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class JList{ + + dataSouce:Array; + + listSize:number; // 列表的大小 + + pos:number; // 列表中当前的位置 + + constructor() { + this.dataSouce = []; + this.listSize = 0; // 列表的大小 + this.pos = 0; // 列表中当前的位置 + } + /** + * 在列表的末尾添加新元素 + * @param {*} element 要添加的元素 + */ + append(element:T) { + this.dataSouce[this.listSize++] = element; + } + + /** + * 在列表中插入一个元素 + * @param {*} element + * @param {*} after + */ + insert(element:T) { + this.dataSouce.push(element); + this.listSize++; + } + + /** + * 在列表中移除一个元素 + * @param {*} element 要删除的元素 + */ + remove(element:T){ + // 查找当前元素的索引 + const index = this.dataSouce.indexOf(element); + if (index >= 0) { + this.dataSouce.splice(index, 1); + this.listSize--; + return true; + } + return false; + } + + /** + * 判断给定的值是否在列表中 + */ + contains(element:T) { + return this.dataSouce.indexOf(element) > -1; + } + + /** + * 将列表的当前位置设移动到第一个元素 + */ + front() { + this.pos = 0; + } + + /** + * 将列表的当前位置移动到最后一个元素 + */ + end() { + this.pos = this.listSize - 1; + } + + /** + * 将当前位置前移一位 + */ + prev() { + if (this.pos > 0) { + --this.pos; + } + } + + /** + * 将当前位置向后移一位 + */ + next() { + if (this.pos <= (this.listSize - 1)) { + ++this.pos; + } + } + + /** + * 返回列表的当前位置 + */ + currPos() { + return this.pos; + } + + /** + * 将当前位置移动到指定位置 + * @param {*} position + */ + moveTo(position) { + this.pos = position; + } + + /** + * 返回当前位置的元素 + */ + getElement() { + return this.dataSouce[this.pos]; + } + + /** + * 返回指定位置的元素 + */ + get(pos:number) { + return this.dataSouce[pos]; + } + + /** + * 清楚列表中的元素 + */ + clear() { + delete this.dataSouce; + this.dataSouce = []; + this.listSize = 0; + this.pos = 0; + } + + /** + * 列表的长度 + */ + length():number { + return this.listSize; + } + + /** + * 显示当前列表的元素 + */ + toString(interval?:string):string { + if (interval == null) { + return this.dataSouce.join(''); + } + return this.dataSouce.join(interval); + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointD.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointD.ets new file mode 100644 index 0000000..047f56d --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointD.ets @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Poolable} from './Poolable' +import {ObjectPool} from './ObjectPool' +import {JArrayList} from './JArrayList' + +/** + * Point encapsulating two double values. + * + * @author Philipp Jahoda + */ +export default class MPPointD extends Poolable { + private static pool: ObjectPool =ObjectPool.create(64, new MPPointD(0, 0)).setReplenishPercentage(0.5); + + public static getInstance(x: number, y: number): MPPointD{ + var result: MPPointD = this.pool.get(); + result.x = x; + result.y = y; + return result; + } + + public static recycleInstance(instance: MPPointD) { + this.pool.recycle(instance); + } + + // public static recycleInstances(instances:JArrayList){ + // this.pool.recycle(instances); + // } + + public instantiate(): Poolable{ + return new MPPointD(0, 0); + } + + public x: number; + public y: number; + + private constructor(x: number, y: number) { + super() + this.x = x; + this.y = y; + } + + /** + * returns a string representation of the object + */ + public toString(): string { + return "MPPointD, x: " + this.x + ", y: " + this.y; + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointF.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointF.ets new file mode 100644 index 0000000..73781b8 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/MPPointF.ets @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ObjectPool} from './ObjectPool' +import {Poolable} from './Poolable' + +export default class MPPointF extends Poolable { + private static pool: ObjectPool =ObjectPool.create(32, new MPPointF(0, 0)).setReplenishPercentage(0.5); + public x: number; + public y: number; + + /*static { + pool = ObjectPool.create(32, new MPPointF(0,0)); + pool.setReplenishPercentage(0.5f); + }*/ + + /* public MPPointF() { + }*/ + + public constructor(x?: number, y?: number) { + super() + this.x = x; + this.y = y; + } + + public static getInstance(x ?: number, y ?: number, copy ?: MPPointF): MPPointF{ + if (copy != null) { + let result: MPPointF = this.pool.get(); + result.x = copy.x; + result.y = copy.y; + return result; + } + if (x != null && y != null) { + let result: MPPointF = this.pool.get(); + result.x = x; + result.y = y; + return result; + } + return this.pool.get(); + } + + /* public static MPPointF getInstance() { + return pool.get(); + } + + public static MPPointF getInstance(MPPointF copy) { + MPPointF result = pool.get(); + result.x = copy.x; + result.y = copy.y; + return result; + }*/ + + public static recycleInstance(instance: MPPointF) { + this.pool.recycle(instance); + } + + public static recycleInstances(instances: Array) { + MPPointF.pool.recycleArray(instances); + } + + /*public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + */ + /** + * Return a new point from the data in the specified parcel. + */ + /* + public MPPointF createFromParcel(Parcel in) { + MPPointF r = new MPPointF(0,0); + r.my_readFromParcel(in); + return r; + } + + */ + /** + * Return an array of rectangles of the specified size. + */ + /* + public MPPointF[] newArray(int size) { + return new MPPointF[size]; + } + };*/ + + /** + * Set the point's coordinates from the data stored in the specified + * parcel. To write a point to a parcel, call writeToParcel(). + * + * @param in The parcel to read the point's coordinates from + */ + /* public void my_readFromParcel(Parcel in) { + x = in.readFloat(); + y = in.readFloat(); + }*/ + + public getX(): number{ + return this.x; + } + + public getY(): number{ + return this.y; + } + + instantiate(): Poolable { + return new MPPointF(0, 0); + } +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Matrix.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Matrix.ets new file mode 100644 index 0000000..66a821a --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Matrix.ets @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MyRect from '../data/Rect' + +/** + * 仅用于translate 和 scale,rotate、skew未移植 + */ +export default class Matrix { + public static MSCALE_X: number = 0; //!< use with getValues/setValues + public static MSKEW_X: number = 1; //!< use with getValues/setValues + public static MTRANS_X: number = 2; //!< use with getValues/setValues + public static MSKEW_Y: number = 3; //!< use with getValues/setValues + public static MSCALE_Y: number = 4; //!< use with getValues/setValues + public static MTRANS_Y: number = 5; //!< use with getValues/setValues + public static MPERSP_0: number = 6; //!< use with getValues/setValues + public static MPERSP_1: number = 7; //!< use with getValues/setValues + public static MPERSP_2: number = 8; //!< use with getValues/setValues + + private data: number[] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]; + + public reset() { + this.data.splice(0, this.data.length) + this.data[Matrix.MSCALE_X] = 1.0; + this.data[Matrix.MSCALE_Y] = 1.0; + this.data[Matrix.MPERSP_2] = 1.0; + this.data[Matrix.MTRANS_X] = 0.0; + this.data[Matrix.MTRANS_Y] = 0.0; + } + + public set(matrix: Matrix) { + this.data[Matrix.MSCALE_X] = matrix.getValues()[Matrix.MSCALE_X] + this.data[Matrix.MSKEW_X] = matrix.getValues()[Matrix.MSKEW_X] + this.data[Matrix.MTRANS_X] = matrix.getValues()[Matrix.MTRANS_X] + this.data[Matrix.MSKEW_Y] = matrix.getValues()[Matrix.MSKEW_Y] + this.data[Matrix.MSCALE_Y] = matrix.getValues()[Matrix.MSCALE_Y] + this.data[Matrix.MTRANS_Y] = matrix.getValues()[Matrix.MTRANS_Y] + this.data[Matrix.MPERSP_0] = matrix.getValues()[Matrix.MPERSP_0] + this.data[Matrix.MPERSP_1] = matrix.getValues()[Matrix.MPERSP_1] + this.data[Matrix.MPERSP_2] = matrix.getValues()[Matrix.MPERSP_2] + } + + public getValues(): number[]{ + return this.data; + } + + public setValues(values: number[]) { + this.data = values; + } + + public postScale(scaleX: number, scaleY: number, centerX?: number, centerY?: number) { + this.data[Matrix.MSCALE_X] *= scaleX; + this.data[Matrix.MSCALE_Y] *= scaleY; + this.data[Matrix.MTRANS_X] *= scaleX; + this.data[Matrix.MTRANS_Y] *= scaleY; + if (centerX != null && centerX != undefined) { + this.data[Matrix.MTRANS_X] += -centerX; + } + if (centerY != null && centerY != undefined) { + this.data[Matrix.MTRANS_Y] += -centerY; + } + } + + public setScale(scaleX: number, scaleY: number, centerX?: number, centerY?: number) { + this.data[Matrix.MSCALE_X] = scaleX; + this.data[Matrix.MSCALE_Y] = scaleY; + if (centerX != null && centerX != undefined) { + this.data[Matrix.MTRANS_X] += -centerX * scaleX; + }else{ + this.data[Matrix.MTRANS_X] = 0 + } + if (centerY != null && centerY != undefined) { + this.data[Matrix.MTRANS_Y] += -centerY * scaleY; + }else{ + this.data[Matrix.MTRANS_Y] = 0 + } + } + + public postTranslate(dx: number, dy: number) { + if(!isNaN(dx) && dx != undefined){ + this.data[Matrix.MTRANS_X] += dx; + } + if(!isNaN(dy) && dy != undefined){ + this.data[Matrix.MTRANS_Y] += dy; + } + } + + public setTranslate(dx: number, dy: number) { + if(!isNaN(dx) && dx != undefined){ + this.data[Matrix.MTRANS_X] = dx; + } + if(!isNaN(dy) && dy != undefined){ + this.data[Matrix.MTRANS_Y] = dy; + } + this.data[Matrix.MSCALE_X] = 1 + this.data[Matrix.MSCALE_Y] = 1 + } + + public postConcat(matrix:Matrix){ + var values = matrix.getValues(); + this.postScale(values[Matrix.MSCALE_X],values[Matrix.MSCALE_Y]); + this.postTranslate(values[Matrix.MTRANS_X],values[Matrix.MTRANS_Y]); +// this.data[Matrix.MTRANS_X] = values[Matrix.MTRANS_X] * this.data[Matrix.MSCALE_X] +// this.data[Matrix.MSCALE_X] *= values[Matrix.MSCALE_X] +// this.data[Matrix.MSCALE_Y] *= values[Matrix.MSCALE_Y] + } + + public mapPoints(pts: number[]) { + this.checkValue() + for(var i = 0;i < pts.length;i++){ + if(i % 2 == 0){ //x轴 + pts[i] = pts[i] * this.data[Matrix.MSCALE_X] + this.data[Matrix.MTRANS_X] + }else{ //y轴 + pts[i] = pts[i] * this.data[Matrix.MSCALE_Y] + this.data[Matrix.MTRANS_Y] + } + } + } + + public mapRect(rect: MyRect){ + var x: number = this.data[Matrix.MTRANS_X]; + var y: number = this.data[Matrix.MTRANS_Y]; + rect.set(rect.left + x, rect.top + y, rect.right + x, rect.bottom + y); + } + + public invert(matrix:Matrix){ + var values: number[] = matrix.getValues(); + values[Matrix.MSCALE_X] = 1 / (this.data[Matrix.MSCALE_X] / 1) + values[Matrix.MSCALE_Y] = 1 / (this.data[Matrix.MSCALE_Y] / 1) + values[Matrix.MTRANS_X] = -this.data[Matrix.MTRANS_X] * values[Matrix.MSCALE_X] + values[Matrix.MTRANS_Y] = -this.data[Matrix.MTRANS_Y] * values[Matrix.MSCALE_Y] + matrix.setValues(values); + return matrix; + } + + private checkValue(){ + for(var i = 0;i < this.data.length;i++){ + if(isNaN(this.data[i]) || this.data[i] == undefined){ + if(i == Matrix.MSCALE_X || i == Matrix.MSCALE_Y || i == Matrix.MPERSP_2){ + this.data[i] = 1 + }else{ + this.data[i] = 0 + } + } + } + } + +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ObjectPool.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ObjectPool.ets new file mode 100644 index 0000000..17b27f1 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ObjectPool.ets @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-nocheck + +import {Poolable} from './Poolable' +/** + * An object pool for recycling of object instances extending Poolable. + * + * + * Cost/Benefit : + * Cost - The pool can only contain objects extending Poolable. + * Benefit - The pool can very quickly determine if an object is elligable for storage without iteration. + * Benefit - The pool can also know if an instance of Poolable is already stored in a different pool instance. + * Benefit - The pool can grow as needed, if it is empty + * Cost - However, refilling the pool when it is empty might incur a time cost with sufficiently large capacity. Set the replenishPercentage to a lower number if this is a concern. + */ +export abstract class ObjectPool { + private static ids = 0; + private poolId: number; + private desiredCapacity: number; + private objects: Object[]; + private objectsPointer: number; + private modelObject: T; + private replenishPercentage: number; + + /** + * Returns the id of the given pool instance. + * + * @return an integer ID belonging to this pool instance. + */ + public getPoolId(): number{ + return this.poolId; + } + + /** + * Returns an ObjectPool instance, of a given starting capacity, that recycles instances of a given Poolable object. + * + * @param withCapacity A positive integer value. + * @param object An instance of the object that the pool should recycle. + * @return + */ + public static create(withCapacity: number, object: Poolable): ObjectPool{ + let result = new ObjectPool(withCapacity, object); + result.poolId = this.ids; + this.ids++; + + return result; + } + + constructor(withCapacity: number, object: T) { + if (withCapacity <= 0) { + throw new Error("Object Pool must be instantiated with a capacity greater than 0!"); + } + this.desiredCapacity = withCapacity; + this.objects = new Array(this.desiredCapacity); + this.objectsPointer = 0; + this.modelObject = object; + this.replenishPercentage = 1.0; + this.refillPool(); + } + + /** + * Set the percentage of the pool to replenish on empty. Valid values are between + * 0.00f and 1.00f + * + * @param percentage a value between 0 and 1, representing the percentage of the pool to replenish. + */ + public setReplenishPercentage(percentage: number):ObjectPool{ + let p = percentage; + if (p > 1) { + p = 1; + } + else if (p < 0) { + p = 0; + } + this.replenishPercentage = p; + return this; + } + + public getReplenishPercentage(): number{ + return this.replenishPercentage; + } + + /*private refillPool() { + this.refillPool(this.replenishPercentage); + } +*/ + private refillPool(percentage?: number) { + + if (percentage == null) { + percentage = this.replenishPercentage + } + + let portionOfCapacity = (this.desiredCapacity * percentage); + + if (portionOfCapacity < 1) { + portionOfCapacity = 1; + } else if (portionOfCapacity > this.desiredCapacity) { + portionOfCapacity = this.desiredCapacity; + } + + for (var i = 0; i < portionOfCapacity; i++) { + this.objects[i] = this.modelObject.instantiate(); + } + this.objectsPointer = portionOfCapacity - 1; + } + + /** + * Returns an instance of Poolable. If get() is called with an empty pool, the pool will be + * replenished. If the pool capacity is sufficiently large, this could come at a performance + * cost. + * + * @return An instance of Poolable object T + */ + public get(): T { + + if (this.objectsPointer == -1 && this.replenishPercentage > 0.0) { + this.refillPool(); + } + + let result: T = this.objects[this.objectsPointer] as T; + result.currentOwnerId = Poolable.NO_OWNER; + this.objectsPointer--; + + return result; + } + + /** + * Recycle an instance of Poolable that this pool is capable of generating. + * The T instance passed must not already exist inside this or any other ObjectPool instance. + * + * @param object An object of type T to recycle + */ + public recycle(object: T) { + if (object.currentOwnerId != Poolable.NO_OWNER) { + if (object.currentOwnerId == this.poolId) { + throw new Error("The object passed is already stored in this pool!"); + } else { + throw new Error("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + + this.objectsPointer++; + if (this.objectsPointer >= this.objects.length) { + this.resizePool(); + } + + object.currentOwnerId = this.poolId; + this.objects[this.objectsPointer] = object; + + } + + /** + * Recycle a List of Poolables that this pool is capable of generating. + * The T instances passed must not already exist inside this or any other ObjectPool instance. + * + * @param objects A list of objects of type T to recycle + */ + // public recycle( objects:Array){ + public recycleArray(objects: Array) { + while (objects.size() + this.objectsPointer + 1 > this.desiredCapacity) { + this.resizePool(); + } + let objectsListSize = objects.length; + + // Not relying on recycle(T object) because this is more performant. + for (var i = 0; i < objectsListSize; i++) { + let object: T = objects[i]; + if (object.currentOwnerId != Poolable.NO_OWNER) { + if (object.currentOwnerId == this.poolId) { + throw new Error("The object passed is already stored in this pool!"); + } else { + throw new Error("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + object.currentOwnerId = this.poolId; + this.objects[this.objectsPointer + 1 + i] = object; + } + this.objectsPointer += objectsListSize; + } + + private resizePool() { + let oldCapacity = this.desiredCapacity; + this.desiredCapacity *= 2; + let temp: Object[] = new Array(this.desiredCapacity); + for (var i = 0; i < oldCapacity; i++) { + temp[i] = this.objects[i]; + } + this.objects = temp; + } + + /** + * Returns the capacity of this object pool. Note : The pool will automatically resize + * to contain additional objects if the user tries to add more objects than the pool's + * capacity allows, but this comes at a performance cost. + * + * @return The capacity of the pool. + */ + public getPoolCapacity(): number{ + return this.objects.length; + } + + /** + * Returns the number of objects remaining in the pool, for diagnostic purposes. + * + * @return The number of objects remaining in the pool. + */ + public getPoolCount(): number{ + return this.objectsPointer + 1; + } +} + + diff --git a/device/device_ui/entry/src/main/cpp/types/libsmartperf/index.d.ts b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Poolable.ets similarity index 76% rename from device/device_ui/entry/src/main/cpp/types/libsmartperf/index.d.ts rename to device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Poolable.ets index c678fb8..1a9f0c1 100644 --- a/device/device_ui/entry/src/main/cpp/types/libsmartperf/index.d.ts +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Poolable.ets @@ -13,11 +13,9 @@ * limitations under the License. */ -export const getFpsData: (pkg: string) => string - -export const getRamData: (pid: string) => string - -export const checkDaemon: () => string - -export const checkAccess: (path: string) => string +export abstract class Poolable { + public static NO_OWNER = -1; + currentOwnerId = Poolable.NO_OWNER; + abstract instantiate(): Poolable; +} \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Transformer.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Transformer.ets new file mode 100644 index 0000000..0a5d1c6 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Transformer.ets @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MPPointD from '../utils/MPPointD' +import ViewPortHandler from './ViewPortHandler' +import Matrix from './Matrix' +import IScatterDataSet from '../interfaces/datasets/IScatterDataSet' +import Entry from '../data/EntryOhos' +import IBubbleDataSet from '../interfaces/datasets/IBubbleDataSet' +import ILineDataSet from '../interfaces/datasets/ILineDataSet' +import ICandleDataSet from '../interfaces/datasets/ICandleDataSet' + +import MyRect from '../data/Rect' + +/** + * Transformer class that contains all matrices and is responsible for + * transforming values into pixels on the screen and backwards. + * + * @author Philipp Jahoda + */ +export default class Transformer { + + /** + * matrix to map the values to the screen pixels + */ + protected mMatrixValueToPx: Matrix = new Matrix(); + + /** + * matrix for handling the different offsets of the chart + */ + protected mMatrixOffset: Matrix = new Matrix(); + protected mViewPortHandler: ViewPortHandler; + + constructor(viewPortHandler: ViewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } + + /** + * Prepares the matrix that transforms values to pixels. Calculates the + * scale factors from the charts size and offsets. + * + * @param xChartMin + * @param deltaX + * @param deltaY + * @param yChartMin + */ + public prepareMatrixValuePx(xChartMin: number, deltaX: number, deltaY: number, yChartMin: number) { + + var scaleX: number = this.mViewPortHandler.contentWidth() / deltaX; + var scaleY: number = this.mViewPortHandler.contentHeight() / deltaY; + + if (scaleX == Infinity) { + scaleX = 0; + } + if (scaleY == Infinity) { + scaleY = 0; + } + + // setup all matrices + this.mMatrixValueToPx.reset(); + this.mMatrixValueToPx.postTranslate(-xChartMin, -yChartMin); + this.mMatrixValueToPx.postScale(scaleX, -scaleY); + } + + /** + * Prepares the matrix that contains all offsets. + * + * @param inverted + */ + public prepareMatrixOffset(inverted: boolean) { + + this.mMatrixOffset.reset(); + + // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); + + if (!inverted) + this.mMatrixOffset.postTranslate(this.mViewPortHandler.offsetLeft(), + this.mViewPortHandler.getChartHeight() - this.mViewPortHandler.offsetBottom()); + else { + this.mMatrixOffset + .setTranslate(this.mViewPortHandler.offsetLeft(), -this.mViewPortHandler.offsetTop()); + this.mMatrixOffset.postScale(1.0, -1.0); + } + } + + protected valuePointsForGenerateTransformedValuesScatter: number[] = new Array(1); + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the SCATTERCHART. + * + * @param data + * @return + */ + public generateTransformedValuesScatter(data: IScatterDataSet, phaseX: number, + phaseY: number, froms: number, to: number): number[] { + + const count: number = to - froms * phaseX + 1 * 2; + + if (this.valuePointsForGenerateTransformedValuesScatter.length != count) { + this.valuePointsForGenerateTransformedValuesScatter = new Array(count); + } + var valuePoints: number[] = this.valuePointsForGenerateTransformedValuesScatter; + + for (var j = 0; j < count; j += 2) { + + var e: Entry = data.getEntryForIndex(j / 2 + froms); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + this.getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected valuePointsForGenerateTransformedValuesBubble: number[] = new Array(1); + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the BUBBLECHART. + * + * @param data + * @return + */ + public generateTransformedValuesBubble(data: IBubbleDataSet, phaseY: number, froms: number, to: number): number[] { + + const count = (to - froms + 1) * 2; // (int) Math.ceil((to - from) * phaseX) * 2; + + if (this.valuePointsForGenerateTransformedValuesBubble.length != count) { + this.valuePointsForGenerateTransformedValuesBubble = new Array(count); + } + var valuePoints: number[] = this.valuePointsForGenerateTransformedValuesBubble; + + for (var j = 0; j < count; j += 2) { + + var e: Entry = data.getEntryForIndex(j / 2 + froms); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + this.getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected valuePointsForGenerateTransformedValuesLine: number[] = new Array(1); + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the LINECHART. + * + * @param data + * @return + */ + public generateTransformedValuesLine(data: ILineDataSet, + phaseX: number, phaseY: number, + min: number, max: number): number[] { + + const count: number = (((max - min) * phaseX) + 1) * 2; + + if (this.valuePointsForGenerateTransformedValuesLine.length != count) { + this.valuePointsForGenerateTransformedValuesLine = new Array(count); + } + var valuePoints: number[] = this.valuePointsForGenerateTransformedValuesLine; + + for (var j = 0; j < count; j += 2) { + + var e: Entry = data.getEntryForIndex(j / 2 + min); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + this.getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected valuePointsForGenerateTransformedValuesCandle: number[] = new Array(1); + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the CANDLESTICKCHART. + * + * @param data + * @return + */ + public generateTransformedValuesCandle(data: ICandleDataSet, + phaseX: number, phaseY: number, froms: number, to: number): number[] { + + const count: number = ((to - froms) * phaseX + 1) * 2; + + if (this.valuePointsForGenerateTransformedValuesCandle.length != count) { + this.valuePointsForGenerateTransformedValuesCandle = new Array(count); + } + var valuePoints: number[] = this.valuePointsForGenerateTransformedValuesCandle; + + for (var j = 0; j < count; j += 2) { + +// var e: CandleEntry = data.getEntryForIndex(j / 2 + froms); +// +// if (e != null) { +// valuePoints[j] = e.getX(); +// valuePoints[j + 1] = e.getHigh() * phaseY; +// } else { +// valuePoints[j] = 0; +// valuePoints[j + 1] = 0; +// } + } + + this.getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + /** + * transform a path with all the given matrices VERY IMPORTANT: keep order + * to value-touch-offset + * + * @param path + */ + public pathValueToPixel(path: string): string { + path = this.pathTransform(path, this.mMatrixValueToPx); + path = this.pathTransform(path, this.mViewPortHandler.getMatrixTouch()); + path = this.pathTransform(path, this.mMatrixOffset); + return path; + } + + public pathTransform(path: string, matrix: Matrix): string { + var num = path.match(/(\d+\.\d+)|(\d+)/g) + for (var i = 0;i < num.length; i++) { + var beforeNum: number = Number(num[i]) + var lastNum: number = 0; + var values: number[] = matrix.getValues(); + if (i % 2 == 0) { //x轴 + lastNum = beforeNum * values[Matrix.MSCALE_X] + values[Matrix.MTRANS_X] + } else { //y轴 + lastNum = beforeNum * values[Matrix.MSCALE_Y] + values[Matrix.MTRANS_Y] + } + path = path.replace(String(beforeNum), String(lastNum)) + } + return path; + } + + + /** + * Transform an array of points with all matrices. VERY IMPORTANT: Keep + * matrix order "value-touch-offset" when transforming. + * + * @param pts + */ + public pointValuesToPixel(pts: number[]) { + this.mMatrixValueToPx.mapPoints(pts); + this.mViewPortHandler.getMatrixTouch().mapPoints(pts); + this.mMatrixOffset.mapPoints(pts); + } + + /** + * Transform a rectangle with all matrices. + * + * @param r + */ + public rectValueToPixel(r: MyRect) { + + this.mMatrixValueToPx.mapRect(r); + this.mViewPortHandler.getMatrixTouch().mapRect(r); + this.mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + * @param phaseY + */ + public rectToPixelPhase(r: MyRect, phaseY: number) { + + // multiply the height of the rect with the phase + r.top *= phaseY; + r.bottom *= phaseY; + + this.mMatrixValueToPx.mapRect(r); + this.mViewPortHandler.getMatrixTouch().mapRect(r); + this.mMatrixOffset.mapRect(r); + } + + public rectToPixelPhaseHorizontal(r: MyRect, phaseY: number) { + + // multiply the height of the rect with the phase + r.left *= phaseY; + r.right *= phaseY; + + this.mMatrixValueToPx.mapRect(r); + this.mViewPortHandler.getMatrixTouch().mapRect(r); + this.mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + */ + public rectValueToPixelHorizontal(r: MyRect, phaseY?: number) { + if (phaseY != null && phaseY != undefined) { + r.left *= phaseY; + r.right *= phaseY; + } + this.mMatrixValueToPx.mapRect(r); + this.mViewPortHandler.getMatrixTouch().mapRect(r); + this.mMatrixOffset.mapRect(r); + } + + /** + * transforms multiple rects with all matrices + * + * @param rects + */ + public rectValuesToPixel(rects: MyRect[]) { + + var m: Matrix = this.getValueToPixelMatrix(); + + for (var i = 0; i < rects.length; i++) { + m.mapRect(rects[i]); + } + } + + protected mPixelToValueMatrixBuffer: Matrix = new Matrix(); + + /** + * Transforms the given array of touch positions (pixels) (x, y, x, y, ...) + * into values on the chart. + * + * @param pixels + */ + public pixelsToValue(pixels: number[]) { + + var tmp: Matrix = this.mPixelToValueMatrixBuffer; + tmp.reset(); + + // invert all matrixes to convert back to the original value + this.mMatrixOffset.invert(tmp); + tmp.mapPoints(pixels); + + this.mViewPortHandler.getMatrixTouch().invert(tmp); + tmp.mapPoints(pixels); + + this.mMatrixValueToPx.invert(tmp); + tmp.mapPoints(pixels); + } + + /** + * buffer for performance + */ + ptsBuffer: number[] = new Array(2); + + + /** + * Returns a recyclable MPPointD instance. + * returns the x and y values in the chart at the given touch point + * (encapsulated in a MPPointD). This method transforms pixel coordinates to + * coordinates / values in the chart. This is the opposite method to + * getPixelForValues(...). + * + * @param x + * @param y + * @return + */ + public getValuesByTouchPoint(x: number, y: number, outputPoint?: MPPointD): MPPointD { + var result: MPPointD = (outputPoint != null&&outputPoint!=undefined)?outputPoint:MPPointD.getInstance(0, 0); + + this.ptsBuffer[0] = x; + this.ptsBuffer[1] = y; + + this.pixelsToValue(this.ptsBuffer); + + outputPoint.x = this.ptsBuffer[0]; + outputPoint.y = this.ptsBuffer[1]; + + return result; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the x and y coordinates (pixels) for a given x and y value in the chart. + * + * @param x + * @param y + * @return + */ + public getPixelForValues(x: number, y: number): MPPointD { + + this.ptsBuffer[0] = x; + this.ptsBuffer[1] = y; + + this.pointValuesToPixel(this.ptsBuffer); + + var xPx: number = this.ptsBuffer[0]; + var yPx: number = this.ptsBuffer[1]; + + return MPPointD.getInstance(xPx, yPx); + } + + public getValueMatrix(): Matrix { + return this.mMatrixValueToPx; + } + + public getOffsetMatrix(): Matrix { + return this.mMatrixOffset; + } + + private mMBuffer1: Matrix = new Matrix(); + + public getValueToPixelMatrix(): Matrix { + this.mMBuffer1.set(this.mMatrixValueToPx); + this.mMBuffer1.postConcat(this.mViewPortHandler.mMatrixTouch); + this.mMBuffer1.postConcat(this.mMatrixOffset); + return this.mMBuffer1; + } + + private mMBuffer2: Matrix = new Matrix(); + + public getPixelToValueMatrix(): Matrix { + this.getValueToPixelMatrix().invert(this.mMBuffer2); + return this.mMBuffer2; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Utils.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Utils.ets new file mode 100644 index 0000000..f393c1b --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/Utils.ets @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Paint, {TextPaint, ImagePaint} from '../data/Paint' +import MyRect from '../data/Rect' +import FSize from './FSize' +import MPPointF from './MPPointF' +import IValueFormatter from '../formatter/IValueFormatter'; +import DefaultValueFormatter from '../formatter/DefaultValueFormatter'; +import deviceInfo from '@ohos.deviceInfo'; +/** + * Utilities class that has some helper methods. Needs to be initialized by + * calling Utils.init(...) before usage. Inside the Chart.init() method, this is + * done, if the Utils are used before that, Utils.init(...) needs to be called + * manually. + * + * @author Philipp Jahoda + */ +export default abstract class Utils { + private static scaledDensity: number = 3.3125; + private static mMinimumFlingVelocity: number = 50; + private static mMaximumFlingVelocity: number= 8000; + public static DEG2RAD: number = (Math.PI / 180.0); + public static FDEG2RAD: number = Math.PI / 180.; + public static DOUBLE_EPSILON: number = 4.9E-324; + public static FLOAT_EPSILON: number = 1.4E-45; + +/** + * initialize method, called inside the Chart.init() method. + * + * @param context + */ + public static init() { + + // if (context == null) { + // // noinspection deprecation + // mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + // // noinspection deprecation + // mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); + // + // Log.e("MPChartLib-Utils" + // , "Utils.init(...) PROVIDED CONTEXT OBJECT IS NULL"); + // + // } else { + // ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + // mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + // mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + // + // Resources res = context.getResources(); + // mMetrics = res.getDisplayMetrics(); + // } + } + + +/** + * This method converts dp unit to equivalent pixels, depending on device + * density. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. + * + * @param dp A value in dp (density independent pixels) unit. Which we need + * to convert into pixels + * @return A float value to represent px equivalent to dp depending on + * device density + */ + public static convertDpToPixel(dp: number): number{ + //return dp * this.scaledDensity; + return vp2px(dp); + } + + public static setScaledDensity(value: number) { + this.scaledDensity = value + } + +// +// /** +// * This method converts device specific pixels to density independent +// * pixels. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. +// * +// * @param px A value in px (pixels) unit. Which we need to convert into db +// * @return A float value to represent dp equivalent to px value +// */ +// public static float convertPixelsToDp(float px) { +// +// if (mMetrics == null) { +// +// Log.e("MPChartLib-Utils", +// "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + +// " calling Utils.convertPixelsToDp(...). Otherwise conversion does not" + +// " take place."); +// return px; +// } +// +// return px / mMetrics.density; +// } +// +/** + * calculates the approximate width of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static calcTextWidth(paint: Paint, demoText: string):number{ + return demoText.length * paint.getTextSize() / 2; + } + + private static mCalcTextHeightRect: MyRect = new MyRect(); +/** + * calculates the approximate height of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static calcTextHeight(paint: Paint, demoText: string): number { + return paint.getTextSize(); + } + +// private static Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); +// +// public static getLineHeight(paint : Paint) : number { +// return getLineHeight(paint, mFontMetrics); +// } + + public static getLineHeight(paint: Paint): number { + // paint.getFontMetrics(fontMetrics); + //// return fontMetrics.descent - fontMetrics.ascent; + return paint.getTextSize(); + } +// + public static getLineSpacing(paint: Paint): number { + return 1.2; + } +// +// public static float getLineSpacing(Paint paint, Paint.FontMetrics fontMetrics){ +// paint.getFontMetrics(fontMetrics); +// return fontMetrics.ascent - fontMetrics.top + fontMetrics.bottom; +// } +// +// /** +// * Returns a recyclable FSize instance. +// * calculates the approximate size of a text, depending on a demo text +// * avoid repeated calls (e.g. inside drawing methods) +// * +// * @param paint +// * @param demoText +// * @return A Recyclable FSize instance +// */ + public static calcTextSize(paint: Paint, demoText: string): FSize{ + var fsize: FSize = new FSize(paint.getTextSize() * demoText.length, paint.getTextSize()); + return fsize; + } +// +// private static Rect mCalcTextSizeRect = new Rect(); +// /** +// * calculates the approximate size of a text, depending on a demo text +// * avoid repeated calls (e.g. inside drawing methods) +// * +// * @param paint +// * @param demoText +// * @param outputFSize An output variable, modified by the function. +// */ +// public static void calcTextSize(Paint paint, String demoText, FSize outputFSize) { +// +// Rect r = mCalcTextSizeRect; +// r.set(0,0,0,0); +// paint.getTextBounds(demoText, 0, demoText.length(), r); +// outputFSize.width = r.width(); +// outputFSize.height = r.height(); +// +// } +// +// +// /** +// * Math.pow(...) is very expensive, so avoid calling it and create it +// * yourself. +// */ +// private static final int POW_10[] = { +// 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 +// }; +// + private static mDefaultValueFormatter: IValueFormatter = Utils.generateDefaultValueFormatter(); +// + private static generateDefaultValueFormatter(): IValueFormatter { + var formatter: DefaultValueFormatter = new DefaultValueFormatter(1); + return formatter; + } + +// /// - returns: The default value formatter used for all chart components that needs a default + public static getDefaultValueFormatter(): IValueFormatter + { + return Utils.mDefaultValueFormatter; + } +// +// /** +// * Formats the given number to the given number of decimals, and returns the +// * number as a string, maximum 35 characters. If thousands are separated, the separating +// * character is a dot ("."). +// * +// * @param number +// * @param digitCount +// * @param separateThousands set this to true to separate thousands values +// * @return +// */ +// public static String formatNumber(float number, int digitCount, boolean separateThousands) { +// return formatNumber(number, digitCount, separateThousands, '.'); +// } +// +// /** +// * Formats the given number to the given number of decimals, and returns the +// * number as a string, maximum 35 characters. +// * +// * @param number +// * @param digitCount +// * @param separateThousands set this to true to separate thousands values +// * @param separateChar a caracter to be paced between the "thousands" +// * @return +// */ +// public static String formatNumber(float number, int digitCount, boolean separateThousands, +// char separateChar) { +// +// char[] out = new char[35]; +// +// boolean neg = false; +// if (number == 0) { +// return "0"; +// } +// +// boolean zero = false; +// if (number < 1 && number > -1) { +// zero = true; +// } +// +// if (number < 0) { +// neg = true; +// number = -number; +// } +// +// if (digitCount > POW_10.length) { +// digitCount = POW_10.length - 1; +// } +// +// number *= POW_10[digitCount]; +// long lval = Math.round(number); +// int ind = out.length - 1; +// int charCount = 0; +// boolean decimalPointAdded = false; +// +// while (lval != 0 || charCount < (digitCount + 1)) { +// int digit = (int) (lval % 10); +// lval = lval / 10; +// out[ind--] = (char) (digit + '0'); +// charCount++; +// +// // add decimal point +// if (charCount == digitCount) { +// out[ind--] = ','; +// charCount++; +// decimalPointAdded = true; +// +// // add thousand separators +// } else if (separateThousands && lval != 0 && charCount > digitCount) { +// +// if (decimalPointAdded) { +// +// if ((charCount - digitCount) % 4 == 0) { +// out[ind--] = separateChar; +// charCount++; +// } +// +// } else { +// +// if ((charCount - digitCount) % 4 == 3) { +// out[ind--] = separateChar; +// charCount++; +// } +// } +// } +// } +// +// // if number around zero (between 1 and -1) +// if (zero) { +// out[ind--] = '0'; +// charCount += 1; +// } +// +// // if the number is negative +// if (neg) { +// out[ind--] = '-'; +// charCount += 1; +// } +// +// int start = out.length - charCount; +// +// return String.valueOf(out, start, out.length - start); +// } +// +/** + * rounds the given number to the next significant number + * + * @param number + * @return + */ + public static roundToNextSignificant(number: number): number { + if (number == Infinity || + isNaN(number) || + number == 0.0) + return 0; + + const d: number = Math.ceil(Math.log10(number < 0 ? -number : number)); + const pw: number = 1 - Math.floor(d); + const magnitude: number = Math.pow(10, pw); + const shifted: number = Math.round(number * magnitude); + return shifted / magnitude; + } + +/** + * Returns the appropriate number of decimals to be used for the provided + * number. + * + * @param number + * @return + */ + public static getDecimals(number: number): number { + + let i: number = this.roundToNextSignificant(number); + + if (i == Infinity) + return 0; + + return Math.floor(Math.ceil(-Math.log10(i)) + 2); + } +// +// /** +// * Converts the provided Integer List to an int array. +// * +// * @param integers +// * @return +// */ +// public static int[] convertIntegers(List integers) { +// +// int[] ret = new int[integers.size()]; +// +// copyIntegers(integers, ret); +// +// return ret; +// } +// +// public static void copyIntegers(List from, int[] to){ +// int count = to.length < from.size() ? to.length : from.size(); +// for(int i = 0 ; i < count ; i++){ +// to[i] = from.get(i); +// } +// } +// +// /** +// * Converts the provided String List to a String array. +// * +// * @param strings +// * @return +// */ +// public static String[] convertStrings(List strings) { +// +// String[] ret = new String[strings.size()]; +// +// for (int i = 0; i < ret.length; i++) { +// ret[i] = strings.get(i); +// } +// +// return ret; +// } +// +// public static void copyStrings(List from, String[] to){ +// int count = to.length < from.size() ? to.length : from.size(); +// for(int i = 0 ; i < count ; i++){ +// to[i] = from.get(i); +// } +// } +// +/** + * Replacement for the Math.nextUp(...) method that is only available in + * HONEYCOMB and higher. Dat's some seeeeek sheeet. + * + * @param d + * @return + */ + public static nextUp(d: number): number { + if (d == Infinity) + return d; + else { + d += 0.0; + return d >= 0.0 ? d += 0.000000001 : d -= 0.000000001; + } + } +// +// /** +// * Returns a recyclable MPPointF instance. +// * Calculates the position around a center point, depending on the distance +// * from the center, and the angle of the position around the center. +// * +// * @param center +// * @param dist +// * @param angle in degrees, converted to radians internally +// * @return +// */ + + public static getPosition(center:MPPointF,dist:number, angle:number, outputPoint?:MPPointF):MPPointF{ + let p:MPPointF = ((outputPoint==null||outputPoint==undefined)?MPPointF.getInstance(0,0):outputPoint); + p.x = center.x + dist * Math.cos((angle* Math.PI / 180)); + p.y = center.y + dist * Math.sin((angle* Math.PI / 180)); + return p + } +// +// public static void velocityTrackerPointerUpCleanUpIfNecessary(MotionEvent ev, +// VelocityTracker tracker) { +// +// // Check the dot product of current velocities. +// // If the pointer that left was opposing another velocity vector, clear. +// tracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); +// final int upIndex = ev.getActionIndex(); +// final int id1 = ev.getPointerId(upIndex); +// final float x1 = tracker.getXVelocity(id1); +// final float y1 = tracker.getYVelocity(id1); +// for (int i = 0, count = ev.getPointerCount(); i < count; i++) { +// if (i == upIndex) +// continue; +// +// final int id2 = ev.getPointerId(i); +// final float x = x1 * tracker.getXVelocity(id2); +// final float y = y1 * tracker.getYVelocity(id2); +// +// final float dot = x + y; +// if (dot < 0) { +// tracker.clear(); +// break; +// } +// } +// } +// +// /** +// * Original method view.postInvalidateOnAnimation() only supportd in API >= +// * 16, This is a replica of the code from ViewCompat. +// * +// * @param view +// */ +// @SuppressLint("NewApi") +// public static void postInvalidateOnAnimation(View view) { +// if (Build.VERSION.SDK_INT >= 16) +// view.postInvalidateOnAnimation(); +// else +// view.postInvalidateDelayed(10); +// } +// + public static getMinimumFlingVelocity():number { + return Utils.mMinimumFlingVelocity; + } +// +// public static int getMaximumFlingVelocity() { +// return mMaximumFlingVelocity; +// } +// + /** + * returns an angle between 0.f < 360.f (not less than zero, less than 360) + */ + public static getNormalizedAngle(angle:number):number { + while (angle < 0) + angle += 360; + + return angle % 360; + } + + private static mDrawableBoundsCache: MyRect = new MyRect(); + + public static drawImage(icon: string|Resource,x: number, y: number, + width: number, height: number): Paint[] { + + let drawOffset: MPPointF = MPPointF.getInstance(); + drawOffset.x = x - (width / 2); + drawOffset.y = y - (height / 2); + + let drawable:ImagePaint = new ImagePaint(); + drawable.setX(this.mDrawableBoundsCache.left); + drawable.setY(this.mDrawableBoundsCache.top); + drawable.setWidth(width); + drawable.setHeight(width); + drawable.setIcon(icon) + + drawable.setX(drawable.x + drawOffset.x); + drawable.setY(drawable.y + drawOffset.y); + return [drawable]; + } + +// private static Rect mDrawTextRectBuffer = new Rect(); +// private static Paint.FontMetrics mFontMetricsBuffer = new Paint.FontMetrics(); +// + public static drawXAxisValue(text: string, x: number, y: number, paint: TextPaint, + anchor: MPPointF, angleDegrees: number): Paint{ + + var drawOffsetX: number = 0; + var drawOffsetY: number = 0; + + var labelSize: FSize = Utils.calcTextSize(paint, text); + + drawOffsetX -= labelSize.width; + + // and draws from bottom to top. + // And we want to normalize it. + drawOffsetY += -labelSize.height; + + // To have a consistent point of reference, we always draw left-aligned + paint.setTextAlign(TextAlign.Start); + paint.setText(text) + if (angleDegrees != 0) { + + // Move the text drawing rect in a way that it always rotates around its center + drawOffsetX -= labelSize.width * 0.5; + drawOffsetY -= labelSize.height * 0.5; + + var translateX: number = x; + var translateY: number = y; + + // Move the "outer" rect relative to the anchor, assuming its centered + if (anchor.x != 0.5 || anchor.y != 0.5) { + var rotatedSize: FSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelSize.width, + labelSize.height, + angleDegrees); + + translateX -= rotatedSize.width * (anchor.x - 0.5); + translateY -= rotatedSize.height * (anchor.y - 0.5); + FSize.recycleInstance(rotatedSize); + } + paint.setTranslateX(translateX) + paint.setTranslateY(translateY) + paint.setRotate(angleDegrees) + paint.setX(drawOffsetX); + paint.setY(drawOffsetY); + } else { + if (anchor.x != 0 || anchor.y != 0) { + drawOffsetX = labelSize.width / 2 * anchor.x; + drawOffsetY = 12 * anchor.y; + } + x -= drawOffsetX; + y -= drawOffsetY; + paint.setX(x); + paint.setY(y); + } + return paint + } +// +// public static void drawMultilineText(Canvas c, StaticLayout textLayout, +// float x, float y, +// TextPaint paint, +// MPPointF anchor, float angleDegrees) { +// +// float drawOffsetX = 0.f; +// float drawOffsetY = 0.f; +// float drawWidth; +// float drawHeight; +// +// final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); +// +// drawWidth = textLayout.getWidth(); +// drawHeight = textLayout.getLineCount() * lineHeight; +// +// drawOffsetX -= mDrawTextRectBuffer.left; +// +// // and draws from bottom to top. +// // And we want to normalize it. +// drawOffsetY += drawHeight; +// +// // To have a consistent point of reference, we always draw left-aligned +// Paint.Align originalTextAlign = paint.getTextAlign(); +// paint.setTextAlign(Paint.Align.LEFT); +// +// if (angleDegrees != 0.f) { +// +// // Move the text drawing rect in a way that it always rotates around its center +// drawOffsetX -= drawWidth * 0.5f; +// drawOffsetY -= drawHeight * 0.5f; +// +// float translateX = x; +// float translateY = y; +// +// // Move the "outer" rect relative to the anchor, assuming its centered +// if (anchor.x != 0.5f || anchor.y != 0.5f) { +// final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( +// drawWidth, +// drawHeight, +// angleDegrees); +// +// translateX -= rotatedSize.width * (anchor.x - 0.5f); +// translateY -= rotatedSize.height * (anchor.y - 0.5f); +// FSize.recycleInstance(rotatedSize); +// } +// +// c.save(); +// c.translate(translateX, translateY); +// c.rotate(angleDegrees); +// +// c.translate(drawOffsetX, drawOffsetY); +// textLayout.draw(c); +// +// c.restore(); +// } else { +// if (anchor.x != 0.f || anchor.y != 0.f) { +// +// drawOffsetX -= drawWidth * anchor.x; +// drawOffsetY -= drawHeight * anchor.y; +// } +// +// drawOffsetX += x; +// drawOffsetY += y; +// +// c.save(); +// +// c.translate(drawOffsetX, drawOffsetY); +// textLayout.draw(c); +// +// c.restore(); +// } +// +// paint.setTextAlign(originalTextAlign); +// } +// +// public static void drawMultilineText(Canvas c, String text, +// float x, float y, +// TextPaint paint, +// FSize constrainedToSize, +// MPPointF anchor, float angleDegrees) { +// +// StaticLayout textLayout = new StaticLayout( +// text, 0, text.length(), +// paint, +// (int) Math.max(Math.ceil(constrainedToSize.width), 1.f), +// Layout.Alignment.ALIGN_NORMAL, 1.f, 0.f, false); +// +// +// drawMultilineText(c, textLayout, x, y, paint, anchor, angleDegrees); +// } +// +// /** +// * Returns a recyclable FSize instance. +// * Represents size of a rotated rectangle by degrees. +// * +// * @param rectangleSize +// * @param degrees +// * @return A Recyclable FSize instance +// */ +// public static getSizeOfRotatedRectangleByDegrees( rectangleSize:FSize, degrees:number):FSize{ +// var radians:number = degrees * Utils.FDEG2RAD; +// return this.getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, +// radians); +// } +// +// /** +// * Returns a recyclable FSize instance. +// * Represents size of a rotated rectangle by radians. +// * +// * @param rectangleSize +// * @param radians +// * @return A Recyclable FSize instance +// */ +// public static getSizeOfRotatedRectangleByRadians( rectangleSize:FSize, radians:number):FSize { +// return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, +// radians); +// } +// +// /** +// * Returns a recyclable FSize instance. +// * Represents size of a rotated rectangle by degrees. +// * +// * @param rectangleWidth +// * @param rectangleHeight +// * @param degrees +// * @return A Recyclable FSize instance +// */ + public static getSizeOfRotatedRectangleByDegrees(rectangleWidth: number, + rectangleHeight: number, degrees: number): FSize{ + var radians: number = degrees * Utils.FDEG2RAD; + return Utils.getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians); + } +// +// /** +// * Returns a recyclable FSize instance. +// * Represents size of a rotated rectangle by radians. +// * +// * @param rectangleWidth +// * @param rectangleHeight +// * @param radians +// * @return A Recyclable FSize instance +// */ + public static getSizeOfRotatedRectangleByRadians(rectangleWidth: number, + rectangleHeight: number, radians: number): FSize{ + return FSize.getInstance( + Math.abs(rectangleWidth * Math.cos(radians)) + Math.abs(rectangleHeight * + Math.sin(radians)), + Math.abs(rectangleWidth * Math.sin(radians)) + Math.abs(rectangleHeight * + Math.cos(radians)) + ); + } + + public static getSDKInt():number { + return deviceInfo.sdkApiVersion; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ViewPortHandler.ets b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ViewPortHandler.ets new file mode 100644 index 0000000..254e5d0 --- /dev/null +++ b/device/device_ui/entry/src/main/ets/common/ui/detail/chart/utils/ViewPortHandler.ets @@ -0,0 +1,687 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MyRect from '../data/Rect' +import Matrix from '../utils/Matrix' +import MPPointF from './MPPointF' +import Utils from './Utils' +import Chart from '../charts/Chart' + +/** + * Class that contains information about the charts current viewport settings, including offsets, scale & translation + * levels, ... + * + * @author Philipp Jahoda + */ +export default class ViewPortHandler { + + /** + * matrix used for touch events + */ + public mMatrixTouch: Matrix = new Matrix(); + + /** + * this rectangle defines the area in which graph values can be drawn + */ + protected mContentRect = new MyRect(); + protected mChartWidth: number = 0; + protected mChartHeight: number = 0; + + /** + * minimum scale value on the y-axis + */ + private mMinScaleY: number = 1; + + /** + * maximum scale value on the y-axis + */ + private mMaxScaleY: number = Number.MAX_VALUE; + + /** + * minimum scale value on the x-axis + */ + private mMinScaleX: number = 1; + + /** + * maximum scale value on the x-axis + */ + private mMaxScaleX: number = Number.MAX_VALUE; + + /** + * contains the current scale factor of the x-axis + */ + private mScaleX: number = 1; + + /** + * contains the current scale factor of the y-axis + */ + private mScaleY: number = 1; + + /** + * current translation (drag distance) on the x-axis + */ + private mTransX: number = 0; + + /** + * current translation (drag distance) on the y-axis + */ + private mTransY: number = 0; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private mTransOffsetX: number = 0; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private mTransOffsetY: number = 0; + + + /** + * Sets the width and height of the chart. + * + * @param width + * @param height + */ + public setChartDimens(width: number, height: number) { + + var offsetLeft = this.offsetLeft(); + var offsetTop = this.offsetTop(); + var offsetRight = this.offsetRight(); + var offsetBottom = this.offsetBottom(); + + this.mChartHeight = height; + this.mChartWidth = width; + + this.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); + } + + public hasChartDimens(): boolean { + if (this.mChartHeight > 0 && this.mChartWidth > 0) + return true; + else + return false; + } + + public restrainViewPort(offsetLeft: number, offsetTop: number, offsetRight: number, + offsetBottom: number) { + this.mContentRect.set(offsetLeft, offsetTop, this.mChartWidth - offsetRight, this.mChartHeight + - offsetBottom); + } + + public offsetLeft(): number { + return this.mContentRect.left; + } + + public offsetRight(): number { + return this.mChartWidth - this.mContentRect.right; + } + + public offsetTop(): number { + return this.mContentRect.top; + } + + public offsetBottom(): number { + return this.mChartHeight - this.mContentRect.bottom; + } + + public contentTop(): number { + return this.mContentRect.top; + } + + public contentLeft(): number { + return this.mContentRect.left; + } + + public contentRight(): number { + return this.mContentRect.right; + } + + public contentBottom(): number { + return this.mContentRect.bottom; + } + + public contentWidth(): number { + return this.mContentRect.width(); + } + + public contentHeight(): number { + return this.mContentRect.height(); + } + + public getContentRect(): MyRect { + return this.mContentRect; + } + + public getContentCenter(): MPPointF { + return MPPointF.getInstance(this.mContentRect.centerX(), this.mContentRect.centerY()); + } + + public getChartHeight(): number { + return this.mChartHeight; + } + + public getChartWidth(): number { + return this.mChartWidth; + } + + /** + * Returns the smallest extension of the content rect (width or height). + * + * @return + */ + public getSmallestContentExtension(): number { + return Math.min(this.mContentRect.width(), this.mContentRect.height()); + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */ + + /** + * Zooms in by 1.4f, x and y are the coordinates (in pixels) of the zoom + * center. + * + * @param x + * @param y + */ + public zoomIn(x: number, y: number,outputMatrix?:Matrix): Matrix { + var save: Matrix =(outputMatrix==null ||outputMatrix==undefined)?new Matrix():outputMatrix; + save.reset(); + save.set(this.mMatrixTouch); + save.postScale(1.4, 1.4, x, y); + return save; + } + + public zoomOut(x: number, y: number, outputMatrix: Matrix) { + outputMatrix.reset(); + outputMatrix.set(this.mMatrixTouch); + outputMatrix.postScale(0.7, 0.7, x, y); + } + + /** + * Zooms out to original size. + * @param outputMatrix + */ + public resetZoom(outputMatrix: Matrix) { + outputMatrix.reset(); + outputMatrix.set(this.mMatrixTouch); + outputMatrix.postScale(1.0, 1.0, 0.0, 0.0); + } + + /** + * Post-scales by the specified scale factors. + * + * @param scaleX + * @param scaleY + * @return + */ + public zoom(scaleX: number, scaleY: number, x?: number, y?: number,outputMatrix?:Matrix): Matrix { + + var save: Matrix =(outputMatrix!=null && outputMatrix!=undefined)?outputMatrix: new Matrix(); + save.reset(); + save.set(this.mMatrixTouch); + if (x != undefined &&x !=null && y != undefined&&y !=null) { + save.postScale(scaleX, scaleY, x, y); + } else { + save.postScale(scaleX, scaleY); + } + return save; + } + + + /** + * Sets the scale factor to the specified values. + * + * @param scaleX + * @param scaleY + * @return + */ + public setZoom(scaleX: number, scaleY: number, x?: number, y?: number): Matrix { + + var save: Matrix = new Matrix(); + save.reset(); + save.set(this.mMatrixTouch); + if (x != undefined && y != undefined) { + save.setScale(scaleX, scaleY, x, y); + } else { + save.setScale(scaleX, scaleY); + } + return save; + } + + protected valsBufferForFitScreen: number[] = new Array(9); + + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. Output Matrix is available for those who wish to cache the object. + */ + public fitScreen(outputMatrix?:Matrix): Matrix { + + var save: Matrix = (outputMatrix ==null||outputMatrix==undefined)?new Matrix():outputMatrix; + this.mMinScaleX = 1; + this.mMinScaleY = 1; + + save.set(this.mMatrixTouch); + + var vals: number[] = save.getValues(); + + // reset all translations and scaling + vals[Matrix.MTRANS_X] = 0; + vals[Matrix.MTRANS_Y] = 0; + vals[Matrix.MSCALE_X] = 1; + vals[Matrix.MSCALE_Y] = 1; + + save.setValues(vals); + + return save; + } + + /** + * Post-translates to the specified points. Less Performant. + * + * @param transformedPts + * @return + */ + public translate(transformedPts: number[],outputMatrix:Matrix): Matrix { + + var save: Matrix =(outputMatrix!=null && outputMatrix!=undefined)?outputMatrix:new Matrix(); + save.reset(); + save.set(this.mMatrixTouch); + const x: number = transformedPts[0] - this.offsetLeft(); + const y: number = transformedPts[1] - this.offsetTop(); + save.postTranslate(-x, -y); + return save; + } + + protected mCenterViewPortMatrixBuffer: Matrix = new Matrix(); + + /** + * Centers the viewport around the specified position (x-index and y-value) + * in the chart. Centering the viewport outside the bounds of the chart is + * not possible. Makes most sense in combination with the + * setScaleMinima(...) method. + * + * @param transformedPts the position to center view viewport to + * @param view + * @return save + */ + public centerViewPort(transformedPts: number[], view: Chart) { + + let save: Matrix = this.mCenterViewPortMatrixBuffer; + save.reset(); + save.set(this.mMatrixTouch); + + const x: number = transformedPts[0] - this.offsetLeft(); + const y: number = transformedPts[1] - this.offsetTop(); + + save.postTranslate(-x, -y); + + this.refresh(save, view, true); + } + + /** + * buffer for storing the 9 matrix values of a 3x3 matrix + */ + protected matrixBuffer: number[] = new Array(9); + + /** + * call this method to refresh the graph with a given matrix + * + * @param newMatrix + * @return + */ + public refresh(newMatrix: Matrix, chart: Chart, invalidate: boolean): Matrix { + + this.mMatrixTouch.set(newMatrix); + + // make sure scale and translation are within their bounds + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + + if (invalidate) + chart.invalidate(); + + newMatrix.set(this.mMatrixTouch); + return newMatrix; + } + + /** + * limits the maximum scale and X translation of the given matrix + * + * @param matrix + */ + public limitTransAndScale(matrix: Matrix, content: MyRect) { + + this.matrixBuffer = matrix.getValues(); + + var curTransX: number = this.matrixBuffer[Matrix.MTRANS_X]; + var curScaleX: number = this.matrixBuffer[Matrix.MSCALE_X]; + + var curTransY: number = this.matrixBuffer[Matrix.MTRANS_Y]; + var curScaleY: number = this.matrixBuffer[Matrix.MSCALE_Y]; + + // min scale-x is 1f + this.mScaleX = Math.min(Math.max(this.mMinScaleX, curScaleX), this.mMaxScaleX); + + // min scale-y is 1f + this.mScaleY = Math.min(Math.max(this.mMinScaleY, curScaleY), this.mMaxScaleY); + + var width: number = 0; + var height: number = 0; + + if (content != null) { + width = content.width(); + height = content.height(); + } + + var maxTransX: number = -width * (this.mScaleX - 1); + this.mTransX = Math.min(Math.max(curTransX, maxTransX - this.mTransOffsetX), this.mTransOffsetX); + + var maxTransY: number = height * (this.mScaleY - 1); + this.mTransY = Math.max(Math.min(curTransY, maxTransY + this.mTransOffsetY), -this.mTransOffsetY); + + this.matrixBuffer[Matrix.MTRANS_X] = this.mTransX; + this.matrixBuffer[Matrix.MSCALE_X] = this.mScaleX; + + this.matrixBuffer[Matrix.MTRANS_Y] = this.mTransY; + this.matrixBuffer[Matrix.MSCALE_Y] = this.mScaleY; + + matrix.setValues(this.matrixBuffer); + } + + /** + * Sets the minimum scale factor for the x-axis + * + * @param xScale + */ + public setMinimumScaleX(xScale: number) { + + if (xScale < 1) + xScale = 1; + + this.mMinScaleX = xScale; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + /** + * Sets the maximum scale factor for the x-axis + * + * @param xScale + */ + public setMaximumScaleX(xScale: number) { + + if (xScale == 0.0) + xScale = Number.MAX_VALUE; + + this.mMaxScaleX = xScale; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + /** + * Sets the minimum and maximum scale factors for the x-axis + * + * @param minScaleX + * @param maxScaleX + */ + public setMinMaxScaleX(minScaleX: number, maxScaleX: number) { + + if (minScaleX < 1) + minScaleX = 1; + + if (maxScaleX == 0.0) + maxScaleX = Number.MAX_VALUE; + + this.mMinScaleX = minScaleX; + this.mMaxScaleX = maxScaleX; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + /** + * Sets the minimum scale factor for the y-axis + * + * @param yScale + */ + public setMinimumScaleY(yScale: number) { + + if (yScale < 1) + yScale = 1; + + this.mMinScaleY = yScale; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + /** + * Sets the maximum scale factor for the y-axis + * + * @param yScale + */ + public setMaximumScaleY(yScale: number) { + + if (yScale == 0.0) + yScale = Number.MAX_VALUE; + + this.mMaxScaleY = yScale; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + public setMinMaxScaleY(minScaleY: number, maxScaleY: number) { + + if (minScaleY < 1) + minScaleY = 1; + + if (maxScaleY == 0.0) + maxScaleY = Number.MAX_VALUE; + + this.mMinScaleY = minScaleY; + this.mMaxScaleY = maxScaleY; + + this.limitTransAndScale(this.mMatrixTouch, this.mContentRect); + } + + /** + * Returns the charts-touch matrix used for translation and scale on touch. + * + * @return + */ + public getMatrixTouch(): Matrix { + return this.mMatrixTouch; + } + + /** + * ################ ################ ################ ################ + */ + /** + * BELOW METHODS FOR BOUNDS CHECK + */ + public isInBoundsX(x: number): boolean { + return this.isInBoundsLeft(x) && this.isInBoundsRight(x); + } + + public isInBoundsY(y: number) { + return this.isInBoundsTop(y) && this.isInBoundsBottom(y); + } + + public isInBounds(x: number, y: number): boolean { + return this.isInBoundsX(x) && this.isInBoundsY(y); + } + + public isInBoundsLeft(x: number): boolean { + return this.mContentRect.left <= x + 1; + } + + public isInBoundsRight(x: number): boolean { + x = x * 100 / 100; + return this.mContentRect.right >= x - 1; + } + + public isInBoundsTop(y: number): boolean { + return this.mContentRect.top <= y; + } + + public isInBoundsBottom(y: number): boolean { + y = y * 100 / 100; + return this.mContentRect.bottom >= y; + } + + /** + * returns the current x-scale factor + */ + public getScaleX(): number { + return this.mScaleX; + } + + /** + * returns the current y-scale factor + */ + public getScaleY(): number { + return this.mScaleY; + } + + public getMinScaleX(): number { + return this.mMinScaleX; + } + + public getMaxScaleX(): number { + return this.mMaxScaleX; + } + + public getMinScaleY(): number { + return this.mMinScaleY; + } + + public getMaxScaleY(): number { + return this.mMaxScaleY; + } + + /** + * Returns the translation (drag / pan) distance on the x-axis + * + * @return + */ + public getTransX(): number { + return this.mTransX; + } + + /** + * Returns the translation (drag / pan) distance on the y-axis + * + * @return + */ + public getTransY(): number { + return this.mTransY; + } + + /** + * if the chart is fully zoomed out, return true + * + * @return + */ + public isFullyZoomedOut(): boolean { + + return this.isFullyZoomedOutX() && this.isFullyZoomedOutY(); + } + + /** + * Returns true if the chart is fully zoomed out on it's y-axis (vertical). + * + * @return + */ + public isFullyZoomedOutY(): boolean { + return!(this.mScaleY > this.mMinScaleY || this.mMinScaleY > 1); + } + + /** + * Returns true if the chart is fully zoomed out on it's x-axis + * (horizontal). + * + * @return + */ + public isFullyZoomedOutX(): boolean { + return!(this.mScaleX > this.mMinScaleX || this.mMinScaleX > 1); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the x-axis. + * + * @param offset + */ + public setDragOffsetX(offset: number) { + this.mTransOffsetX = Utils.convertDpToPixel(offset); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the y-axis. + * + * @param offset + */ + public setDragOffsetY(offset: number) { + this.mTransOffsetY = Utils.convertDpToPixel(offset); + } + + /** + * Returns true if both drag offsets (x and y) are zero or smaller. + * + * @return + */ + public hasNoDragOffset(): boolean { + return this.mTransOffsetX <= 0 && this.mTransOffsetY <= 0; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the x-axis + * + * @return + */ + public canZoomOutMoreX(): boolean { + return this.mScaleX > this.mMinScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the x-axis + * + * @return + */ + public canZoomInMoreX(): boolean { + return this.mScaleX < this.mMaxScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the y-axis + * + * @return + */ + public canZoomOutMoreY(): boolean { + return this.mScaleY > this.mMinScaleY; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the y-axis + * + * @return + */ + public canZoomInMoreY(): boolean { + return this.mScaleY < this.mMaxScaleY; + } +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowConstant.ets b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowConstant.ets index fa3741d..082c5ff 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowConstant.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowConstant.ets @@ -1,31 +1,25 @@ - /* - * Copyright (C) 2022 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export const CURRENTNOW = 0 - -export const SHELLBACKTEMP = 1 - -export const DDRFREQUENCY = 2 - -export const GPUFREQUENCY = 3 - -export const CPU0FREQUENCY = 4 - -export const CPU1FREQUENCY = 5 - -export const CPU2FREQUENCY = 6 - -export const RAM = 6 - -export const FPS = 7 \ No newline at end of file +/* +* Copyright (C) 2022 Huawei Device Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export default class FloatWindowConstant { + static CURRENT_NOW = 0 + static SHELL_BACK_TEMP = 1 + static DDR_FREQUENCY = 2 + static GPU_FREQUENCY = 3 + static CPU0_FREQUENCY = 4 + static CPU1_FREQUENCY = 5 + static CPU2_FREQUENCY = 6 + static RAM = 6 + static FPS = 7 +} diff --git a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowFun.ts b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowFun.ts index b5f2298..a6914a6 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowFun.ts +++ b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/FloatWindowFun.ts @@ -20,7 +20,7 @@ const TAG = "FloatWindowFun" export class FloatWindowFun { static floatingWindowOffsetX: number = 50 - static floatingWindowOffsetY: number = 200 + static floatingWindowOffsetY: number = 300 static titleWindowOffsetX: number = 300 static titleWindowOffsetY: number = 200 static lineChartWindowOffsetX: number= 700 @@ -33,19 +33,17 @@ export class FloatWindowFun { globalThis.CreateFloatingWindow = (() => { //5.5SP2 2106 改成 8 wm.create(globalThis.abilityContext, 'floatingWindow', 2106).then((floatWin) => { - floatWin.setWindowMode(102).then(() => { - floatWin.moveTo(this.floatingWindowOffsetX, this.floatingWindowOffsetY).then(() => { - floatWin.resetSize(95, 95).then(() => { - floatWin.getProperties().then((property) => { - property.isTransparent = false - }) - floatWin.loadContent('pages/FloatBall').then(() => { - floatWin.setTransparent(true).then(() => { //透明 - floatWin.show().then(() => { - globalThis.showFloatingWindow = true - }) - + floatWin.moveTo(this.floatingWindowOffsetX, this.floatingWindowOffsetY).then(() => { + floatWin.resetSize(95, 95).then(() => { + floatWin.getProperties().then((property) => { + property.isTransparent = false + }) + floatWin.loadContent('pages/FloatBall').then(() => { + floatWin.setBackgroundColor("#00000000").then(() => { //透明 + floatWin.show().then(() => { + globalThis.showFloatingWindow = true }) + }) }) }) @@ -76,17 +74,15 @@ export class FloatWindowFun { globalThis.CreateTitleWindow = (() => { wm.create(globalThis.abilityContext, 'TitleWindow', 2106).then((floatWin) => { - floatWin.setWindowMode(102).then(() => { - floatWin.moveTo(this.titleWindowOffsetX, this.titleWindowOffsetY).then(() => { - floatWin.resetSize(350, 260).then(() => { - floatWin.getProperties().then((property) => { - property.isTransparent = false - }) - floatWin.loadContent('pages/TitleWindowPage').then(() => { - floatWin.setTransparent(true) - floatWin.hide() - SPLogger.DEBUG(TAG, 'CreateTitleWindow Done') - }) + floatWin.moveTo(this.titleWindowOffsetX, this.titleWindowOffsetY).then(() => { + floatWin.resetSize(350, globalThis.screenWith > 800 ? 480 : 360).then(() => { + floatWin.getProperties().then((property) => { + property.isTransparent = false + }) + floatWin.loadContent('pages/TitleWindowPage').then(() => { + floatWin.setBackgroundColor("#4d000000") + floatWin.hide() + SPLogger.DEBUG(TAG, 'CreateTitleWindow Done') }) }) }) @@ -125,56 +121,6 @@ export class FloatWindowFun { }) }) - globalThis.CreateFPSLineChartWindow = (() => { - //5.5SP2 2106 改成 8 - wm.create(globalThis.abilityContext, 'fpsLineChartWindow', 2106).then((floatWin) => { - floatWin.setWindowMode(102).then(() => { - floatWin.moveTo(this.lineChartWindowOffsetX, this.lineChartWindowOffsetY).then(() => { - floatWin.resetSize(130,90).then(() => { - floatWin.loadContent('pages/FpsLineChartPage').then(() => { - floatWin.show().then(() => { - floatWin.hide() - }) - }) - }) - }) - }) - }) - }) - - globalThis.MoveFPSLineChartWindow = ((offsetX: number, offsetY: number) => { - var xx = (this.lineChartWindowOffsetX + offsetX * 2) < 0 ? 0 : ((this.lineChartWindowOffsetX + offsetX * 2) > (this.windowWidth - 200) ? (this.windowWidth - 200) : (this.lineChartWindowOffsetX + offsetX * 2)) - var yy = (this.lineChartWindowOffsetY + offsetY * 2) < 0 ? 0 : ((this.lineChartWindowOffsetY + offsetY * 2) > (this.windowHeight - 200) ? (this.windowHeight - 200) : (this.lineChartWindowOffsetY + offsetY * 2)) - - wm.find("fpsLineChartWindow").then((fltWin) => { - fltWin.moveTo(xx, yy) - }) - }) - - globalThis.SetFPSLineChartWindowPosition = ((offsetX: number, offsetY: number) => { - this.lineChartWindowOffsetX = (this.lineChartWindowOffsetX + offsetX * 2) < 0 ? 0 : ((this.lineChartWindowOffsetX + offsetX * 2) > (this.windowWidth - 200) ? (this.windowWidth - 200) : (this.lineChartWindowOffsetX + offsetX * 2)) - this.lineChartWindowOffsetY = (this.lineChartWindowOffsetY + offsetY * 2) < 0 ? 0 : ((this.lineChartWindowOffsetY + offsetY * 2) > (this.windowHeight - 200) ? (this.windowHeight - 200) : (this.lineChartWindowOffsetY + offsetY * 2)) - }) - - globalThis.DestroyFPSLineChartWindow = (() => { - wm.find("fpsLineChartWindow").then((fltWin) => { - fltWin.destroy().then(() => { - globalThis.showFPSLineChartWindow = false - }) - }) - }) - - globalThis.HideFPSLineChartWindow = (() => { - wm.find("fpsLineChartWindow").then((fltWin) => { - fltWin.hide() - }) - }) - - globalThis.ShowFPSLineChartWindow = (() => { - wm.find("fpsLineChartWindow").then((fltWin) => { - fltWin.show() - }) - }) } } diff --git a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/utils/FloatWindowUtils.ets b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/utils/FloatWindowUtils.ets index 171bdcd..453541f 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/floatwindow/utils/FloatWindowUtils.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/floatwindow/utils/FloatWindowUtils.ets @@ -18,8 +18,8 @@ export let floatingWindowOffsetX: number = 50 export let floatingWindowOffsetY: number = 200 export let titleWindowOffsetX: number = 300 export let titleWindowOffsetY: number = 200 -export let lineChartWindowOffsetX:number=1 -export let lineChartWindowOffsetY:number=1 +export let lineChartWindowOffsetX:number=30 +export let lineChartWindowOffsetY:number=20 export let windowWidth: number = 2560 export let windowHeight: number = 1600 @@ -32,21 +32,34 @@ export function initFloatWindow() { createFloatWindow("cpu1Frequency") createFloatWindow("cpu2Frequency") createFloatWindow("RAM") + createFloatWindow("FPS") } export function createFloatWindow(floatName:string) { //5.5SP2 2106 改成 8 wm.create(globalThis.abilityContext, floatName, 2106).then((floatWin) => { - floatWin.setWindowMode(102).then(() => { floatWin.moveTo(lineChartWindowOffsetX, lineChartWindowOffsetY).then(() => { - floatWin.resetSize(240,180).then(() => { + floatWin.resetSize(200,150).then(() => { + floatWin.getProperties().then((property) => { + property.isTransparent = false + }) if(floatName=="currentNow"){ floatWin.loadContent('pages/CurrentNowLineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) }) }else if(floatName=="shellBackTemp"){ floatWin.loadContent('pages/ShellBackTempLineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -54,6 +67,11 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="ddrFrequency"){ floatWin.loadContent('pages/DDRLineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -61,6 +79,11 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="gpuFrequency"){ floatWin.loadContent('pages/GPULineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -68,6 +91,11 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="cpu0Frequency"){ floatWin.loadContent('pages/CPU0LineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -75,6 +103,11 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="cpu1Frequency"){ floatWin.loadContent('pages/CPU1LineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -82,6 +115,11 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="cpu2Frequency"){ floatWin.loadContent('pages/CPU2LineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -89,6 +127,22 @@ export function createFloatWindow(floatName:string) { } else if(floatName=="RAM"){ floatWin.loadContent('pages/RAMLineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) + floatWin.show().then(() => { + floatWin.hide() + }) + }) + } else if(floatName=="FPS"){ + floatWin.loadContent('pages/FpsLineChartPage').then(() => { + floatWin.setBackgroundColor("#B3000000").then(() => { //透明 + console.log("xhq win setTransparent end."); + floatWin.setBackgroundColor("#59020000").then(() => { //背景颜色 + }) + }) floatWin.show().then(() => { floatWin.hide() }) @@ -96,10 +150,20 @@ export function createFloatWindow(floatName:string) { } }) }) - }) }) } -export function destoryFloatWindow(floatName:string) { +export function destoryAllFloatWindow() { + destroyFloatWindow("currentNow") + destroyFloatWindow("shellBackTemp") + destroyFloatWindow("ddrFrequency") + destroyFloatWindow("gpuFrequency") + destroyFloatWindow("cpu0Frequency") + destroyFloatWindow("cpu1Frequency") + destroyFloatWindow("cpu2Frequency") + destroyFloatWindow("RAM") + destroyFloatWindow("FPS") +} +export function destroyFloatWindow(floatName:string) { wm.find(floatName).then((fltWin) => { fltWin.destroy().then(() => { diff --git a/device/device_ui/entry/src/main/ets/common/ui/main/Home.ets b/device/device_ui/entry/src/main/ets/common/ui/main/Home.ets index fbf96d9..7163248 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/main/Home.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/main/Home.ets @@ -17,8 +17,10 @@ import router from '@system.router'; import prompt from '@system.prompt'; import { TopComponent } from '../main/TopComponent'; import { otherSupportList, OtherSupport, TestMode } from '../../entity/LocalConfigEntity'; +import SPLogger from '../../utils/SPLogger' +const TAG = "Home" /* home页 */ @@ -29,7 +31,7 @@ export struct Home { @State private circleHeight: string = '180vp' @State private circleWidth: string = '180vp' @State private circleRadius: string = '90vp' - @State opacity: number = 0.6 + @State atOpacity: number = 0.6 build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Start }) { @@ -69,7 +71,7 @@ export struct Home { .width(this.circleWidth) .height(this.circleHeight) .border({ width: '1vp', radius: this.circleRadius, color: $r("app.color.color_80fff") }) - .opacity(this.opacity) + .opacity(this.atOpacity) .animation({ duration: 1000, iterations: -1, @@ -78,7 +80,7 @@ export struct Home { this.circleWidth = '220vp' this.circleHeight = '220vp' this.circleRadius = '100vp' - this.opacity = 0 + this.atOpacity = 0 }) }.onClick(() => { if (!globalThis.showFloatingWindow) { @@ -97,6 +99,7 @@ export struct Home { }.backgroundColor($r('app.color.colorPrimary')).width('100%') .alignItems(HorizontalAlign.Center) .onAppear(() => { + SPLogger.DEBUG(TAG, "deviceInfo.brand:->" + deviceInfo.brand + "deviceInfo.productModel:->" + deviceInfo.productModel ) this.deviceStr = deviceInfo.brand + " " + deviceInfo.productModel }) diff --git a/device/device_ui/entry/src/main/ets/common/ui/main/Mine.ets b/device/device_ui/entry/src/main/ets/common/ui/main/Mine.ets index 6b90577..b8d4f19 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/main/Mine.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/main/Mine.ets @@ -89,7 +89,7 @@ export struct Mine { } .width('100%') .backgroundColor(0xeeeeee) - .height('35vp') + .height('50vp') .borderRadius('5vp') .onClick( res => { @@ -105,7 +105,7 @@ export struct Mine { Text('设置').fontSize('16vp').margin('5vp') } .width('100%') - .height('35vp') + .height('50vp') .backgroundColor(0xeeeeee) .borderRadius('5vp') .onClick( diff --git a/device/device_ui/entry/src/main/ets/common/ui/main/Report.ets b/device/device_ui/entry/src/main/ets/common/ui/main/Report.ets index 108b43f..0394c27 100644 --- a/device/device_ui/entry/src/main/ets/common/ui/main/Report.ets +++ b/device/device_ui/entry/src/main/ets/common/ui/main/Report.ets @@ -73,6 +73,10 @@ export struct Report { }) }.margin({ left: '5%', top: '2%', bottom: '2%', right: '2%' }).onClick(() => { + if(item.testDuration <= 3) { + prompt.showToast({message: "测试时长过短", duration: 1000}) + return + } let databasePath: string = item.dbPath //截取时间戳 let timeStamp = databasePath.substring(databasePath.lastIndexOf("/") + 1, databasePath.length) diff --git a/device/device_ui/entry/src/main/ets/common/utils/BundleMangerUtils.ts b/device/device_ui/entry/src/main/ets/common/utils/BundleMangerUtils.ts index b87ed0a..d00b1f9 100644 --- a/device/device_ui/entry/src/main/ets/common/utils/BundleMangerUtils.ts +++ b/device/device_ui/entry/src/main/ets/common/utils/BundleMangerUtils.ts @@ -22,14 +22,7 @@ import SPLogger from '../utils/SPLogger' */ const TAG = "BundleManager" export default class BundleManager { - static async getResourceManager(context, bundleName, then?: Function) { - if (!then) { - return await ResMgr.getResourceManager(context, bundleName); - } - ResMgr.getResourceManager(context, bundleName).then(then) - } - - //根据包名获取base64 + //根据包名获取base64 static async getIconByBundleName(mBundleNameArr: Array): Promise> { let mBundleNames = Array.from(new Set(mBundleNameArr)) let mMap = new Map @@ -40,12 +33,19 @@ export default class BundleManager { let bundleName = data[j].name for (let i = 0; i < mBundleNames.length; i++) { if (mBundleNames[i] == bundleName) { - let item = await BundleManager.getResourceManager(globalThis.abilityContext, mBundleNames[i]) - let icon = await item.getMediaBase64(data[j].iconId) - mMap.set(mBundleNames[i], icon) + let bundleContext = globalThis.abilityContext.createBundleContext(mBundleNames[i]) + await bundleContext.resourceManager.getMediaBase64(data[j].iconId).then((value)=> { + if (value != null) { + mMap.set(mBundleNames[i], value) + } + }); + } } } + }).catch((error) => { + SPLogger.ERROR(TAG,'Operation failed. Cause: ' + JSON.stringify(error)) + console.error('BundleManager ... Operation failed. Cause: ' + JSON.stringify(error)); }) return mMap } @@ -58,10 +58,11 @@ export default class BundleManager { SPLogger.INFO(TAG,'xxx getAllApplicationInfo data length [' + data.length + ']') for (let i = 0; i < data.length; i++) { let bundleName = data[i].name - let item = await BundleManager.getResourceManager(globalThis.abilityContext, bundleName) + let bundleContext = globalThis.abilityContext.createBundleContext(data[i + ].name) try { - let appName = await item.getString(data[i].labelId) - let icon = await item.getMediaBase64(data[i].iconId) + let appName = await bundleContext.resourceManager.getString(data[i].labelId) + let icon = await bundleContext.resourceManager.getMediaBase64(data[i].iconId) bundle.getBundleInfo(bundleName, 1).then(bundleData => { BundleManager.getAbility(bundleName).then(abilityName => { console.info("index[" + i + "].getAbility for begin data: ", abilityName); @@ -72,6 +73,9 @@ export default class BundleManager { SPLogger.ERROR(TAG,"index[" + i + "] getAllApplicationInfo err" + err); } } + }).catch((error) => { + SPLogger.ERROR(TAG,'Operation failed. Cause: ' + JSON.stringify(error)) + console.error('BundleManager ... Operation failed. Cause: ' + JSON.stringify(error)); }) return appInfoList } diff --git a/device/device_ui/entry/src/main/ets/common/utils/CSVUtils.ts b/device/device_ui/entry/src/main/ets/common/utils/CSVUtils.ts index c802132..76ccd62 100644 --- a/device/device_ui/entry/src/main/ets/common/utils/CSVUtils.ts +++ b/device/device_ui/entry/src/main/ets/common/utils/CSVUtils.ts @@ -34,6 +34,7 @@ export function csvTIndexInfo(tIndexInfos: Array): string { for (let k of Object.keys(t)) { data += t[k] + "," } + data = data.substring(0, data.lastIndexOf(",")) data += "\n" } let result = tittle + data diff --git a/device/device_ui/entry/src/main/ets/pages/AppSelectPage.ets b/device/device_ui/entry/src/main/ets/pages/AppSelectPage.ets index 84cab70..68f3752 100644 --- a/device/device_ui/entry/src/main/ets/pages/AppSelectPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/AppSelectPage.ets @@ -12,11 +12,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { StartTestTitleComponent } from '../common/ui/StartTestTitleComponent'; import { AppInfoItem } from '../common/entity/LocalConfigEntity'; import router from '@system.router'; +import SPLogger from '../common/utils/SPLogger' + +const TAG = "AppSelectPage" /** * app应用选择页 */ @@ -34,6 +36,9 @@ struct AppSelectPage { @Component struct appInfoComponent { @State appInfoList: Array = globalThis.appList + aboutToAppear() { + + } build() { List() { diff --git a/device/device_ui/entry/src/main/ets/pages/CPU0LineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/CPU0LineChartPage.ets index 13c701f..5201ad0 100644 --- a/device/device_ui/entry/src/main/ets/pages/CPU0LineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/CPU0LineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow} from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow} from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -35,10 +35,10 @@ struct CPU0LineChartPage { if (this.data.length >= 22) { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.cpu0Frequency == undefined) { + if (globalThis.cpu0Frequency == undefined) { this.cpu0Frequency = 0 } else { - this.cpu0Frequency = parseInt(globalThis.tTndex.cpu0Frequency) / 1e3 + this.cpu0Frequency = parseInt(globalThis.cpu0Frequency) / 1e3 if (this.cpu0Frequency == 0) { this.data.push(0) @@ -59,18 +59,18 @@ struct CPU0LineChartPage { Stack({ alignContent: Alignment.Top }) { FloatWindowComponent({ title: `cpu0Frequency`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text(this.floatName + ":" + this.cpu0Frequency + "Hz") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + Text(this.floatName + ":" + this.cpu0Frequency + "MHz") + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%').opacity(0.2) + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/CPU1LineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/CPU1LineChartPage.ets index 7c8f8a0..e648ed1 100644 --- a/device/device_ui/entry/src/main/ets/pages/CPU1LineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/CPU1LineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,10 +36,10 @@ struct CPU1LineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.cpu4Frequency == undefined) { + if (globalThis.cpu1Frequency == undefined) { this.cpu1Frequency = 0 } else { - this.cpu1Frequency = parseInt(globalThis.tTndex.cpu4Frequency) / 1e3 + this.cpu1Frequency = parseInt(globalThis.cpu1Frequency) / 1e3 if (this.cpu1Frequency == 0) { this.data.push(0) @@ -60,18 +60,18 @@ struct CPU1LineChartPage { Stack({ alignContent: Alignment.Top }) { FloatWindowComponent({ title: `cpu1Frequency`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text(this.floatName + ":" + this.cpu1Frequency + "Hz") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + Text(this.floatName + ":" + this.cpu1Frequency + "MHz") + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) console.log("hideFloatWindow---------------------" + this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/CPU2LineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/CPU2LineChartPage.ets index 4f25eb5..aa4188e 100644 --- a/device/device_ui/entry/src/main/ets/pages/CPU2LineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/CPU2LineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {hideFloatWindow} from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,10 +36,10 @@ struct CPU2LineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.cpu7Frequency == undefined) { + if (globalThis.cpu2Frequency == undefined) { this.cpu2Frequency = 0 } else { - this.cpu2Frequency = parseInt(globalThis.tTndex.cpu7Frequency) / 1e3 + this.cpu2Frequency = parseInt(globalThis.cpu2Frequency) / 1e3 if (this.cpu2Frequency == 0) { this.data.push(0) @@ -59,18 +59,18 @@ struct CPU2LineChartPage { Stack({ alignContent: Alignment.Top }) { FloatWindowComponent({ title: `cpu2Frequency`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text(this.floatName + ":" + this.cpu2Frequency + "Hz") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + Text(this.floatName + ":" + this.cpu2Frequency + "MHz") + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) console.log("hideFloatWindow---------------------" + this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/CurrentNowLineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/CurrentNowLineChartPage.ets index e7ddaf0..e5502e9 100644 --- a/device/device_ui/entry/src/main/ets/pages/CurrentNowLineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/CurrentNowLineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {hideFloatWindow} from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,10 +36,10 @@ struct CurrentNowLineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.currentNow == undefined) { + if (globalThis.currentNow == undefined) { this.currentNow = 0 } else { - this.currentNow = globalThis.tTndex.currentNow + this.currentNow = globalThis.currentNow if (this.currentNow == 0) { this.data.push(0) @@ -60,17 +60,17 @@ struct CurrentNowLineChartPage { FloatWindowComponent({ title: `currentNow`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text(this.floatName + ":" + this.currentNow + "mA") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) console.log("hideFloatWindow---------------------" + this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/DDRLineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/DDRLineChartPage.ets index ba8dff5..a3e9e1d 100644 --- a/device/device_ui/entry/src/main/ets/pages/DDRLineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/DDRLineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,15 +36,15 @@ struct DDRNowLineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.ddrFrequency == undefined) { + if (globalThis.ddrFreq == undefined) { this.DDR = 0 } else { - this.DDR = Number(globalThis.tTndex.ddrFrequency / 1e6).valueOf() + this.DDR = Number(globalThis.ddrFreq / 1e6).valueOf() if (this.DDR == 0) { this.data.push(0) } else { - let lineCount: number = this.DDR / 30 + let lineCount: number = this.DDR / 15 this.data.push(Math.abs(lineCount)) //在末尾填充一个元素 } this.random = Math.random() @@ -60,18 +60,18 @@ struct DDRNowLineChartPage { FloatWindowComponent({ title: `ddrFrequency`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text(this.floatName + ":" + this.DDR + "MHz") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) console.log("hideFloatWindow---------------------" + this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/FloatBall.ets b/device/device_ui/entry/src/main/ets/pages/FloatBall.ets index a0c5f08..0581efa 100644 --- a/device/device_ui/entry/src/main/ets/pages/FloatBall.ets +++ b/device/device_ui/entry/src/main/ets/pages/FloatBall.ets @@ -13,23 +13,23 @@ * limitations under the License. */ import { secToTime } from '../common/utils/TimeUtils'; -import { TaskStatus,CatchTraceStatus} from '../common/profiler/base/ProfilerConstant'; -import { MainWorker } from "../common/profiler/MainWorkTask" +import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; import { ProfilerTask } from "../common/profiler/ProfilerTask" -import { initFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' -let traceTime = 0 -let socketCollectItems -let isCollectRam = false +import { initFloatWindow,destoryAllFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' +import WorkerHandler from '../common/profiler/WorkerHandler'; +import worker from '@ohos.worker'; +let MainWorker = globalThis.MainWorker +MainWorker.onmessage = function (result) { + WorkerHandler.socketHandler(result) +} @Entry @Component struct FloatBall { @State playerState: number = TaskStatus.task_init @State timerNum: number = 0 - @State FpsTimer: number = -1 timerId: number = -1 @State windShowState: boolean = false - @State windFpsShowState: boolean = false offsetX: number = -1 offsetY: number = -1 //解決手势失效的问题 @@ -45,97 +45,46 @@ struct FloatBall { ProfilerTask.getInstance().taskInit() //创建TiTile窗 globalThis.CreateTitleWindow() - globalThis.CreateFPSLineChartWindow() globalThis.task_status = TaskStatus.task_init initFloatWindow() - if (globalThis.collectConfigs.trace) { - globalThis.catchTraceState = CatchTraceStatus.catch_trace_start - globalThis.jitterTrace = false - } - +// this.initCollectPid() } +// initCollectPid(){ +// //尝试循环刷新 当前running abilitysInfo 获取pid +// globalThis.collectIntervalPid = setInterval(()=>{ +// //初始化采集目标进程pid +// getPidOfAbility(globalThis.collectPkg).then(pid=>{ +// globalThis.processPid = pid +// if(pid != "-1"){ +// clearInterval(globalThis.collectIntervalPid) +// SPLogger.DEBUG(TAG, "clear collectIntervalPid and cur pid:" + pid) +// } +// }) +// },1000) +// } + initAllCollect() { + console.log("collectIntervalCollect initAllCollect...."); if (globalThis.collectConfigs != -1 && globalThis.collectPkg != -1) { + if(globalThis.collectConfigs.screen_capture){ + MainWorker.postMessage({"screen_capture":true}) + } + if(globalThis.collectConfigs.trace){ + MainWorker.postMessage({"catch_trace_start":true}) + } globalThis.collectIntervalCollect = setInterval(() => { - if (globalThis.timerFps == undefined) { - } else { - this.FpsTimer = globalThis.timerFps - } + console.log("collectIntervalCollect running...."); if (this.playerState == TaskStatus.task_running) { ProfilerTask.getInstance().taskStart() - } - }, 1000) - globalThis.collectSocketCollect = setInterval(() => { - if (this.playerState == TaskStatus.task_running) { this.timerNum++ - isCollectRam = false - if ((globalThis.collectIntervalNum % 5 == 0) && globalThis.collectConfigs.RAM) { - isCollectRam = true - } else { - isCollectRam = false - } - - socketCollectItems = { - "pkg": globalThis.collectPkg, - "ram": isCollectRam, - "fps": globalThis.collectConfigs.FPS, - "screen_capture": globalThis.collectConfigs.screen_capture, - "is_video": globalThis.collectConfigs.is_video, - "is_camera": globalThis.collectConfigs.is_camera - } - - if (globalThis.collectConfigs.trace) { - this.taskTrace(socketCollectItems) - } - MainWorker.postMessage(socketCollectItems) } + console.log("collectIntervalCollect running status::"+ this.playerState); }, 1000) } globalThis.task_status = TaskStatus.task_running this.playerState = TaskStatus.task_running - } - - getJitterTrace() { - if (globalThis.fpsJitterArr != undefined && globalThis.fpsJitterArr != null && globalThis.fpsJitterArr != "") { - let tempQueue: Array = globalThis.fpsJitterArr - let curJitter = tempQueue.pop() - let tempJitterArr = curJitter.split("==") - for (var i = 0; i < tempJitterArr.length; i++) { - let tmp = tempJitterArr[i] - let jitter = parseInt(tmp) / 1e6 - if (jitter > 100) { - globalThis.jitterTrace = true - return - } - } - } - } - - taskTrace(defaultSocketCollectItems) { - if (globalThis.catchTraceState == CatchTraceStatus.catch_trace_start) { //开始 - defaultSocketCollectItems["catch_trace_start"] = true - globalThis.catchTraceState = CatchTraceStatus.catch_trace_first_running - } else if (globalThis.catchTraceState == CatchTraceStatus.catch_trace_finish) { //抓取 - defaultSocketCollectItems["catch_trace_finish"] = true - defaultSocketCollectItems["traceName"] = this.timerNum + "" - globalThis.catchTraceState = CatchTraceStatus.catch_trace_running; - traceTime = 0 - } else if (globalThis.catchTraceState == CatchTraceStatus.catch_trace_first_running) { //第一次运行 - if (globalThis.jitterTrace) { //抓取 - defaultSocketCollectItems["catch_trace_finish"] = true - defaultSocketCollectItems["traceName"] = this.timerNum + "" - globalThis.catchTraceState = CatchTraceStatus.catch_trace_running; - traceTime = 0 - globalThis.jitterTrace = false - } - } else if (globalThis.catchTraceState == CatchTraceStatus.catch_trace_running) { //有时间间隔的运行 - traceTime++ - if (traceTime > CatchTraceStatus.catch_trace_times) { - globalThis.catchTraceState = CatchTraceStatus.catch_trace_start - } - } - socketCollectItems = defaultSocketCollectItems + console.log("collectIntervalCollect initAllCollect finished...."); } singleEvent() { @@ -149,16 +98,6 @@ struct FloatBall { } } - fpsEvent(){ - if (this.windFpsShowState) { - globalThis.HideFPSLineChartWindow() - this.windFpsShowState = false - } else { - globalThis.ShowFPSLineChartWindow() - this.windFpsShowState = true - } - } - doubleEvent() { // 双击启动悬浮TITLE if (this.windShowState) { @@ -179,13 +118,14 @@ struct FloatBall { destroyAllWindow(){ globalThis.DestroyFloatingWindow() globalThis.DestroyTitleWindow() - globalThis.DestroyFPSLineChartWindow() + destoryAllFloatWindow() } clearAllInterVal(){ + if(globalThis.collectConfigs.trace){ + MainWorker.postMessage({"catch_trace_end":true}) + } clearInterval(globalThis.collectIntervalCollect) - clearInterval(globalThis.collectSocketCollect) - clearInterval(globalThis.collectOtherSocket) } MoveWindow(offsetX: number, offsetY: number) { @@ -212,13 +152,15 @@ struct FloatBall { colors: [[$r("app.color.colorPrimary"), 1.0], [$r("app.color.colorPrimary"), 1.0]] }) Text('start') - .fontSize(20) + .fontSize(18) .textAlign(TextAlign.Center) .fontColor($r("app.color.color_fff")) .width('100%') .height('100%') .onClick(() => { + console.log("collectIntervalCollect single click ...."); this.initAllCollect() + console.log("collectIntervalCollect single click finished...."); }) .gesture( GestureGroup(GestureMode.Exclusive, @@ -269,7 +211,7 @@ struct FloatBall { }) } Text(secToTime(this.timerNum).toString()) - .fontSize(20) + .fontSize('16fp') .textAlign(TextAlign.Center) .fontColor($r("app.color.color_fff")) .width('100%') diff --git a/device/device_ui/entry/src/main/ets/pages/FpsLineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/FpsLineChartPage.ets index c94fe4a..1c45b0f 100644 --- a/device/device_ui/entry/src/main/ets/pages/FpsLineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/FpsLineChartPage.ets @@ -12,34 +12,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' +import { FloatWindowComponent } from '../common/FloatWindowComponent' + @Entry @Component struct FpsLineChartPage { - private settings: RenderingContextSettings = new RenderingContextSettings(true) - private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) - private XPoint: number = 5 //x起始坐标 - private YPoint: number = 73 //Y起始坐标 - private XScale: number = 5 //刻度 - private YScale: number = 15 //刻度 - private XLength: number = 110 //X轴长度 - private YLength: number = 70 //Y轴长度 - data: number[]= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] //数据集合 - private MaxDataSize: number= this.XLength / this.XScale //数据集合的最大长度 - @State LineFps: number= 0 //FPS数值 - offsetX: number = -1 //悬浮框移动触摸点 X - offsetY: number = -1 //悬浮框移动触摸点 X + data: number[]= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] //数据集合 + @State random: number= 0 //用于刷新的随机数 + @State LineFps: number= 0 //数值 - MoveWindow(offsetX: number, offsetY: number) { - globalThis.MoveFPSLineChartWindow(offsetX, offsetY) - } - - SetWindowPosition(offsetX: number, offsetY: number) { - globalThis.SetFPSLineChartWindowPosition(offsetX, offsetY) - } + offsetX: number = -1 //悬浮框移动触摸点 X + offsetY: number = -1 //悬浮框移动触摸点 X + private floatName: string= "FPS" + task_state = 1 aboutToAppear() { globalThis.LineChartCollect = setInterval(() => { - if (this.data.length >= this.MaxDataSize) { + if (this.data.length >= 22) { console.log("GestureEvent--------------shift:" + this.data); this.data.shift() //移除第一个元素 } @@ -60,60 +50,23 @@ struct FpsLineChartPage { } build() { - Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) { - + Stack({ alignContent: Alignment.Top }) { + FloatWindowComponent({ title: `FPS`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text("FPS:" + this.LineFps).fontSize('6fp').fontColor($r("app.color.color_333")).margin({ left: 5, top: 3 }) //文本显示 + Text(this.floatName + ":" + this.LineFps + "fps") + .fontSize('6fp') + .fontColor($r("app.color.color_fff")) + .margin({ left: 5, top: 3 }) //文本显示 + Text(this.random + "") + .fontSize('1fp') + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { - globalThis.HideFPSLineChartWindow() - }) + hideFloatWindow(this.floatName) + console.log("hideFloatWindow---------------------" + this.floatName) + }).align(Alignment.TopEnd) }.height('15vp').width('100%') - Canvas(this.context) - .width('100%') - .height('100%') - .onReady(() => { - //Y轴 - this.context.clearRect(this.XPoint + 0.5, this.YPoint - this.YLength, this.XLength, this.YLength) - this.context.beginPath() - this.context.moveTo(this.XPoint, this.YPoint - this.YLength) - this.context.lineTo(this.XPoint, this.YPoint) - this.context.stroke() - //X轴 - this.context.beginPath() - this.context.moveTo(this.XPoint, this.YPoint) - this.context.lineTo(this.XPoint + this.XLength, this.YPoint) - this.context.stroke() - //K线绘制 - if (this.data.length > 1) { - for (let i = 1; i < this.data.length; i++) { - this.context.beginPath() - console.log("GestureEvent--------------beginPath:" + this.data[i-1]); - this.context.moveTo(this.XPoint + (i - 1) * this.XScale, this.YPoint - this.data[i-1]) - this.context.lineTo(this.XPoint + i * this.XScale, this.YPoint - this.data[i]) - this.context.stroke() - } - } - }) - } - .width('100%') - .height('100%').gesture( - GestureGroup(GestureMode.Exclusive, - PanGesture({}) - .onActionStart((event: GestureEvent) => { - - }) - .onActionUpdate((event: GestureEvent) => { - this.offsetX = event.offsetX - this.offsetY = event.offsetY - - }) - .onActionEnd(() => { - this.MoveWindow(this.offsetX, this.offsetY) - this.SetWindowPosition(this.offsetX, this.offsetY) - - }) - )) + } } } \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/pages/GPULineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/GPULineChartPage.ets index d28efd4..4bcac5a 100644 --- a/device/device_ui/entry/src/main/ets/pages/GPULineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/GPULineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -32,19 +32,20 @@ struct GPULineChartPage { globalThis.LineChartCollect = setInterval(() => { this.task_state = globalThis.task_status if (this.task_state == TaskStatus.task_running) { + if (this.data.length >= 22) { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.gpuFrequency == undefined) { + if (globalThis.gpuFrequency == undefined) { this.gpuFrequency = 0 } else { - this.gpuFrequency = Number((globalThis.tTndex.gpuFrequency / 1e6).toFixed(2)).valueOf() + this.gpuFrequency = Number((globalThis.gpuFrequency / 1e6).toFixed(2)).valueOf() if (this.gpuFrequency == 0) { this.data.push(0) } else { - let lineCount: number = this.gpuFrequency / 30 + let lineCount: number = this.gpuFrequency / 15 this.data.push(Math.abs(lineCount)) //在末尾填充一个元素 } this.random = Math.random() @@ -60,18 +61,18 @@ struct GPULineChartPage { FloatWindowComponent({ title: `gpuFrequency`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text(this.floatName + ":" + this.gpuFrequency + "MHz") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) console.log("hideFloatWindow---------------------" + this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } } diff --git a/device/device_ui/entry/src/main/ets/pages/LightAdjust.ets b/device/device_ui/entry/src/main/ets/pages/LightAdjust.ets index 2045b90..eb07259 100644 --- a/device/device_ui/entry/src/main/ets/pages/LightAdjust.ets +++ b/device/device_ui/entry/src/main/ets/pages/LightAdjust.ets @@ -15,7 +15,7 @@ import brightness from '@ohos.brightness'; import { StartTestTitleComponent } from '../common/ui/StartTestTitleComponent'; - +import SPLogger from '../common/utils/SPLogger' /** * 亮度调整 */ diff --git a/device/device_ui/entry/src/main/ets/pages/LoginPage.ets b/device/device_ui/entry/src/main/ets/pages/LoginPage.ets index fdc3572..7b1f36b 100644 --- a/device/device_ui/entry/src/main/ets/pages/LoginPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/LoginPage.ets @@ -12,17 +12,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import router from '@system.router'; @Entry @Component struct Login { - aboutToAppear() { - console.log("cur router length:" + router.getLength()) - console.log("cur router state:" + router.getState()) - } - build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { Column() { @@ -38,9 +32,7 @@ struct Login { .height('45vp') .textAlign(TextAlign.Center) .onClick(() => { - router.push({ uri: 'pages/MainPage', params: { - title: "123" - } }) + router.push({ uri: 'pages/MainPage' }) }) } .width('100%') diff --git a/device/device_ui/entry/src/main/ets/pages/MainPage.ets b/device/device_ui/entry/src/main/ets/pages/MainPage.ets index 0d2005c..5760688 100644 --- a/device/device_ui/entry/src/main/ets/pages/MainPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/MainPage.ets @@ -13,10 +13,10 @@ * limitations under the License. */ -import { Mine } from '../Common/ui/main/Mine'; -import { Home } from '../Common/ui/main/Home'; -import { Report } from '../Common/ui/main/Report'; -import { HomeBottomPage } from '../Common/ui/main/HomeBottomPage'; +import { Mine } from '../common/ui/main/Mine'; +import { Home } from '../common/ui/main/Home'; +import { Report } from '../common/ui/main/Report'; +import { HomeBottomPage } from '../common/ui/main/HomeBottomPage'; /* * 主页 程序主页面 */ diff --git a/device/device_ui/entry/src/main/ets/pages/RAMLineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/RAMLineChartPage.ets index 6967014..a269ba8 100644 --- a/device/device_ui/entry/src/main/ets/pages/RAMLineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/RAMLineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,16 +36,16 @@ struct RAMLineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.pss == undefined || isNaN(globalThis.tTndex.pss)) { + if (globalThis.pss == undefined || isNaN(globalThis.pss)) { this.pss = 0 } else { - this.pss = globalThis.tTndex.pss + this.pss = globalThis.pss if (this.pss == 0) { this.data.push(0) } else { let lineCount: number = this.pss / 2000 - console.log("shellBackTemp---------------------lineCount:" + lineCount) + this.data.push(Math.abs(lineCount)) //在末尾填充一个元素 } @@ -62,19 +62,17 @@ struct RAMLineChartPage { FloatWindowComponent({ title: `RAM`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text(this.floatName + ":" + this.pss + "KB") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') - - + }.height('20vp').width('100%') } } } \ No newline at end of file diff --git a/device/device_ui/entry/src/main/ets/pages/ReportDetail.ets b/device/device_ui/entry/src/main/ets/pages/ReportDetail.ets index b109879..6b36f75 100644 --- a/device/device_ui/entry/src/main/ets/pages/ReportDetail.ets +++ b/device/device_ui/entry/src/main/ets/pages/ReportDetail.ets @@ -17,8 +17,13 @@ import { secToTime } from '../common/utils/TimeUtils'; import { TIndexInfo } from '../common/entity/DatabaseEntity'; import router from '@system.router'; import { Summary } from '../common/ui/detail/Summary'; +import { Power } from '../common/ui/detail/Power'; +import { Performance } from '../common/ui/detail/Performance'; +import { Load } from '../common/ui/detail/Load'; +import { Temperature } from '../common/ui/detail/Temperature'; import { StartTestTitleComponent } from '../common/ui/StartTestTitleComponent'; import { ReportItem } from '../common/entity/LocalConfigEntity'; +import CheckEmptyUtils from'../common/utils/CheckEmptyUtils' @Entry @@ -29,8 +34,8 @@ struct ReportDetail { private reportItem: ReportItem = null aboutToAppear() { - let data = router.getParams()["gpData"] - let report = router.getParams()["reportItem"] + let data : any = router.getParams()["gpData"] + let report : any = router.getParams()["reportItem"] if (data != null) { this.gpData = data } @@ -84,7 +89,33 @@ struct ReportDetail { TabContent() { Summary({ gpData: this.gpData }) }.tabBar('概览') - } + + TabContent() { + Column() { + Performance({ gpData: this.gpData }) + }.width('100%').height('100%') + }.tabBar('性能') + + TabContent() { + Column() { + Load({ gpData: this.gpData }) + }.width('100%').height('100%') + }.tabBar('负载') + + if (!CheckEmptyUtils.checkStrIsEmpty(this.gpData[0].currentNow)) { + TabContent() { + Column() { + Power({ gpData: this.gpData }) + }.width('100%').height('100%') + }.tabBar('功耗') + } + + TabContent() { + Column() { + Temperature({ gpData: this.gpData }) + }.width('100%').height('100%') + }.tabBar('热') + }.backgroundColor("#f5f5f5") .barWidth(360) .scrollable(true) .barHeight(60) diff --git a/device/device_ui/entry/src/main/ets/pages/SettingsPage.ets b/device/device_ui/entry/src/main/ets/pages/SettingsPage.ets index f759678..015541d 100644 --- a/device/device_ui/entry/src/main/ets/pages/SettingsPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/SettingsPage.ets @@ -46,7 +46,7 @@ struct SettingsPage { .border({ radius: '20vp' }) .backgroundColor($r("app.color.colorPrimary")) .width('80%') - .height('40vp') + .height('45vp') .textAlign(TextAlign.Center) .onClick(res => { router.replace({ uri: 'pages/LoginPage' }) diff --git a/device/device_ui/entry/src/main/ets/pages/ShellBackTempLineChartPage.ets b/device/device_ui/entry/src/main/ets/pages/ShellBackTempLineChartPage.ets index a3008a0..452d904 100644 --- a/device/device_ui/entry/src/main/ets/pages/ShellBackTempLineChartPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/ShellBackTempLineChartPage.ets @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' +import { hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' import { FloatWindowComponent } from '../common/FloatWindowComponent' import { TaskStatus } from '../common/profiler/base/ProfilerConstant'; @@ -36,16 +36,16 @@ struct ShellBackTempLineChartPage { this.data.shift() //移除第一个元素 } - if (globalThis.tTndex.shellBackTemp == undefined) { + if (globalThis.shellBackTemp== undefined) { this.shellBackTemp = 0 } else { - this.shellBackTemp = globalThis.tTndex.shellBackTemp / 1e3 + this.shellBackTemp = globalThis.shellBackTemp console.log("shellBackTemp---------------------" + this.shellBackTemp) if (this.shellBackTemp == 0) { this.data.push(0) } else { console.log("shellBackTemp---------------------" + this.shellBackTemp) - let lineCount: number = this.shellBackTemp + let lineCount: number = parseInt(this.shellBackTemp.toString())/ 1e3 console.log("shellBackTemp---------------------lineCount:" + lineCount) this.data.push(Math.abs(lineCount)) //在末尾填充一个元素 @@ -63,18 +63,18 @@ struct ShellBackTempLineChartPage { FloatWindowComponent({ title: `shellBackTemp`, data: this.data }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text(this.floatName + ":" + this.shellBackTemp + "℃") - .fontSize('6fp') - .fontColor($r("app.color.color_333")) + .fontSize('10fp') + .fontColor($r("app.color.color_fff")) .margin({ left: 5, top: 3 }) //文本显示 Text(this.random + "") .fontSize('1fp') - .fontColor($r("app.color.color_333")).visibility(Visibility.None) + .fontColor($r("app.color.color_fff")).visibility(Visibility.None) - Image($r("app.media.icon_close_small")).width('15vp').height('15vp').onClick(() => { + Image($r("app.media.icon_close_small")).width('20vp').height('20vp').onClick(() => { hideFloatWindow(this.floatName) }).align(Alignment.TopEnd) - }.height('15vp').width('100%') + }.height('20vp').width('100%') } diff --git a/device/device_ui/entry/src/main/ets/pages/StartTestPage.ets b/device/device_ui/entry/src/main/ets/pages/StartTestPage.ets index 3f8fe3a..e8039e9 100644 --- a/device/device_ui/entry/src/main/ets/pages/StartTestPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/StartTestPage.ets @@ -19,9 +19,12 @@ import { switchList, SwitchItem, CollectItem } from '../common/entity/LocalConfi import { StartTestTitleComponent } from '../common/ui/StartTestTitleComponent'; import { ProfilerTask } from '../common/profiler/ProfilerTask' import { CollectorType } from '../common/profiler/base/ProfilerConstant' +import { getPidOfAbility } from '../common/utils/SystemUtils'; import prompt from '@system.prompt'; +import SPLogger from '../../ets/common/utils/SPLogger' +const TAG = "StartTestPage" /* * 测试配置页 */ @@ -31,7 +34,7 @@ struct StartTestPage { @State selectApp: string = "请选择一个应用" @State selectAppIcon: string = "" @State private collectConfigs: Array = new Array() - @State private switchList: Array = new Array() + @State private switchList: any = new Array() @State private testName: string = "" // 测试名称 dialogController: CustomDialogController = new CustomDialogController({ @@ -53,28 +56,27 @@ struct StartTestPage { aboutToAppear() { let supportMap = ProfilerTask - .getInstance() - .getSupports([ + .getInstance() + .getSupports([ CollectorType.TYPE_CPU.toString(), CollectorType.TYPE_GPU.toString(), CollectorType.TYPE_DDR.toString(), CollectorType.TYPE_FPS.toString(), CollectorType.TYPE_POWER.toString(), CollectorType.TYPE_TEMPERATURE.toString(), - CollectorType.TYPE_RAM.toString(), - CollectorType.TYPE_NET.toString(), - ]) + CollectorType.TYPE_RAM.toString() + ]) var iterator = supportMap.keys() for (var i = 0; i < supportMap.size; i++) { let key = iterator.next().value - let val = supportMap.get(key) - console.log("supportMap:item" + key + " " + val); + let val = supportMap.get(key).valueOf() this.collectConfigs.push( new CollectItem(key, val, val) ) } this.switchList = switchList + SPLogger.DEBUG(TAG, "this.switchList value" + JSON.stringify(this.switchList)) } build() { @@ -96,7 +98,7 @@ struct StartTestPage { Text(this.selectApp).fontSize('15fp').fontColor($r("app.color.color_333")) Image($r('app.media.icon_enter')).width('15vp').height('15vp').margin({ right: '35vp' }) - }.height('70vp').width('90%') + }.height('70vp').width('85%') } .height('100vp') .width('95%') @@ -118,11 +120,15 @@ struct StartTestPage { Image($r('app.media.icon_enter')).width('15vp').height('15vp').margin({ right: '15vp' }) }.height('60vp').width('90%').onClick(() => { + console.log("TestIndicators---------onClick") if (this.selectApp == "请选择一个应用" || this.selectApp == "SmartPerf") { + console.log("TestIndicators---------please choose app") prompt.showToast({ message: "please choose app!", duration: 1000 }) return } + console.log("TestIndicators---------dialogController.open()") this.dialogController.open() + console.log("TestIndicators---------dialogController.open End()") }) } .height('60vp') @@ -168,13 +174,9 @@ struct StartTestPage { return } - if (this.search("is_camera", this.switchList).isOpen && this.search("is_video", this.switchList).isOpen) { - prompt.showToast({ message: "video and camera can't be all selected", duration: 1000 }) - return - } - //启动app - commonStartAbility(router.getParams()["selectPackageName"], router.getParams()["selectAbilityName"]) + commonStartAbility(new String(router.getParams()["selectPackageName"]).toString(), new String(router.getParams()["selectAbilityName"]).toString()) + //启动悬浮窗 globalThis.CreateFloatingWindow() router.back({ uri: "pages/MainPage" }) @@ -189,17 +191,27 @@ struct StartTestPage { } onPageShow() { - console.info("startTestPage" + "onPageShow调用了"); - if (router.getParams()["appName"] == null) { + let routerParams:any = router.getParams() + let appName:any + let appVersion:any + let selectPackageName:any + let appIconId:any + if (routerParams != undefined && routerParams !=null) { + appName = routerParams["appName"] + appVersion = routerParams["appVersion"] + selectPackageName = routerParams["selectPackageName"] + appIconId = routerParams["appIconId"] + } + if (appName == null && appName == undefined) { this.selectApp = "请选择一个应用" this.testName = "" this.selectAppIcon = "" } else { - this.selectApp = router.getParams()["appName"] - globalThis.appName = router.getParams()["appName"] - globalThis.appVersion = router.getParams()["appVersion"] - globalThis.packageName = router.getParams()["selectPackageName"] - this.selectAppIcon = router.getParams()["appIconId"] + this.selectApp = new String(appName).toString() + globalThis.appName = new String(appName).toString() + globalThis.appVersion = new String(appVersion).toString() + globalThis.packageName = new String(selectPackageName).toString() + this.selectAppIcon = new String(appIconId).toString() let date = new Date() let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1); let D = date.getDate() + '-'; @@ -279,7 +291,7 @@ struct StartTestPage { @Component struct SwitchComponent { - @Link private switchList: Array + @Link private switchList: any build() { Column() { diff --git a/device/device_ui/entry/src/main/ets/pages/TitleWindowPage.ets b/device/device_ui/entry/src/main/ets/pages/TitleWindowPage.ets index 3a2b9e6..f2ad21b 100644 --- a/device/device_ui/entry/src/main/ets/pages/TitleWindowPage.ets +++ b/device/device_ui/entry/src/main/ets/pages/TitleWindowPage.ets @@ -13,25 +13,9 @@ * limitations under the License. */ import { TIndexInfo } from '../common/entity/DatabaseEntity' -import CheckEmptyUtils from '../common/utils/CheckEmptyUtils' -import { showFloatWindow, hideFloatWindow } from '../common/ui/floatwindow/utils/floatwindowutils' -import {FPS, GPUFREQUENCY, CPU0FREQUENCY, CPU1FREQUENCY, CPU2FREQUENCY, RAM } from '../common/ui/floatwindow/FloatWindowConstant' -import SPLogger from '../common/utils/SPLogger' - -//展示配置项 -export enum ShowItemConfig { - ITEM_CPU = 0X01, - ITEM_GPU = 0X01, - ITEM_DDR = 0X01, - ITEM_FPS = 0X00, - ITEM_POWER = 0X01, - ITEM_SOC_TEMP = 0X01, - ITEM_SHELL_FRAME = 0X01, - ITEM_SHELL_BACK = 0X01, - ITEM_SHELL_FRONT = 0X01, - ITEM_RAM = 0X01, - ITEM_UNKNOWN = 0X00, -} +import { showFloatWindow, hideFloatWindow } from '../common/ui/floatwindow/utils/FloatWindowUtils' +import FloatWindowConstant from '../common/ui/floatwindow/FloatWindowConstant' +import CommonEvent from '@ohos.commonEvent' @Component @@ -43,13 +27,13 @@ export struct ItemContent { build() { Row() { - Image(this.icon).width('20vp').height('20vp').margin({ right: '2%' }) + Image(this.icon).width(16).height(16).margin({ right: '2%' }) Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text(this.tittle).fontSize(20).fontColor(Color.White) - Text(this.value).fontSize(20).fontColor(Color.White) - }.width('88%').height('25vp') + Text(this.tittle).fontSize(10).fontColor(Color.White) + Text(this.value).fontSize(10).fontColor(Color.White) + }.width('88%').height(20) } - .height('26vp') + .height(22) .width('88%') .onClick(() => { this.onClickCallBack() @@ -61,22 +45,54 @@ export struct ItemContent { @Component struct TitleWindowPage { private data: boolean[]= [false, false, false, false, false, false, false, false] - //计时器 - @State timerNum: number= 0 @State tIndexInfo: TIndexInfo = new TIndexInfo() offsetX: number = -1 offsetY: number = -1 aboutToAppear() { - globalThis.floatIntervalCollect = setInterval(() => { - if (CheckEmptyUtils.isEmptyArr(globalThis.tTndex)) { - this.tIndexInfo.setDefaultValue() - SPLogger.DEBUG("TitleWindowPage","this.tIndexInfo isEmpty" + JSON.stringify(this.tIndexInfo)) + + let that = this + var subscriber + //订阅者信息 + var subscribeInfo = { + events: ["event"] + }; + //订阅公共事件回调 + function SubscribeCallBack(err, data) { + if (data.data == "") { } else { - this.tIndexInfo = globalThis.tTndex - SPLogger.DEBUG("TitleWindowPage","this.tIndexInfo isEmpty" + JSON.stringify(this.tIndexInfo)) + console.error("subscriberCurData:" + data.data); + that.tIndexInfo = JSON.parse(data.data) + globalThis.cpu0Frequency = that.tIndexInfo.cpu0Frequency + globalThis.cpu1Frequency = that.tIndexInfo.cpu1Frequency + globalThis.cpu2Frequency = that.tIndexInfo.cpu2Frequency + + if (that.tIndexInfo.cpu4Frequency != undefined && that.tIndexInfo.cpu7Frequency != undefined) { + globalThis.cpu1Frequency = that.tIndexInfo.cpu4Frequency + globalThis.cpu2Frequency = that.tIndexInfo.cpu7Frequency + that.tIndexInfo.cpu1Frequency = that.tIndexInfo.cpu4Frequency + that.tIndexInfo.cpu2Frequency = that.tIndexInfo.cpu7Frequency + that.tIndexInfo.cpu1Load = that.tIndexInfo.cpu1Load + that.tIndexInfo.cpu2Load = that.tIndexInfo.cpu2Load + } + + globalThis.currentNow = that.tIndexInfo.currentNow + globalThis.ddrFrequency = that.tIndexInfo.ddrFrequency + globalThis.lineFps = that.tIndexInfo.fps + globalThis.gpuFrequency = that.tIndexInfo.gpuFrequency + globalThis.pss = that.tIndexInfo.pss + globalThis.shellBackTemp = that.tIndexInfo.socThermalTemp + } - }, 999) + } + //创建订阅者回调 + function CreateSubscriberCallBack(err, data) { + subscriber = data; + //订阅公共事件 + CommonEvent.subscribe(subscriber, SubscribeCallBack); + } + //创建订阅者 + CommonEvent.createSubscriber(subscribeInfo, CreateSubscriberCallBack); } MoveWindow(offsetX: number, offsetY: number) { @@ -99,105 +115,120 @@ struct TitleWindowPage { build() { Stack({ alignContent: Alignment.Center }) { - Rect({ width: '100%', height: '100%' }).radius(10).opacity(0.5) + Rect({ width: '100%', height: '100%' }).radius(20).opacity(0.4) Column({ space: 2 }) { Row() { - Image($r("app.media.logo")).width('20vp').height('20vp').margin({ left: '2%' }) - Text( "SmartPerf") - .fontSize('20fp') + Image($r("app.media.logo")).width(10).height(10).margin({ left: '2%' }) + Text("SmartPerf") + .fontSize(12) .fontColor($r("app.color.color_fff")).margin({ left: '2%' }) - Image($r("app.media.icon_close_small")).height('18vp').width('18vp').margin({ left: '55%' }).onClick(() => { + Image($r("app.media.icon_close_small")).height(15).width(15).margin({ left: '55%' }).onClick(() => { //关闭实时悬浮框 globalThis.HideTitleWindow() }) - } .height('26vp') + }.height(20) .width('95%') - ItemContent({ - icon: $r("app.media.icon_average_frame_b"), - value: (this.tIndexInfo.fps.toString()) + "FPS", - tittle: "帧率", - onClickCallBack: () => { - this.floatWindowEvent("FPS",FPS) - } - }) - ItemContent({ - icon: $r("app.media.icon_counter"), - value: (parseInt(this.tIndexInfo.cpu0Frequency.toString()) / 1e3).toString() + "MHz", - tittle: "CPU0频率", - onClickCallBack: () => { - this.floatWindowEvent("cpu0Frequency", CPU0FREQUENCY) - } - }) - ItemContent({ - icon: $r("app.media.icon_counter"), - value: (parseInt(this.tIndexInfo.cpu1Frequency.toString()) / 1e3).toString() + "MHz", - tittle: "CPU1频率", - onClickCallBack: () => { - this.floatWindowEvent("cpu1Frequency", CPU1FREQUENCY) - } - }) - ItemContent({ - icon: $r("app.media.icon_counter"), - value: (parseInt(this.tIndexInfo.cpu2Frequency.toString()) / 1e3).toString() + "MHz", - tittle: "CPU2频率", - onClickCallBack: () => { - this.floatWindowEvent("cpu2Frequency", CPU2FREQUENCY) - } - }) - ItemContent({ - icon: $r("app.media.icon_frame_score"), - value: (parseInt(this.tIndexInfo.gpuFrequency.toString()) / 1e6).toString() + "MHz", - tittle: "GPU频点", - onClickCallBack: () => { - this.floatWindowEvent("gpuFrequency", GPUFREQUENCY) - } - }) - ItemContent({ - icon: $r("app.media.icon_jank_each_hour"), - value: this.tIndexInfo.pss + "KB", - tittle: "RAM", - onClickCallBack: () => { - this.floatWindowEvent("RAM", RAM) - } - }) - ItemContent({ - icon: $r("app.media.icon_max_temperature"), - value: (parseInt(this.tIndexInfo.socThermalTemp.toString()) / 1e3).toString() + "℃", - tittle: "SOC温度" - }) - ItemContent({ - icon: $r("app.media.icon_counter"), - value: this.tIndexInfo.networkUpSpeed.toString() + "kbps" + " " +this.tIndexInfo.networkDownSpeed.toString() + "kbps", - tittle: "带宽" - }) - // ItemContent({ - // icon: $r("app.media.icon_max_temperature"), - // value: (ParseIntValue(this.tIndexInfo.shellBackTemp.toString()) / 1e3).toString() + "℃", - // tittle: "后壳温" - // }) - // ItemContent({ - // icon: $r("app.media.icon_max_temperature"), - // value: (ParseIntValue(this.tIndexInfo.shellFrontTemp.toString()) / 1e3).toString() + "℃", - // tittle: "前壳温" - // }) - // ItemContent({ - // icon: $r("app.media.icon_max_temperature"), - // value: (ParseIntValue(this.tIndexInfo.shellFrameTemp.toString()) / 1e3).toString() + "℃", - // tittle: "壳温" - // }) - // ItemContent({ - // icon: $r("app.media.icon_normalized_current"), - // value: this.tIndexInfo.currentNow + "mA", - // tittle: "电流" - // }) - // ItemContent({ - // icon: $r("app.media.icon_counter"), - // value: (ParseIntValue(this.tIndexInfo.ddrFrequency.toString()) / 1e6).toString() + "MHz", - // tittle: "DDR频点" - // }) + if (this.tIndexInfo.fps != undefined) { + ItemContent({ + icon: $r("app.media.icon_average_frame_b"), + value: (this.tIndexInfo.fps.toString()) + "FPS", + tittle: "帧率", + onClickCallBack: () => { + this.floatWindowEvent("FPS", FloatWindowConstant.FPS) + } + }) + } + + if (this.tIndexInfo.currentNow != undefined) { + ItemContent({ + icon: $r("app.media.icon_normalized_current"), + value: (0 - Number(this.tIndexInfo.currentNow)).toString() + "mA", + tittle: "电流" + }) + } + if (this.tIndexInfo.ddrFrequency != undefined) { + ItemContent({ + icon: $r("app.media.icon_counter"), + value: (parseInt(this.tIndexInfo.ddrFrequency.toString()) / 1e6).toString() + "MHz", + tittle: "DDR频率" + }) + } + + if (this.tIndexInfo.cpu0Frequency != undefined) { + ItemContent({ + icon: $r("app.media.icon_counter"), + value: (parseInt(this.tIndexInfo.cpu0Frequency.toString()) / 1e3).toString() + "MHz " + this.tIndexInfo.cpu0Load + "%", + tittle: "CPU0频率", + onClickCallBack: () => { + this.floatWindowEvent("cpu0Frequency", FloatWindowConstant.CPU0_FREQUENCY) + } + }) + } + + if (this.tIndexInfo.cpu1Frequency != undefined) { + ItemContent({ + icon: $r("app.media.icon_counter"), + value: (parseInt(this.tIndexInfo.cpu1Frequency.toString()) / 1e3).toString() + "MHz " + this.tIndexInfo.cpu1Load + "%", + tittle: "CPU1频率", + onClickCallBack: () => { + this.floatWindowEvent("cpu1Frequency", FloatWindowConstant.CPU1_FREQUENCY) + } + }) + } + if (this.tIndexInfo.cpu2Frequency != undefined) { + + ItemContent({ + icon: $r("app.media.icon_counter"), + value: (parseInt(this.tIndexInfo.cpu2Frequency.toString()) / 1e3).toString() + "MHz " + this.tIndexInfo.cpu2Load + "%", + tittle: "CPU2频率", + onClickCallBack: () => { + this.floatWindowEvent("cpu2Frequency", FloatWindowConstant.CPU2_FREQUENCY) + } + }) + } + + if (this.tIndexInfo.gpuFrequency != undefined) { + ItemContent({ + icon: $r("app.media.icon_frame_score"), + value: (parseInt(this.tIndexInfo.gpuFrequency.toString()) / 1e6).toString() + "MHz " + this.tIndexInfo.gpuLoad + "%", + tittle: "GPU频点", + onClickCallBack: () => { + this.floatWindowEvent("gpuFrequency", FloatWindowConstant.GPU_FREQUENCY) + } + }) + } + if (this.tIndexInfo.pss != undefined) { + ItemContent({ + icon: $r("app.media.icon_jank_each_hour"), + value: this.tIndexInfo.pss + "KB", + tittle: "RAM", + onClickCallBack: () => { + this.floatWindowEvent("RAM", FloatWindowConstant.RAM) + } + }) + } + if (this.tIndexInfo.socThermalTemp != undefined) { + ItemContent({ + icon: $r("app.media.icon_max_temperature"), + value: (parseInt(this.tIndexInfo.socThermalTemp.toString()) / 1e3).toString() + "℃", + tittle: "SOC温度", + onClickCallBack: () => { + this.floatWindowEvent("shellBackTemp", FloatWindowConstant.SHELL_BACK_TEMP) + } + }) + } + + if (this.tIndexInfo.shellFrameTemp != undefined) { + ItemContent({ + icon: $r("app.media.icon_max_temperature"), + value: (parseInt(this.tIndexInfo.shellFrameTemp.toString()) / 1e3).toString() + "℃", + tittle: "壳温" + }) + } + }.width('100%') .gesture( GestureGroup(GestureMode.Exclusive, diff --git a/device/device_ui/entry/src/main/ets/workers/worker.js b/device/device_ui/entry/src/main/ets/workers/worker.js index a2146ba..e31ccb7 100644 --- a/device/device_ui/entry/src/main/ets/workers/worker.js +++ b/device/device_ui/entry/src/main/ets/workers/worker.js @@ -13,10 +13,10 @@ * limitations under the License. */ -import worker from '@ohos.worker'; // 导入worker模块 +import worker from '@ohos.worker'; import net_socket from '@ohos.net.socket'; - +const workTag = "SmartPerf::Work:: " let parentPort = worker.parentPort; // 获取parentPort属性 export let IPv4 = 1 @@ -24,7 +24,7 @@ export let IPv4 = 1 export let IPv4BindAddr = { address: "127.0.0.1", family: IPv4, - port: 8283 + port: 8282 } export let UdpSendAddress = { @@ -38,75 +38,58 @@ export let flagPackageNum = 0 export let udp = net_socket.constructUDPSocketInstance() udp.bind(IPv4BindAddr, err => { if (err) { - console.log('Worker socket bind fail'); + console.log(workTag + 'Worker socket bind fail'); return; } - console.log('Worker socket bind success'); + console.log(workTag + 'Worker socket bind success'); udp.getState((err, data) => { if (err) { - console.log('Worker socket getState fail'); + console.log(workTag + 'Worker socket getState fail'); return; } - console.log('Worker socket getState success:' + JSON.stringify(data)); + console.log(workTag + 'Worker socket getState success:' + JSON.stringify(data)); }) }) - -parentPort.onexit = function (e) { - console.log("Worker onexit") -} - -parentPort.onerror = function (e) { - console.log("Worker onerror") -} - -parentPort.onmessageerror = function (e) { - console.log("Worker onmessageerror") -} - -udp.on('listening', () => { - console.log("Worker socket on listening success"); -}); -udp.on('close', () => { - console.log("Worker socket on close success"); -}); - -udp.on('error', err => { - console.log("Worker socket on error, err:" + JSON.stringify(err)) -}); parentPort.onmessage = function (e) { let socketCollectItems = e.data - console.log("sub worker recv:" + JSON.stringify(e.data)); + console.log(workTag + "sub worker recv:" + JSON.stringify(e.data)); let messageSetPkg = "set_pkgName::" + socketCollectItems.pkg udp.getState((err, data) => { if (err) { - parentPort.postMessage("FPS$-1") - console.log("Worker socket getState error", err); + parentPort.postMessage("UdpStatus$-1") + console.log(workTag + "Worker socket getState error", err); + } + console.log(workTag + 'Worker socket getState success:' + JSON.stringify(data)); + + if (socketCollectItems.testConnection) { + for (let i = 0;i < 10; i++) { + udp.send({ + address: UdpSendAddress, + data: "set_pkgName::com.ohos.gameperceptio" + }) + console.log(workTag + "Worker socket test connection send"); + } } - console.log('Worker socket getState success:' + JSON.stringify(data)); if (flagPackageNum < 2) { - udp.send({ - address: UdpSendAddress, - data: messageSetPkg - }) + if (socketCollectItems.pkg != undefined) { + udp.send({ + address: UdpSendAddress, + data: messageSetPkg + }) + flagPackageNum++ + } } - flagPackageNum++ - if (socketCollectItems.fps) { - let messageFps = "get_fps_and_jitters::0::0" - if (socketCollectItems.is_video) { - messageFps = "get_fps_and_jitters::1::0" - } else if (socketCollectItems.is_camera) { - messageFps = "get_fps_and_jitters::0::1" - } + let messageFps = "get_fps_and_jitters" udp.send({ address: UdpSendAddress, data: messageFps }) - console.log("sub worker messageFps :" + messageFps); + console.log(workTag + "sub worker messageFps :" + messageFps); } if (socketCollectItems.ram) { @@ -115,24 +98,14 @@ parentPort.onmessage = function (e) { address: UdpSendAddress, data: messageRam }) - console.log("sub worker messageRam :" + messageRam); + console.log(workTag + "sub worker messageRam :" + messageRam); } - if (socketCollectItems.screen_capture) { udp.send({ address: UdpSendAddress, data: "get_capture" }) - console.log("sub worker screen_capture :" + screen_capture); - } - - if (socketCollectItems.power) { - let messagePower = "get_power" - udp.send({ - address: UdpSendAddress, - data: messagePower - }) - console.log("sub worker messagePower :" + messagePower); + console.log(workTag + "sub worker screen_capture :" + "get_capture"); } if (socketCollectItems.catch_trace_start) { let messageTrace = "catch_trace_start" @@ -140,16 +113,16 @@ parentPort.onmessage = function (e) { address: UdpSendAddress, data: messageTrace }) + console.log(workTag + "sub worker catch_trace_start :" + "catch_trace_start"); } - if (socketCollectItems.catch_trace_finish) { - let messageTrace = "catch_trace_finish::" + socketCollectItems.traceName + if (socketCollectItems.catch_trace_end) { + let messageTrace = "catch_trace_end" udp.send({ address: UdpSendAddress, data: messageTrace }) + console.log(workTag + "sub worker catch_trace_end :" + "catch_trace_end"); } - - }) } @@ -160,30 +133,27 @@ udp.on("message", function (data) { for (let i = 0;i < dataView.byteLength; ++i) { str += String.fromCharCode(dataView.getUint8(i)) } + console.log(workTag + "sub worker SocketRecv:" + str); + if (str.length > 0) { + parentPort.postMessage("UdpStatus$1") + } try { - if (includes(str, "Pss")) { + if (includes(str, "pss")) { parentPort.postMessage("RAM$" + str) } else if (includes(str, "fps")) { - if (str.indexOf("::") != -1) { - let arrStr = str.split("::") - console.log("SockOnMessage solveFps1:" + JSON.stringify(arrStr)) - if (arrStr[1].indexOf(";") != -1) { - let fpsDataArr = arrStr[1].split(";") - console.log("SockOnMessage solveFps2:" + JSON.stringify(fpsDataArr)) - if (fpsDataArr[1].indexOf("|") != -1) { - let fps = fpsDataArr[1].split("|") - console.log("SockOnMessage solveFps fps:" + JSON.stringify(fps)) - if (fpsDataArr.length > 1 && fpsDataArr[2].indexOf("|")) { - let fpsJitter = fpsDataArr[2].split("|") - console.log("SockOnMessage solveFps fpsJitter:" + JSON.stringify(fpsJitter)) - parentPort.postMessage("FPS$" + fps[1].toString() + "$" + fpsJitter[1].toString()) - } + if (str.indexOf("$$") != -1) { + let arrStr = str.split("$$") + if (arrStr.length > 0) { + if (arrStr[0].indexOf("||") != -1 && arrStr[1].indexOf("||") != -1) { + let fps = arrStr[0].split("||") + let fpsJitter = arrStr[1].split("||") + parentPort.postMessage("FPS$" + fps[1].toString() + "$" + fpsJitter[1].toString()) } } } } } catch (e) { - console.log("SockOnMessage recv callback err:" + e) + console.log(workTag + "SockOnMessage recv callback err:" + e) } }) @@ -199,10 +169,8 @@ function includes(all, sub) { for (let i = 0; i < all.length - subLength + 1; i++) { - if (all.charAt(i) == firstChar) - { - if (all.substring(i, i + subLength) == sub) - { + if (all.charAt(i) == firstChar) { + if (all.substring(i, i + subLength) == sub) { return true; } } diff --git a/device/device_ui/entry/src/main/module.json5 b/device/device_ui/entry/src/main/module.json similarity index 82% rename from device/device_ui/entry/src/main/module.json5 rename to device/device_ui/entry/src/main/module.json index fe95e0f..dd49b3f 100644 --- a/device/device_ui/entry/src/main/module.json5 +++ b/device/device_ui/entry/src/main/module.json @@ -2,14 +2,12 @@ "module": { "name": "entry", "type": "entry", - "description": "./ets/Application/AbilityStage.ts", + "srcEntrance": "./ets/Application/AbilityStage.ts", + "description": "$string:entry_desc", "mainElement": "MainAbility", "deviceTypes": [ - "phone", - "tablet", - "wearable", - "car", - "tv", + "default", + "tablet" ], "deliveryWithInstall": true, "installationFree": false, @@ -55,15 +53,15 @@ "skills": [ { "entities": [ - "entity.system.home", + "entity.system.home" ], "actions": [ - "action.system.home", - ], - }, - ], - }, - ], + "action.system.home" + ] + } + ] + } + ] - }, + } } \ No newline at end of file diff --git a/device/device_ui/hvigorfile.js b/device/device_ui/hvigorfile.js deleted file mode 100644 index 5f2735e..0000000 --- a/device/device_ui/hvigorfile.js +++ /dev/null @@ -1,2 +0,0 @@ -// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. -module.exports = require('@ohos/hvigor-ohos-plugin').appTasks \ No newline at end of file diff --git a/device/device_ui/package.json b/device/device_ui/package.json deleted file mode 100644 index 6cae941..0000000 --- a/device/device_ui/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "devDependencies":{}, - "name":"OpenHarmony_SP", - "ohos":{ - "org":"ohos", - "directoryLevel":"project", - "buildTool":"hvigor" - }, - "version":"1.0.0", - "dependencies":{ - "@ohos/hvigor-ohos-plugin":"1.0.4-rc", - "@ohos/hvigor":"1.0.4-rc" - } -} \ No newline at end of file diff --git a/device/device_ui/signature/openharmony_smartperf.p7b b/device/device_ui/signature/openharmony_smartperf.p7b new file mode 100644 index 0000000000000000000000000000000000000000..d7bde8a2af753ff4f97a3653281548dc21c06699 GIT binary patch literal 3633 zcmcgvd2kcw6}JxCF~-Kk+~F_+YzoN!R+enZ2}yRP)k?xES@O!ZjHp>jtHYLdWohM= zMIl5q4o85{Ou!9C%w?cl8IBm*0aDso5RB52o z^&psNU4UjOUxXCAJg|uj20Q__SynbnMl-D3MR}Qsz)Yo4s!LP~m3m5-pY*a+gm;&y zHOkUr-aUJY%oU?OK}sH=y^OHjLp4!BCQL=-UDPdMiZ+mu5JNZf@99IvO-0y%H{d2& zfwf%tAt+`v=V?(JVbmM7FoAY?oiuJVYVm|t3%e*E%o!2bXGBShwkV;4am43q^f%O7 zDy&u=%-LZa!{gcnY(jkIDg<^AaD&;x?_jfXsL!FRvRbF199!YWs$!LxzS&i+iBSYv ziz99fhU};|j$4UhBm4<= zuuGxzSpmu!{BAQ|k8*fD+{|+zjByKr$E$g-S=SK9>+$#_Up>O$)+nd7c5%c|&ZHyi zQ9F(pyLtKXawq-7JL4Du*L0sVN}#BQtF2ZU9W^G_rBJDidRx#@4dT;uaHt!D6HeLy zom<7KRCz!=klX z!__LUr@>(`D^*RPrWiJmaY#{Lfp}0I6V=;{wGdSkid!6sO3X={INTdG`0aKLK-EDb zAZi@7v^nI%p(xW7b5Yhtm|!3yXV<}Afw`d)M=>2-?L!EcKjIEJX+3K%XY8@!;>xPh zm>1Q1Nh51Zz?LqwRgJ)2HHyHv7E!{SLCB~LSP;a4>MPiIB<2q~X}1lrMok{hUE5e5 z(=cXxm5;<>4%2EIu}WO2;aBSrE36}7n5(rK>4_k1W273Nx@4TW`eTDY923ETL6pN!ip(K@8u zjuI9NVR%X$Hdj`oHerYIr$jN7s3XvFw8CI9JL)QVm7-5URak^AaM%oEs7*G5XaAUB z*C8T&A|e4d%b%l9WV(Rm4-ykboh;lC=o*$0nheTjw)3=IM=3P!)&%wDyChJ&GkH#0Y0N0Mi+b`Q5v5Cz6lwJ-@wfZicYO^v9vpo9R}(+j zt}mUJ)jCG>kL{EG_|C=imq+89OK(fge1C)%PbOX)bLd!`by>!cKfI?rDWMk~8TP?? z8>j$dcsirTNr!L7$OV4{S=9&z^dJ_b;!f-v6O4OJwU?Gf5x?c*Ri4wk10+e$g z{b{>iT%E(|U%h{QU2DhJ$WqUF@9oQ*Ze3oSRx z;qTk$TEKbT-xlUyu#Nacq}_aV{OGr4Tq-9+Aq&s)pX#R!*P20%5|?NfBg6hhZI|cW3A!k$i{7LxAxcFBij3C zUvSS~`Xx4S{aaZV_ibEvuWsv+h4Jv@^V+Y4xgGU`3;4S@XF~L(fibKZy-%N#%7!@;d75(a~t?lc{}~Jjd$w z8b*++SkYTYg86(V;xi;PcFlK3SMMCE+P?6_hPuCBg(nn9uZ;Nc<`?Wd?T*c9KWx8I zFygaw?`8dVU+IQ9;hF_U=e!i&)ztp+pKPFd&sQrZmQ?KR*m-#6h*cYJE?AIr^K|Rj z1$W<=xo7g2yVs6iUh<>dzTOSUKEvs$8u>po_SE6l=Z2B^Qvf9fq4Pgi&-*od7+tz+<+Yv%^?!|KQj47a5$vewp- zFTc09X4|LBm**S~zI*cT|Q1;$#2&`VxVWkBp2|XaIjHDgdNVf?^e<DjZ#4w2VhS@r(P6LUru6-p*4?}*2yT)L7!dDb_Es}EIKW4+gH>*r^Fv~%&{ P2_?yieNaCdiicXwOdf(C~mFYoH^Ue$Z| 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 aD>~~+^cSjQ_wTa;;D?x;XqB)*z<&UWol^J! literal 0 HcmV?d00001 diff --git a/device/figures/SmartPerfConfig2.png b/device/figures/SmartPerfConfig2.png new file mode 100644 index 0000000000000000000000000000000000000000..ca4806b3623be0218e550353f4cfd5b1002396c7 GIT binary patch literal 11188 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 literal 0 HcmV?d00001 diff --git a/device/figures/SmartPerfConfig3.png b/device/figures/SmartPerfConfig3.png new file mode 100644 index 0000000000000000000000000000000000000000..88389866f85413746e1448a8c3cc284e9909f101 GIT binary patch literal 13194 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

jko38xen16-EyqoNn0l2O~$MD*QH6_pSa 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 literal 0 HcmV?d00001 diff --git a/device/figures/SmartPerfControl1.png b/device/figures/SmartPerfControl1.png new file mode 100644 index 0000000000000000000000000000000000000000..c7279107055ca517306a49c619ee01fa6cf70e6a GIT binary patch literal 17170 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 E0Qs+|{Qv*} literal 0 HcmV?d00001 -- Gitee