feiyu02
2024-06-28 d2d71a6bc8e445ee60b7be2667676138e277d676
1. 修改走航报告自动输出模块
已修改10个文件
已添加1个文件
541 ■■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/FactorFilter.kt 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousSingle.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/ExceptionAnalysisController.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionSlideAverage.kt 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionValueMutation.kt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/DataAnalysisConfig.kt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionReport.kt 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/chart/ChartUtil.kt 126 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/chart/DataToChartUtil.kt 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/templates/report-underway.ftl 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/FactorFilter.kt
@@ -35,7 +35,41 @@
    companion object{
        fun builder() = FactorFilter().Builder()
        fun default() = builder().create()
        fun default() = builder()
            .withMain(FactorType.VOC)
            .withSubs(listOf(
                FactorType.H2S,
                FactorType.O3,
                FactorType.PM25,
            ))
            .withMain(FactorType.H2S)
            .withSubs(listOf(
                FactorType.VOC,
                FactorType.O3,
                FactorType.PM25,
            ))
            .withMain(FactorType.O3)
            .withSubs(listOf(
                FactorType.VOC,
                FactorType.H2S,
                FactorType.PM25,
            ))
            .withMain(FactorType.PM25)
            .withSubs(listOf(
                FactorType.VOC,
                FactorType.H2S,
                FactorType.O3,
            ))
//            .withSubs(listOf(
//                FactorType.NO2,
//                FactorType.CO,
//                FactorType.SO2,
//                FactorType.O3,
//                FactorType.PM25,
//                FactorType.PM10,
//                FactorType.VOC
//            ))
            .create()
    }
    // æ‰€é€‰å› å­é›†åˆ
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt
@@ -70,6 +70,8 @@
    /**
     * åˆ¤æ–­å¼‚常出现的连续时长是否满足条件
     * @param sIndex
     * @param eIndex
     */
    abstract fun judgeDuration(sIndex: Int, eIndex: Int): Boolean
@@ -83,6 +85,7 @@
//            existException.add(false)
//            exceptionData.add(mutableListOf())
//        }
        tagMap.clear()
        config.factorFilter.mainList().forEach {f->
            tagMap[f] = Tag()
        }
@@ -91,16 +94,18 @@
    override fun onNextData(data: BaseRealTimeData) {
        val isContinue = isContinuous(lastData, data)
        val hasException = judgeException(lastData, data)
        config.factorFilter.mainList().forEach {f->
        config.factorFilter.selectedList.forEach {s->
            val f = s.main
            tagMap[f]?.let {
                it.eIndex++
                // èµ·å§‹æ•°æ®
                it.endData = lastData
                if (it.endData == null) {
                    it.refreshAfterCheckResult(data)
                }
                // åˆ¤æ–­ç›¸é‚»æ•°æ®æ˜¯å¦è¿žç»­å¹¶ä¸”是否满足异常判断
                if (!isContinue) {
                    checkResult()
                    checkResult(s)
                    // æ•°æ®ä¸è¿žç»­æ—¶ï¼Œè®°å½•异常情况
                    if (it.eIndex - it.sIndex >= durationCount) {
                        it.refreshAfterCheckResult(data)
@@ -111,7 +116,7 @@
                        it.exceptionData.add(data)
                    } else {
                        // å¼‚常不再重复出现时,记录异常情况
                        checkResult()
                        checkResult(s)
                        if (it.eIndex - it.sIndex >= durationCount) {
                            it.refreshAfterCheckResult(data)
                        }
@@ -165,7 +170,7 @@
    open fun checkResult(factor: FactorFilter.SelectedFactor? = null) {
        val tag = tagMap[factor?.main]
        if (factor != null && tag != null) {
            if (tag.existException && judgeDuration(tag.sIndex, tag.eIndex)) {
            if (tag.existException && judgeDuration(tag.sIndex, tag.eIndex - 1)) {
                tag.startData?.let {
                    resultList.add(newResult(it, lastData, factor, tag.exceptionData))
                }
@@ -174,7 +179,7 @@
        } else {
            config.factorFilter.selectedList.forEach { f ->
                val tag1 = tagMap[f.main] ?: return@forEach
                if (tag1.existException && judgeDuration(tag1.sIndex, tag1.eIndex)) {
                if (tag1.existException && judgeDuration(tag1.sIndex, tag1.eIndex - 1)) {
                    tag1.startData?.let {
                        resultList.add(newResult(it, lastData, f, tag1.exceptionData))
                    }
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousSingle.kt
@@ -11,7 +11,8 @@
    override fun onNextData(data: BaseRealTimeData) {
        val isContinue = isContinuous(lastData, data)
        val hasException = judgeException(lastData, data)
        config.factorFilter.mainList().forEach {f->
        config.factorFilter.selectedList.forEach {s->
            val f = s.main
            tagMap[f]?.let {
                it.eIndex++
                if (lastData == null) {
@@ -19,7 +20,7 @@
                }
                // åˆ¤æ–­ç›¸é‚»æ•°æ®æ˜¯å¦è¿žç»­å¹¶ä¸”是否满足异常判断
                if (!isContinue) {
                    checkResult()
                    checkResult(s)
                    it.sIndex = it.eIndex
                    it.startData = data
                } else {
@@ -31,7 +32,7 @@
                        }
                        it.existException = true
                    } else {
                        checkResult()
                        checkResult(s)
                    }
                }
            }
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/ExceptionAnalysisController.kt
@@ -2,6 +2,7 @@
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.exceptiontype.ExceptionDataExceed
import com.flightfeather.uav.biz.dataanalysis.exceptiontype.ExceptionSlideAverage
import com.flightfeather.uav.biz.dataanalysis.exceptiontype.ExceptionValueMutation
import com.flightfeather.uav.biz.dataanalysis.model.DataAnalysisConfig
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionResult
@@ -25,8 +26,9 @@
    private fun initTask(config: DataAnalysisConfig) {
        taskList.clear()
        taskList.apply {
            add(ExceptionDataExceed(config))
            add(ExceptionValueMutation(config))
//            add(ExceptionDataExceed(config))
//            add(ExceptionValueMutation(config))
            add(ExceptionSlideAverage(config))
        }
    }
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionSlideAverage.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,201 @@
package com.flightfeather.uav.biz.dataanalysis.exceptiontype
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionAnalysis
import com.flightfeather.uav.biz.dataanalysis.model.DataAnalysisConfig
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionType
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.socket.eunm.FactorType
import kotlin.math.abs
/**
 * æ»‘动平均值突变异常
 */
class ExceptionSlideAverage(config: DataAnalysisConfig) : BaseExceptionAnalysis(config) {
    private val historyDataList = mutableListOf<BaseRealTimeData>()
    private val tempDataList = mutableListOf<BaseRealTimeData>()
    private var lastData: BaseRealTimeData? = null
//    private val avgListReverse = mutableListOf<Pair<Double, Boolean>>()
//    private var startData: BaseRealTimeData? = null
//    private var sIndex = 0
//    private var eIndex = -1
//    private var existException = false
    inner class Tag {
        // èµ·å§‹æ•°æ®ä¸‹æ ‡
        var sIndex = 0
        // èµ·å§‹æ•°æ®å¯¹è±¡
        var startData :BaseRealTimeData? = null
        // æœ«å°¾æ•°æ®ä¸‹æ ‡
        var eIndex = -1
        // æœ«å°¾æ•°æ®å¯¹è±¡
        var endData: BaseRealTimeData? = null
        // æ•°æ®ç»„均值的集合
        val avgListReverse = mutableListOf<Pair<Double, Boolean>>()
        // å¼‚常数据段
        var exceptionData = mutableListOf<BaseRealTimeData>()
        // æ˜¯å¦å­˜åœ¨å¼‚常
        var existException = false
        fun refreshAfterCheckResult(data: BaseRealTimeData) {
            // åˆ¤æ–­å¹¶æ›´æ–°èµ·å§‹ç‚¹ä½ç½®
//            val len = config.changeTrendGroup - 1 + config.changeTrendTimes + config.changeTrendInterval
            val len = config.changeTrendGroup
            if ((eIndex - sIndex + 1) > len) {
                sIndex = eIndex + 1 - len
                startData = historyDataList[sIndex]
                exceptionData.clear()
                exceptionData.addAll(historyDataList.subList(sIndex, eIndex + 1))
            }
        }
    }
    protected val tagMap = mutableMapOf<FactorType, Tag>()
    override fun init() {
        super.init()
        historyDataList.clear()
        tempDataList.clear()
        lastData = null
        tagMap.clear()
        config.factorFilter.mainList().forEach {f->
            tagMap[f] = Tag()
        }
//        avgListReverse.clear()
//        startData = null
//        sIndex = 0
//        eIndex = -1
//        existException = false
    }
    override fun getExceptionType(): ExceptionType = ExceptionType.TYPE7
    override fun onNextData(data: BaseRealTimeData) {
        historyDataList.add(data)
        // æ•°æ®åŠ å…¥ä¸´æ—¶æ•°ç»„
        tempDataList.add(data)
        // æ•°æ®é‡è¶…出设置数量时,去除当前数据组首个数据
        if (tempDataList.size > config.changeTrendGroup) {
            tempDataList.removeAt(0)
        }
        config.factorFilter.selectedList.forEach {s->
            val f = s.main
            tagMap[f]?.let {
                it.eIndex++
                it.endData = lastData
                if (it.startData == null) {
                    it.startData = data
                }
                // æ•°æ®é‡ç­‰äºŽè®¾ç½®æ•°é‡æ—¶ï¼Œè®¡ç®—当前数据组均值
                if (tempDataList.size == config.changeTrendGroup) {
                    calAvg(f, tempDataList)
                    if (checkSlideAvg(f)) {
                        it.existException = true
                        it.exceptionData.add(data)
                    } else {
                        checkResult(s)
                        it.refreshAfterCheckResult(data)
                    }
                }
            }
        }
        lastData = data
    }
    override fun onDone() {
        checkResult()
    }
    /**
     * è®¡ç®—一组数据的均值
     */
    private fun calAvg(type: FactorType, list: List<BaseRealTimeData>) {
        var total = .0
        var valid = true
        val count = list.size
        if (count == 0) return
        list.forEach {
            val v = it.getByFactorType(type)
            if (v == null) {
                valid = false
            } else {
                total += v
            }
        }
        val avg = total / count
        tagMap[type]?.avgListReverse?.add(0, Pair(avg, valid))
    }
    /**
     * è®¡ç®—数据组之间的均值差异是否连续超过限定比率
     */
    private fun checkSlideAvg(type: FactorType): Boolean {
        val tag = tagMap[type] ?: return false
        // è®¡ç®—滑动均值最低要求个数
        val minSize = config.changeTrendTimes + config.changeTrendInterval
        if (tag.avgListReverse.size < minSize) {
            return false
        } else {
            // æ»‘动均值满足数量时,计算均值之间是否连续超过限定比率
            val rateList = mutableListOf<Pair<Double, Boolean>>()
            for (i in tag.avgListReverse.indices) {
                if (i >= config.changeTrendTimes) break
                val r = calAvgChangeRate(tag.avgListReverse[i], tag.avgListReverse[i + config.changeTrendInterval])
                rateList.add(r)
            }
            for (y in rateList) {
                if (!y.second || y.first < config.changeTrendRate) {
                    return false
                }
            }
            return true
        }
    }
    /**
     * è®¡ç®—滑动均值变化率
     * æ±‚a1相对于a2的变化率
     */
    private fun calAvgChangeRate(a1: Pair<Double, Boolean>, a2: Pair<Double, Boolean>): Pair<Double, Boolean> {
        val valid = a1.second && a2.second
        return if (a2.first == .0) {
            Pair(1.0, valid)
        } else {
            Pair(abs(a1.first - a2.first) / a2.first, valid)
        }
    }
    /**
     * å½“前数据未出现异常时,或数据循环结束时,判断后续步骤
     */
    private fun checkResult(factor: FactorFilter.SelectedFactor? = null) {
        val tag = tagMap[factor?.main]
        if (factor != null && tag != null) {
            if (tag.existException) {
                tag.startData?.let {
                    resultList.add(newResult(it, lastData, factor, tag.exceptionData))
                }
                tag.existException = false
            }
        } else {
            config.factorFilter.selectedList.forEach { f ->
                val tag1 = tagMap[f.main] ?: return@forEach
                if (tag1.existException) {
                    tag1.startData?.let {
                        resultList.add(newResult(it, lastData, f, tag1.exceptionData))
                    }
                    tag1.existException = false
                }
            }
        }
    }
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionValueMutation.kt
@@ -34,6 +34,7 @@
            val b2 = r >= config.mutationRate
            if (b1) special = true
            res[f] = (b1 || b2)
            if (res[f] == true) println("p: $pValue --- n: $nValue --- r: $r")
        }
//        repeat(config.factorCount) { i->
//            if (p?.getByFactorIndex(i) == null || n.getByFactorIndex(i) == null) {
@@ -56,6 +57,7 @@
        val b1 = special && (eIndex - sIndex) >= (config.mutationNum / 2)
        val b2 = (eIndex - sIndex) >= config.mutationNum
        special = false
        println("sIndex: $sIndex --- eIndex: $eIndex --- special: $special")
        return b1 || b2
    }
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/DataAnalysisConfig.kt
@@ -20,4 +20,12 @@
    var mutationNum = 6
    // çªå˜çއ
    var mutationRate = .5
    // æ±‚滑动平均值的数据组个数
    var changeTrendGroup = 12
    // æ»‘动平均值连续
    var changeTrendInterval = 12
    var changeTrendRate = 1
    // æ»‘动平均值变化率异常连续次数
    var changeTrendTimes = 3
}
src/main/kotlin/com/flightfeather/uav/biz/report/MissionReport.kt
@@ -4,7 +4,6 @@
import com.flightfeather.uav.biz.dataanalysis.ExceptionAnalysisController
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionResult
import com.flightfeather.uav.biz.dataprocess.PreData
import com.flightfeather.uav.common.chart.ChartUtil
import com.flightfeather.uav.common.chart.DataToChartUtil
import com.flightfeather.uav.common.exception.BizException
import com.flightfeather.uav.common.pdf.GeneratePdfUtil
@@ -13,7 +12,6 @@
import com.flightfeather.uav.domain.entity.Mission
import com.flightfeather.uav.domain.repository.MissionRep
import com.flightfeather.uav.domain.repository.RealTimeDataRep
import com.flightfeather.uav.socket.eunm.FactorType
import org.springframework.beans.BeanUtils
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
@@ -55,11 +53,22 @@
         */
        fun addExceptions(exceptions: List<ExceptionResult>) {
            this.exceptions = exceptions.map {
                val byteArray = DataToChartUtil.lineToByteArray(it.selectedFactor, it.dataList)
                val base64Str = ImageUtil.compressImage2(byteArray, 400, needPrefix = false)
                val c = ExceptionChart()
                BeanUtils.copyProperties(it, c)
                c.pict = base64Str
                // åˆ›å»ºä¸»æ±¡æŸ“因子的数据折线图
                val byteArray = DataToChartUtil.lineToByteArray(it.selectedFactor?.main, it.dataList)
                val base64Str = ImageUtil.compressImage2(byteArray, 800, needPrefix = false)
                c.mainPict = base64Str
                // åˆ›å»ºå…³è”因子的数据折线图
                if (it.selectedFactor?.subs?.isNotEmpty() == true) {
                    val subList = mutableListOf<String>()
                    it.selectedFactor!!.subs.forEach { type ->
                        val byteArray1 = DataToChartUtil.lineToByteArray(type, it.dataList)
                        val base64Str1 = ImageUtil.compressImage2(byteArray1, 800, needPrefix = false)
                        subList.add(base64Str1)
                    }
                    c.subPictList = subList
                }
                return@map c
            }
        }
@@ -83,8 +92,11 @@
     * æ•°æ®å¼‚常所在时段的折线图
     */
    class ExceptionChart : ExceptionResult() {
        // æ•°æ®æŠ˜çº¿å›¾Base64编码
        var pict: String? = null
        // æ±¡æŸ“因子的异常数据折线图Base64编码
        var mainPict: String? = null
        // å…³è”因子的异常数据折线图Base64编码
        var subPictList: List<String>? = null
    }
    private val templateName = "report-underway.ftl"
@@ -118,7 +130,7 @@
                max = u.max
                val byteArray = DataToChartUtil.lineToByteArray(t, realTimeData)
                val base64Str = ImageUtil.compressImage2(byteArray, 400, needPrefix = false)
                val base64Str = ImageUtil.compressImage2(byteArray, 800, needPrefix = false)
                pict = base64Str
            })
        }
src/main/kotlin/com/flightfeather/uav/common/chart/ChartUtil.kt
@@ -3,17 +3,21 @@
import org.jfree.chart.ChartFactory
import org.jfree.chart.ChartUtils
import org.jfree.chart.JFreeChart
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator
import org.jfree.chart.axis.NumberAxis
import org.jfree.chart.axis.NumberTickUnit
import org.jfree.chart.labels.StandardCategorySeriesLabelGenerator
import org.jfree.chart.plot.CategoryPlot
import org.jfree.chart.plot.PlotOrientation
import org.jfree.chart.renderer.category.LineAndShapeRenderer
import org.jfree.chart.title.TextTitle
import org.jfree.data.category.DefaultCategoryDataset
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.Paint
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import kotlin.math.max
import kotlin.math.min
/**
 * å›¾è¡¨ç”Ÿæˆ
@@ -22,40 +26,87 @@
 */
object ChartUtil {
    data class ChartValue(val x: String?, val y: Number)
    private const val FONT_NAME = "SimHei"
    private val defaultColors = listOf(
        Color(191, 0, 0),
        Color(181, 125, 69),
        Color(236, 204, 6),
        Color(84, 151, 57),
        Color(13, 221, 151),
        Color(57, 75, 151),
        Color(115, 51, 206),
    )
    fun line(title: String, dataset: DefaultCategoryDataset): JFreeChart {
        val line = ChartFactory.createLineChart(title, "时间", "浓度", dataset)
//        line.categoryPlot.domainAxis.labelFont = Font("宋体", Font.PLAIN, 12)
//        line.categoryPlot.rangeAxis.labelFont = Font("宋体", Font.PLAIN, 12)
    // å›¾è¡¨æ•°æ®å€¼åæ ‡
    data class ChartValue(val x: String?, val y: Double)
    // å›¾è¡¨æ•°æ®é›†
    data class ChartDataset(val dataset: DefaultCategoryDataset, val minValue: Double, val maxValue: Double)
    /**
     * åˆ›å»ºæŠ˜çº¿å›¾
     */
    fun line(title: String, chartDatasetList: List<ChartDataset>): JFreeChart? {
        if (chartDatasetList.isEmpty()) return null
        val line = ChartFactory.createLineChart(
            title, "时间", "浓度(μg/m3)",
            null, PlotOrientation.VERTICAL, true, false, false
        )
//        val line = ChartFactory.createLineChart(
//            title, "", "浓度(μg/m³)",
//            null, PlotOrientation.VERTICAL, true, false, false
//        )
        var minValue = chartDatasetList[0].minValue
        var maxValue = chartDatasetList[0].maxValue
        val datasetList = mutableListOf<DefaultCategoryDataset>()
        chartDatasetList.forEach {
            minValue = min(minValue, it.minValue)
            maxValue = max(maxValue, it.maxValue)
            datasetList.add(it.dataset)
        }
        val plot = line.categoryPlot
        setLine(line)
        setRenderer(plot, datasetList)
        setX(plot)
        setY(plot, "浓度(μg/m3)", minValue, maxValue)
        return line
    }
    fun lineToByteArray(title: String, dataset: DefaultCategoryDataset):ByteArray {
    /**
     * æ–°å»ºæŠ˜çº¿å›¾å¹¶è½¬æ¢ä¸ºbyte数组
     */
    fun lineToByteArray(title: String, dataset: List<ChartDataset>): ByteArray {
        val chart = line(title, dataset)
        val output = ByteArrayOutputStream()
        ChartUtils.writeChartAsPNG(output, chart, 600, 400)
        ChartUtils.writeChartAsJPEG(output, chart, 1080, 607)
        val byteArray = output.toByteArray()
        output.flush()
        output.close()
        return byteArray
    }
    fun newDataset(dataList:List<ChartValue>, seriesName:String): DefaultCategoryDataset {
    /**
     * æ–°å»ºå›¾è¡¨æ•°æ®é›†
     */
    fun newDataset(dataList: List<ChartValue>, seriesName: String): ChartDataset? {
        if (dataList.isEmpty()) return null
        val dataset = DefaultCategoryDataset()
        var minValue = dataList[0].y
        var maxValue = minValue
        dataList.forEach {
            dataset.setValue(it.y, seriesName, it.x)
            minValue = min(minValue, it.y)
            maxValue = max(maxValue, it.y)
        }
        return dataset
        return ChartDataset(dataset, minValue, maxValue)
    }
    /**
     * è®¾ç½®æŠ˜çº¿å›¾æ ·å¼
     */
    private fun setLine(chart: JFreeChart) {
        chart.legend.itemFont = Font("SimHei", Font.PLAIN, 16)
        chart.title.font = Font("SimHei", Font.BOLD, 20)
        chart.legend.itemFont = Font(FONT_NAME, Font.PLAIN, 16)
        chart.title.font = Font(FONT_NAME, Font.BOLD, 20)
        chart.categoryPlot.apply {
            backgroundPaint = Color(255, 255, 255)
            rangeGridlinePaint = Color(200, 200, 200)
@@ -65,7 +116,54 @@
    }
    private fun setRenderer(plot:CategoryPlot, datasetList:List<DefaultCategoryDataset>) {
        val renderer = newBasicRenderer(paint = Color(191, 0, 0))
        datasetList.forEachIndexed { index, v ->
            val renderer = newBasicRenderer(paint = defaultColors[index % defaultColors.size])
            plot.setDataset(index, v)
            plot.setRenderer(index, renderer)
        }
    }
    private fun setX(plot: CategoryPlot) {
        plot.domainAxis.apply {
            // è®¾ç½®x轴每个刻度的字体
            tickLabelFont = Font(FONT_NAME, Font.BOLD, 16)
            // è®¾ç½®x轴标签的字体
            labelFont = Font(FONT_NAME, Font.BOLD, 20)
            // è®¾ç½®x轴轴线是否显示
            isAxisLineVisible = false
            // è®¾ç½®x轴刻度是否显示
            isTickMarksVisible = false
            upperMargin = .0
            lowerMargin = .0
        }
    }
    private fun setY(plot: CategoryPlot, label:String, minValue: Double, maxValue: Double) {
        val tickUnit = (maxValue - minValue) / 10
        val axis1 = NumberAxis().apply {
            this.label = label
            // åˆ»åº¦å±•示格式化
            numberFormatOverride = DecimalFormat("0.0")
            if (tickUnit != .0) {
                // å–消自动分配间距
                isAutoTickUnitSelection = false
                // è®¾ç½®é—´éš”距离
                setTickUnit(NumberTickUnit(tickUnit))
                // è®¾ç½®æ˜¾ç¤ºèŒƒå›´
                setRange(minValue, maxValue)
            }
            // è®¾ç½®y轴每个刻度的字体
            tickLabelFont = Font(FONT_NAME, Font.BOLD, 16)
            // è®¾ç½®y轴标签的字体
            labelFont = Font(FONT_NAME, Font.BOLD, 20)
            // è®¾ç½®y轴轴线不显示
            isAxisLineVisible = false
            // è®¾ç½®y轴刻度不显示
            isTickMarksVisible = false
        }
        plot.rangeAxis = axis1
    }
    private fun newBasicRenderer(series:Int = 0, paint:Paint): LineAndShapeRenderer {
src/main/kotlin/com/flightfeather/uav/common/chart/DataToChartUtil.kt
@@ -1,6 +1,7 @@
package com.flightfeather.uav.common.chart
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.common.exception.BizException
import com.flightfeather.uav.common.utils.DateUtil
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.socket.eunm.FactorType
@@ -12,16 +13,16 @@
 */
object DataToChartUtil {
    fun lineToByteArray(type: FactorFilter.SelectedFactor?, data: List<BaseRealTimeData>): ByteArray {
        val title = type?.main?.des ?: "未知监测因子"
        val seriesName = type?.main?.des ?: "未知系列"
    fun lineToByteArray(type: FactorType?, data: List<BaseRealTimeData>): ByteArray {
        val title = type?.des ?: "未知监测因子"
        val seriesName = type?.des ?: "未知系列"
        val dataList = data.map { d ->
            ChartUtil.ChartValue(
                DateUtil.instance.dateToString(d.dataTime, DateUtil.DateStyle.HH_MM_SS),
                d.getByFactorType(type?.main) ?: 0f
                d.getByFactorType(type)?.toDouble() ?: .0
            )
        }
        val dataset = ChartUtil.newDataset(dataList, seriesName)
        return ChartUtil.lineToByteArray(title, dataset)
        val dataset = ChartUtil.newDataset(dataList, seriesName) ?: throw BizException("折线图数据集为空")
        return ChartUtil.lineToByteArray(title, listOf(dataset))
    }
}
src/main/resources/templates/report-underway.ftl
@@ -28,6 +28,11 @@
                <Relationship Id="imageId_${item_index}"
                    Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
                    Target="media/image_${item_index}.png" />
                <#list item.subPictList as subItem>
                <Relationship Id="imageId_sub_${item_index}-${subItem_index}"
                    Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
                    Target="media/image_sub_${item_index}-${subItem_index}.png" />
                </#list>
                </#list>
                <#list summary as item>
                <Relationship Id="imageId2_${item_index}"
@@ -1247,7 +1252,7 @@
                                <w:szCs w:val="28" />
                                <w:lang w:val="en-US" w:eastAsia="zh-CN" />
                            </w:rPr>
                            <w:t>从${item.startTime}至${item.endTime},${item.factorName}出现${item.exception}<#if item.relatedSceneName??>,结合监测数据变化和受点反向溯源,<#list item.relatedSceneName as scene>${scene}、</#list>等可能存在污染风险。<#else>。</#if></w:t>
                            <w:t>${item_index + 1}. ä»Ž${item.startTime}至${item.endTime},${item.factorName}出现${item.exception}<#if item.relatedSceneName??>,结合监测数据变化和受点反向溯源,<#list item.relatedSceneName as scene>${scene}、</#list>等可能存在污染风险。<#else>。</#if></w:t>
                        </w:r>
                    </w:p>
                    <w:p>
@@ -1274,7 +1279,7 @@
                            <w:pict>
                                <v:shape id="_x0000_i1025_${item_index}" o:spt="75"
                                    alt="f1725d6e3fb17a0e748045906f55ced" type="#_x0000_t75"
                                    style="height:222.8pt;width:414.65pt;" filled="f"
                                    style="height:225pt;width:400pt;" filled="f"
                                    o:preferrelative="t" stroked="f" coordsize="21600,21600">
                                    <v:path />
                                    <v:fill on="f" focussize="0,0" />
@@ -1288,6 +1293,87 @@
                            </w:pict>
                        </w:r>
                    </w:p>
                    <#if item.subPictList??>
                    <w:p>
                        <w:pPr>
                            <w:keepNext w:val="0" />
                            <w:keepLines w:val="0" />
                            <w:pageBreakBefore w:val="0" />
                            <w:widowControl w:val="0" />
                            <w:kinsoku />
                            <w:wordWrap />
                            <w:overflowPunct />
                            <w:topLinePunct w:val="0" />
                            <w:autoSpaceDE />
                            <w:autoSpaceDN />
                            <w:bidi w:val="0" />
                            <w:adjustRightInd />
                            <w:snapToGrid />
                            <w:ind w:firstLine="560" w:firstLineChars="200" />
                            <w:textAlignment w:val="auto" />
                            <w:rPr>
                                <w:rFonts w:hint="default" w:ascii="仿宋_GB2312" w:hAnsi="仿宋"
                                    w:eastAsia="仿宋_GB2312"
                                    w:cs="Times New Roman" />
                                <w:sz w:val="28" />
                                <w:szCs w:val="28" />
                                <w:lang w:val="en-US" w:eastAsia="zh-CN" />
                            </w:rPr>
                        </w:pPr>
                        <w:r>
                            <w:rPr>
                                <w:rFonts w:hint="eastAsia" w:ascii="仿宋_GB2312" w:hAnsi="仿宋"
                                    w:eastAsia="仿宋_GB2312"
                                    w:cs="Times New Roman" />
                                <w:color w:val="000000" />
                                <w:sz w:val="28" />
                                <w:szCs w:val="28" />
                                <w:lang w:val="en-US" w:eastAsia="zh-CN" />
                            </w:rPr>
                            <w:t>关联因子<#list item.subFactorName as subItem><#if (subItem_index > 0)>、</#if>${subItem}</#list>数据量级如下:</w:t>
                        </w:r>
                    </w:p>
                    <#list item.subPictList as subItem>
                    <w:p>
                        <w:pPr>
                            <w:jc w:val="center" />
                            <w:rPr>
                                <w:rFonts w:hint="default" w:ascii="仿宋_GB2312" w:hAnsi="仿宋"
                                    w:eastAsia="仿宋_GB2312" />
                                <w:sz w:val="28" />
                                <w:szCs w:val="28" />
                                <w:highlight w:val="none" />
                                <w:lang w:val="en-US" w:eastAsia="zh-CN" />
                            </w:rPr>
                        </w:pPr>
                        <w:r>
                            <w:rPr>
                                <w:rFonts w:hint="default" w:ascii="仿宋_GB2312" w:hAnsi="仿宋"
                                    w:eastAsia="仿宋_GB2312" />
                                <w:sz w:val="28" />
                                <w:szCs w:val="28" />
                                <w:highlight w:val="none" />
                                <w:lang w:val="en-US" w:eastAsia="zh-CN" />
                            </w:rPr>
                            <w:pict>
                                <v:shape id="_x0000_i1025_sub_${item_index}-${subItem_index}" o:spt="75"
                                    alt="f1725d6e3fb17a0e748045906f55ced" type="#_x0000_t75"
                                    style="height:225pt;width:400pt;" filled="f"
                                    o:preferrelative="t" stroked="f" coordsize="21600,21600">
                                    <v:path />
                                    <v:fill on="f" focussize="0,0" />
                                    <v:stroke on="f" />
                                    <v:imagedata r:id="imageId_sub_${item_index}-${subItem_index}"
                                        o:title="f1725d6e3fb17a0e748045906f55ced" />
                                    <o:lock v:ext="edit" aspectratio="t" />
                                    <w10:wrap type="none" />
                                    <w10:anchorlock />
                                </v:shape>
                            </w:pict>
                        </w:r>
                    </w:p>
                    </#list>
                    </#if>
                    </#list>
                    <w:p>
                        <w:pPr>
@@ -1417,7 +1503,7 @@
                            <w:pict>
                                <v:shape id="_x0000_i1026_${item_index}" o:spt="75"
                                    alt="f1725d6e3fb17a0e748045906f55ced" type="#_x0000_t75"
                                    style="height:222.8pt;width:414.65pt;" filled="f"
                                    style="height:225pt;width:400pt;" filled="f"
                                    o:preferrelative="t" stroked="f" coordsize="21600,21600">
                                    <v:path />
                                    <v:fill on="f" focussize="0,0" />
@@ -1756,8 +1842,13 @@
    </pkg:part>
    <#list exceptions as item>
    <pkg:part pkg:name="/word/media/image_${item_index}.png" pkg:contentType="image/png">
        <pkg:binaryData>${item.pict}</pkg:binaryData>
        <pkg:binaryData>${item.mainPict}</pkg:binaryData>
    </pkg:part>
    <#list item.subPictList as subItem>
    <pkg:part pkg:name="/word/media/image_sub_${item_index}-${subItem_index}.png" pkg:contentType="image/png">
        <pkg:binaryData>${subItem}</pkg:binaryData>
    </pkg:part>
    </#list>
    </#list>
    <#list summary as item>
    <pkg:part pkg:name="/word/media/image2_${item_index}.png" pkg:contentType="image/png">