feiyu02
2025-06-11 f7bdafb7cddd049bbb1bbf265fa006683b4ac693
1. 新增动态污染溯源新的判定逻辑(待完成)
已修改20个文件
已添加1个文件
600 ■■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionAnalysis.kt 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionContinuous.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionDataExceed.kt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionSlideAverage.kt 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionValueMutation.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionTag.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionType.kt 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRTExcWindLevel.kt 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRealTimeException.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RTExcChangeRate.kt 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RealTimeExceptionSlideAverage.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RealTimeExceptionValueMutation.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSourceTest.kt 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionAnalysis.kt
@@ -18,10 +18,10 @@
    /**
     * ç”Ÿæˆä¸€æ¡å¼‚常分析结果
     */
    abstract fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
    ): Y
//    abstract fun newResult(
//        start: BaseRealTimeData,
//        end: BaseRealTimeData?,
//        factor: FactorFilter.SelectedFactor,
//        exceptionData: List<BaseRealTimeData>,
//    ): Y
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt
@@ -37,7 +37,7 @@
    /**
     * ç«‹å³åˆ¤æ–­ï¼šå½“出现异常时,缓存异常数据的同时,立即对已有异常进行判断是否满足异常结果要求
     */
    open fun immeExcCheck(tag: T): Boolean {
    open fun immeExcCheck(tag: T, factorType: FactorType): Boolean {
        return false
    }
@@ -61,11 +61,11 @@
     * åˆ¤æ–­å¼‚常出现的连续个数是否满足条件
     * @param tag å¼‚常数据对象
     */
    abstract fun judgeExceptionCount(tag: T): Boolean
    abstract fun judgeExceptionCount(tag: T, factorType: FactorType?): Boolean
    /**
     * å¼‚常数据的截取判断
     * @return é»˜è®¤ä¸éœ€è¦æˆªå–
     * @return
     */
    open fun needCut(tag: T, hasException: Boolean?): Boolean {
        // é»˜è®¤åˆ¤æ–­æ¡ä»¶ä¸º å½“异常不再重复出现时,形成异常结果
@@ -87,6 +87,8 @@
        config.factorFilter.selectedList.forEach { s ->
            val f = s.main
            tagMap[f]?.let {
                it.addHistoryData(data)
                it.eIndex++
                // èµ·å§‹æ•°æ®
                it.endData = data
@@ -105,7 +107,7 @@
                    // æœ‰å¼‚常出现时,记录异常数据
                    it.addExceptionData(data)
                    // å½“立即判断通过时,形成异常结果
                    if (immeExcCheck(it)) {
                    if (immeExcCheck(it, f)) {
                        recordException(s, it, data)
                    }
                }
@@ -141,13 +143,13 @@
    ) {
        val tag = tagMap[factor?.main]
        if (factor != null && tag != null) {
            if (tag.exceptionExisted && judgeExceptionCount(tag)) {
            if (tag.exceptionExisted && judgeExceptionCount(tag, factor.main)) {
                onNewException(tag, factor, exceptionStatus)
            }
        } else {
            config.factorFilter.selectedList.forEach { f ->
                val tag1 = tagMap[f.main] ?: return@forEach
                if (tag1.exceptionExisted && judgeExceptionCount(tag1)) {
                if (tag1.exceptionExisted && judgeExceptionCount(tag1, null)) {
                    onNewException(tag1, f, exceptionStatus)
                }
            }
@@ -159,7 +161,8 @@
     */
    open fun onNewException(tag: T, factor: FactorFilter.SelectedFactor, exceptionStatus: ExceptionStatusType) {
        if (tag.startData == null) return
        val ex = newResult(tag.startData!!, tag.endData, factor, tag.exceptionData)
//        val ex = newResult(tag.startData!!, tag.endData, factor, tag.exceptionData)
        val ex = newResult(tag, factor)
            .apply { status = exceptionStatus.value }
        // å¼‚常已创建时,更新异常信息
        if (tag.exceptionCreated) {
@@ -177,4 +180,9 @@
        }
    }
    /**
     * ç”Ÿæˆä¸€æ¡å¼‚常分析结果
     */
    abstract fun newResult(tag:T, factor: FactorFilter.SelectedFactor): Y
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionContinuous.kt
@@ -15,13 +15,9 @@
abstract class ExceptionContinuous(config: DataAnalysisConfig) :
    BaseExceptionContinuous<ExceptionTag, DataAnalysisConfig, ExceptionResult>(config, ExceptionTag::class.java) {
    override fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
    ): ExceptionResult {
    override fun newResult(tag:ExceptionTag, factor: FactorFilter.SelectedFactor): ExceptionResult {
        val eType = getExceptionType()
        return ExceptionResult(start, end, factor, exceptionData, config.mission.missionCode, eType)
        return ExceptionResult(tag.startData!!, tag.endData, factor, tag.exceptionData, config.mission.missionCode,
            eType)
    }
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionDataExceed.kt
@@ -33,17 +33,13 @@
        return res
    }
    override fun judgeExceptionCount(tag: ExceptionTag): Boolean {
    override fun judgeExceptionCount(tag: ExceptionTag, factorType: FactorType?): Boolean {
        return true
    }
    override fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
    ): ExceptionResult {
    override fun newResult(tag: ExceptionTag, factor: FactorFilter.SelectedFactor): ExceptionResult {
        val eType = getExceptionType()
        return ExceptionResult(start, end, factor, exceptionData, config.mission.missionCode, eType)
        return ExceptionResult(tag.startData!!, tag.endData, factor, tag.exceptionData, config.mission.missionCode,
            eType)
    }
}
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionSlideAverage.kt
@@ -201,7 +201,9 @@
    }
    override fun newResult(
    fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/exceptiontype/ExceptionValueMutation.kt
@@ -40,7 +40,7 @@
        return res
    }
    override fun judgeExceptionCount(tag: ExceptionTag): Boolean {
    override fun judgeExceptionCount(tag: ExceptionTag, factorType: FactorType?): Boolean {
        // é¦–个数据没有前一个数据参照,不算异常值,最后一个数据是判断结束的正常值,因此异常数据个数的计算下标为sIndex和eIndex
        val sIndex = tag.sIndex
        val eIndex = tag.eIndex - 1
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionTag.kt
@@ -24,6 +24,9 @@
    // å¼‚常数据段
    var exceptionData = mutableListOf<BaseRealTimeData>()
    // è¿‘段时间内的历史数据
    var historyData = mutableListOf<BaseRealTimeData>()
    // æ˜¯å¦å­˜åœ¨å¼‚常
    var exceptionExisted = false
@@ -32,6 +35,13 @@
    var exceptionResult = mutableListOf<BaseExceptionResult>()
    fun addHistoryData(data: BaseRealTimeData) {
        historyData.add(data)
        if (historyData.size > 20) {
            historyData.removeAt(0)
        }
    }
    fun addExceptionData(data: BaseRealTimeData){
        exceptionExisted = true
        exceptionData.add(data)
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionType.kt
@@ -10,4 +10,5 @@
    TYPE6(6, "单日超标次数临近处罚异常"),
    TYPE7(7, "滑动平均值突变异常"),
    TYPE8(8, "有效率异常"),
    TYPE9(9, "变化速率异常"),
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt
@@ -35,23 +35,23 @@
        else
            RTExcWindLevelConfig(
                FactorFilter.builder()
//                .withMain(FactorType.NO2)
                    .withMain(FactorType.CO)
//                .withMain(FactorType.H2S)
//                .withMain(FactorType.SO2)
//                .withMain(FactorType.O3)
//                    .withMain(FactorType.NO2)
//                    .withMain(FactorType.CO)
//                    .withMain(FactorType.H2S)
//                    .withMain(FactorType.SO2)
//                    .withMain(FactorType.O3)
                    .withMain(FactorType.PM25)
                    .withMain(FactorType.PM10)
                    .withMain(FactorType.VOC)
                    .create()
            )
        pollutedSummary = PollutedSummary(config){ summaryCallback(it)}
        pollutedSummary = PollutedSummary(config) { summaryCallback(it) }
        newTask()
    }
    constructor(sceneInfoRep: SceneInfoRep) : this(sceneInfoRep, null)
    private val pollutedSummary:PollutedSummary
    private val pollutedSummary: PollutedSummary
    private val sceneInfoRep: SceneInfoRep
@@ -71,6 +71,7 @@
            add(RTExcWindLevel1_1(config) { exceptionCallback(it) }.also { it.init() })
            add(RTExcWindLevel4(config) { exceptionCallback(it) }.also { it.init() })
            add(RTExcWindLevel6(config) { exceptionCallback(it) }.also { it.init() })
            add(RTExcChangeRate(config) { exceptionCallback(it) }.also { it.init() })
        }
    }
@@ -102,11 +103,11 @@
        // æº¯æºæ±¡æŸ“源信息
        ex.searchScenes(sceneInfoRep)
        // è®°å½•污染线索
        pollutedSummary.addClue(ex)
        // å¹¿æ’­æ±¡æŸ“溯源异常结果
        UnderwayWebSocketSender.broadcast(MsgType.PolClue.value, ex)
        // è®°å½•污染线索
        pollutedSummary.addClue(ex)
    }
    private fun summaryCallback(ex: PollutedSummary.AnalysisResult) {
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt
@@ -3,6 +3,7 @@
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseAnalysisConfig
import com.flightfeather.uav.biz.sourcetrace.model.DistanceType
import com.flightfeather.uav.socket.eunm.FactorType
/**
 *
@@ -22,7 +23,14 @@
    var distanceLimit = 3000
    // é™å®šæ—¶é—´å†…(单位:分钟)
    var timeLimit = 3
    // æº¯æºæ‰©æ•£åç§»è§’度(单位:度)
    var sourceTraceDegOffset = 60.0
    // å®šæ—¶çº¿ç´¢åˆ†æžæ—¶é—´é—´éš”(单位:分钟)
    var analysisPeriod = 15
    // å®šæ—¶åˆ†æžé—´éš”中,立即进行线索分析的最小线索量(单位:个)
    var analysisCount = 4
    /****数据突变*****************************************************************************/
    // 0 - 1级风
    var windLevelCondition1 = WindLevelCondition(
        .0 to 1.5,
@@ -56,42 +64,32 @@
        3
    )
    // æº¯æºæ‰©æ•£åç§»è§’度(单位:度)
    var sourceTraceDegOffset = 120.0
    // å®šæ—¶çº¿ç´¢åˆ†æžæ—¶é—´é—´éš”(单位:分钟)
    var analysisPeriod = 15
    // å®šæ—¶åˆ†æžé—´éš”中,立即进行线索分析的最小线索量(单位:个)
    var analysisCount = 2
//    // 0 - 1级风
//    var windLevelCondition1 = WindLevelCondition(
//        .0 to 1.5,
//        listOf(0.5 to DistanceType.TYPE1, 0.2 to DistanceType.TYPE2,),
//        listOf(1, 1)
//    )
//
//    // 0 - 1级风
//    var windLevelCondition1_1 = WindLevelCondition(
//        .0 to 1.5,
//        0.2 to DistanceType.TYPE2,
//        1
//    )
//
//    // 2 - 4级风
//    var windLevelCondition2 = WindLevelCondition(
//        1.6 to 7.9,
//        listOf(0.2 to DistanceType.TYPE3),
////        listOf(3)
//        listOf(1)
//    )
//
//    // 5 - 6级风
//    var windLevelCondition3 = WindLevelCondition(
//        8.0 to 13.8,
//        listOf(0.1 to DistanceType.TYPE4),
//        listOf(3)
//    )
    /****数据变化速率*****************************************************************************/
    var changeRateCondition = WindLevelCondition(
        .0 to Double.MAX_VALUE,
        0.1 to Double.MAX_VALUE,
        DistanceType.TYPE1,
        3
    )
    // ç›‘测因子在一个监测周期(4秒)内正常变化的量级范围
    var changeRate = mutableMapOf(
        FactorType.PM25 to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            3
        ),
        FactorType.PM10 to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            3
        ),
        FactorType.VOC to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            6.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            1
        ),
    )
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRTExcWindLevel.kt
@@ -66,7 +66,7 @@
        return res
    }
    override fun judgeExceptionCount(tag: ExceptionTag): Boolean {
    override fun judgeExceptionCount(tag: ExceptionTag, factorType: FactorType?): Boolean {
        return tag.exceptionData.size >= windLevelCondition.countLimit
    }
@@ -97,20 +97,24 @@
        return b1 || b2
    }
    override fun immeExcCheck(tag: ExceptionTag): Boolean {
    override fun immeExcCheck(tag: ExceptionTag, factorType: FactorType): Boolean {
        // å¼‚常出现等于限定次数时,就需要形成污染线索
        return tag.exceptionData.size == windLevelCondition.countLimit
    }
    override fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
    ): PollutedClue {
        return PollutedClue(start, end, factor, exceptionData, getExceptionType(), config, windLevelCondition)
    override fun newResult(tag: ExceptionTag, factor: FactorFilter.SelectedFactor): PollutedClue {
        return PollutedClue(tag, factor, getExceptionType(), config, windLevelCondition)
    }
//    override fun newResult(
//        start: BaseRealTimeData,
//        end: BaseRealTimeData?,
//        factor: FactorFilter.SelectedFactor,
//        exceptionData: List<BaseRealTimeData>,
//    ): PollutedClue {
//        return PollutedClue(start, end, factor, exceptionData, getExceptionType(), config, windLevelCondition)
//    }
    override fun onNewException(
        tag: ExceptionTag,
        factor: FactorFilter.SelectedFactor,
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRealTimeException.kt
@@ -55,14 +55,10 @@
//        lastData = data
//    }
    override fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
    ): RealTimeExceptionResult {
    override fun newResult(tag:T, factor: FactorFilter.SelectedFactor): RealTimeExceptionResult {
        val eType = getExceptionType()
        return RealTimeExceptionResult(start, end, factor, exceptionData, eType)
        return RealTimeExceptionResult(tag.startData!!, tag.endData, factor, tag.exceptionData, eType)
    }
    override fun onNewException(tag: T, factor: FactorFilter.SelectedFactor, exceptionStatus: ExceptionStatusType) {
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RTExcChangeRate.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
package com.flightfeather.uav.biz.sourcetrace.exceptiontype
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionContinuous
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionType
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.biz.sourcetrace.model.PollutedClue
import com.flightfeather.uav.common.utils.MapUtil
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.lightshare.eunm.ExceptionStatusType
import com.flightfeather.uav.socket.eunm.FactorType
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneId
/**
 * æ•°æ®å˜åŒ–速率异常
 * @date 2025/6/10
 * @author feiyu02
 */
class RTExcChangeRate(config: RTExcWindLevelConfig) :
    BaseExceptionContinuous<ExceptionTag, RTExcWindLevelConfig, PollutedClue>(config, ExceptionTag::class.java) {
    constructor(config: RTExcWindLevelConfig, callback: NewPolluteClueCallback) : this(config){
        this.callback = callback
    }
    private var callback: NewPolluteClueCallback? = null
    override fun getExceptionType(): ExceptionType {
        return ExceptionType.TYPE9
    }
    override fun judgeException(p: BaseRealTimeData?, n: BaseRealTimeData): MutableMap<FactorType, Boolean> {
        val res = mutableMapOf<FactorType, Boolean>()
        config.factorFilter.mainList().forEach { f ->
            if (p?.getByFactorType(f) == null || n.getByFactorType(f) == null || n.windSpeed == null) {
                res[f] = (false)
                return@forEach
            }
            val rate = config.changeRate[f]
            val pValue = p.getByFactorType(f)!!
            val nValue = n.getByFactorType(f)!!
            // è®¡ç®—后一个数据相比于前一个数据的变化速率
            val v = (nValue - pValue)
            val b1 = if (rate != null) {
                v >= rate.mutationRate.first
            } else {
                false
            }
//                val r = (nValue - pValue) / pValue
//                val b1 = r >= con.mutationRate.first && r < con.mutationRate.second
            println("因子:${f.des},速率:${v},${b1}")
            res[f] = b1
        }
        return res
    }
    override fun judgeExceptionCount(tag: ExceptionTag, factorType: FactorType?): Boolean {
        return tag.exceptionData.size >= (config.changeRate[factorType]?.countLimit ?: 1)
    }
    override fun needCut(tag: ExceptionTag, hasException: Boolean?): Boolean {
        // æŒ‰ç…§æ—¶é•¿å’Œè·ç¦»é™åˆ¶å°†å¼‚常截取
        if (tag.exceptionData.isEmpty()) return false
        val se = tag.exceptionData.first()
        val ee = tag.exceptionData.last()
        val sTime = LocalDateTime.ofInstant(se.dataTime?.toInstant(), ZoneId.systemDefault())
        val eTime = LocalDateTime.ofInstant(ee.dataTime?.toInstant(), ZoneId.systemDefault())
        val duration = Duration.between(sTime, eTime).toMinutes()
        // æ•°æ®é‡‡æ ·çš„æ—¶é•¿è¶…过限制时,需要截取
        val b1 = duration > config.timeLimit
        // èµ°èˆªæ•°æ®çš„距离超过限制时,需要截取
        val b2 = if (se.longitude == null || se.latitude == null || ee.longitude == null || ee.latitude == null) {
            false
        } else {
            val distance = MapUtil.getDistance(
                se.longitude!!.toDouble(), se.latitude!!.toDouble(), ee.longitude!!
                    .toDouble(), ee.latitude!!.toDouble()
            )
            distance > config.distanceLimit
        }
        return b1 || b2
    }
    override fun immeExcCheck(tag: ExceptionTag, factorType: FactorType): Boolean {
        // å¼‚常出现等于限定次数时,就需要形成污染线索
        return judgeExceptionCount(tag, factorType)
    }
    override fun newResult(tag: ExceptionTag, factor: FactorFilter.SelectedFactor): PollutedClue {
        return PollutedClue(tag, factor, getExceptionType(), config, config.changeRate[factor.main])
    }
    override fun onNewException(
        tag: ExceptionTag,
        factor: FactorFilter.SelectedFactor,
        exceptionStatus: ExceptionStatusType,
    ) {
        super.onNewException(tag, factor, exceptionStatus)
        callback?.let { func ->
            val exc = tag.exceptionResult.last()
            func.invoke(exc as PollutedClue)
        }
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RealTimeExceptionSlideAverage.kt
@@ -144,7 +144,7 @@
    }
    override fun newResult(
    fun newResult(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RealTimeExceptionValueMutation.kt
@@ -53,7 +53,7 @@
        return res
    }
    override fun judgeExceptionCount(tag: ExceptionTag): Boolean {
    override fun judgeExceptionCount(tag: ExceptionTag, factorType: FactorType?): Boolean {
        val count = tag.exceptionData.size
        val b1 = special && count >= (config.mutationNum / 2)
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt
@@ -20,18 +20,22 @@
     */
    constructor(
        historyData: List<BaseRealTimeData>,
        exceptionData: List<BaseRealTimeData>,
        config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
    ) : this() {
        distanceType = windLevelCondition.distanceType
        sourceTrace(exceptionData, config, windLevelCondition)
        distanceType = windLevelCondition?.distanceType
        windLevelCondition?.let { sourceTrace(historyData, exceptionData, config, it) }
    }
    var address: String? = null
    // æ±¡æŸ“范围区域(经纬度多边形)
    // æ±¡æŸ“范围扇形区域(经纬度多边形)
    var polygon: List<Pair<Double, Double>>? = null
    // è¿‘距离污染圆形区域
    var closePolygon: List<Pair<Double, Double>>? = null
    // æ±¡æŸ“可能的发生距离
    var distanceType: DistanceType? = null
@@ -40,6 +44,7 @@
     * åå‘溯源
     */
    private fun sourceTrace(
        historyData: List<BaseRealTimeData>,
        exceptionData: List<BaseRealTimeData>,
        config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition,
@@ -53,7 +58,7 @@
        val pair = avgData.longitude!!.toDouble() to avgData.latitude!!.toDouble()
        polygon = calSector(
            avgData.windSpeed!!.toDouble(),
            avgData.windDirection!!.toDouble(),
            pair,
            windLevelCondition.distanceType.disRange,
            config.sourceTraceDegOffset
@@ -61,6 +66,8 @@
            // å°†åæ ‡è½¬æ¢ä¸ºgcj02(火星坐标系),因为污染源场景信息都为此坐标系
            MapUtil.wgs84ToGcj02(it)
        }
        closePolygon = closeSourceTrace(historyData, pair)
        try {
            val address = AMapService.reGeo(pair)
@@ -92,7 +99,13 @@
        val result = mutableListOf<Pair<Double, Double>>()
        if (distanceRange.first == .0) {
            result.add(center)
//            result.add(center)
            var startDeg = 0
            while (startDeg <= 360) {
                val p = MapUtil.getPointByLen(center, 50.0, startDeg * PI / 180)
                result.add(p)
                startDeg++
            }
        } else {
            // ä»Žå¼€å§‹è§’度循环计算坐标点至结束角度,步长1°
            var startDeg = sDeg
@@ -101,16 +114,30 @@
                result.add(p)
                startDeg++
            }
            if (distanceRange.second > .0) {
                // æ­¤å¤„需要从结束角度开始反向循环计算至开始角度,步长1°,使得两组坐标点按顺序排列,可绘制对应的多边形
                startDeg = eDeg
                while (startDeg >= sDeg) {
                    val p = MapUtil.getPointByLen(center, distanceRange.second, startDeg * PI / 180)
                    result.add(p)
                    startDeg--
                }
            }
        }
        if (distanceRange.second > .0) {
            // æ­¤å¤„需要从结束角度开始反向循环计算至开始角度,步长1°,使得两组坐标点按顺序排列,可绘制对应的多边形
            var startDeg = eDeg
            while (startDeg >= sDeg) {
                val p = MapUtil.getPointByLen(center, distanceRange.second, startDeg * PI / 180)
                result.add(p)
                startDeg--
            }
        return result
    }
    private fun closeSourceTrace(
        historyData: List<BaseRealTimeData>,
        center: Pair<Double, Double>,
    ): List<Pair<Double, Double>> {
        val result = mutableListOf<Pair<Double, Double>>()
        var startDeg = 0
        while (startDeg <= 360) {
            val p = MapUtil.getPointByLen(center, 50.0, startDeg * PI / 180)
            result.add(p)
            startDeg++
        }
        return result
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt
@@ -2,6 +2,7 @@
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionResult
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionType
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.common.utils.DateUtil
@@ -18,19 +19,34 @@
 */
class PollutedClue() : BaseExceptionResult() {
//    constructor(
//        start: BaseRealTimeData,
//        end: BaseRealTimeData?,
//        factor: FactorFilter.SelectedFactor,
//        exceptionData: List<BaseRealTimeData>,
//        eType: ExceptionType,
//        config: RTExcWindLevelConfig,
//        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
//    ) : this() {
//        if (exceptionData.isEmpty()) return
//        pollutedData = PollutedData(start, end, factor, exceptionData, eType, windLevelCondition)
//        pollutedArea = PollutedArea(exceptionData, config, windLevelCondition)
//    }
    constructor(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
        eType: ExceptionType,
        config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition
    ) : this() {
        this.factor = factor
        if (exceptionData.isEmpty()) return
        pollutedData = PollutedData(start, end, factor, exceptionData, eType, windLevelCondition)
        pollutedArea = PollutedArea(exceptionData, config, windLevelCondition)
        tag: ExceptionTag, factor: FactorFilter.SelectedFactor, eType: ExceptionType, config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
    ) :this()
//            this(
//        tag.startData!!, tag.endData, factor, tag.exceptionData, eType, config,
//        windLevelCondition
//    )
    {
        if (tag.exceptionData.isEmpty()) return
        pollutedData = PollutedData(
            tag.startData!!, tag.endData, factor, tag.exceptionData, tag.historyData, eType, windLevelCondition
        )
        pollutedArea = PollutedArea(tag.historyData, tag.exceptionData, config, windLevelCondition)
    }
    /**
@@ -43,13 +59,13 @@
    var pollutedSource: PollutedSource? = null
    private var factor: FactorFilter.SelectedFactor? = null
    /**
     * æŸ¥æ‰¾ç³»ç»Ÿå†…部溯源范围内的污染企业
     */
    fun searchScenes(sceneInfoRep: SceneInfoRep) {
        if (pollutedArea == null || factor == null) return
        pollutedSource = PollutedSource().also { it.searchScenes(pollutedArea!!, sceneInfoRep, factor!!) }
        if (pollutedArea == null || pollutedData == null) return
        pollutedSource = PollutedSource().also {
            it.searchScenes(pollutedArea!!, sceneInfoRep, pollutedData!!)
        }
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt
@@ -28,8 +28,9 @@
        end: BaseRealTimeData?,
        factor: FactorFilter.SelectedFactor,
        exceptionData: List<BaseRealTimeData>,
        historyData: List<BaseRealTimeData>,
        eType: ExceptionType,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
    ) : this() {
        exception = eType.des
        exceptionType = eType.value
@@ -47,14 +48,15 @@
        endData = end
        windSpeed = exceptionData.first().windSpeed?.toDouble()
        percentage = windLevelCondition.mutationRate.first
        times = windLevelCondition.countLimit
        percentage = windLevelCondition?.mutationRate?.first
        times = windLevelCondition?.countLimit
        dataList.add(start)
        exceptionData.forEach {
            dataList.add(it)
        }
        dataVoList.addAll(dataList.map { it.toDataVo() })
        historyDataList.addAll(historyData.map { it.toDataVo() })
        calPer()
    }
@@ -87,6 +89,7 @@
    // å‘生次数
    var times: Int? = null
    var historyDataList = mutableListOf<DataVo>()
    // å¼‚常监测数据
    var dataList: MutableList<BaseRealTimeData> = mutableListOf()
    var dataVoList: MutableList<DataVo> = mutableListOf()
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -1,6 +1,5 @@
package com.flightfeather.uav.biz.sourcetrace.model
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.common.utils.MapUtil
import com.flightfeather.uav.domain.entity.SceneInfo
import com.flightfeather.uav.domain.repository.SceneInfoRep
@@ -10,7 +9,7 @@
import com.flightfeather.uav.socket.eunm.FactorType
import org.springframework.beans.BeanUtils
import org.springframework.web.context.ContextLoader
import kotlin.math.min
import kotlin.math.round
/**
 * æ±¡æŸ“来源
@@ -26,33 +25,29 @@
     */
    // æº¯æºä¼ä¸š
    var sceneList:List<SceneInfoVo?>? = null
    var sceneList: List<SceneInfoVo?>? = null
    init {
    // æº¯æºæŽ¨ç†ç»“论
    var conclusion: String? = null
    }
    fun searchScenes(pollutedArea: PollutedArea, factor: FactorFilter.SelectedFactor) {
    fun searchScenes(pollutedArea: PollutedArea, pollutedData: PollutedData) {
        ContextLoader.getCurrentWebApplicationContext()?.getBean(SceneInfoRep::class.java)?.run {
            searchScenes(pollutedArea, this, factor)
            searchScenes(pollutedArea, this, pollutedData)
        }
    }
    /**
     * æŸ¥æ‰¾ç³»ç»Ÿå†…部溯源范围内的污染企业
     */
    fun searchScenes(pollutedArea: PollutedArea, sceneInfoRep: SceneInfoRep, factor: FactorFilter.SelectedFactor) {
    fun searchScenes(pollutedArea: PollutedArea, sceneInfoRep: SceneInfoRep, pollutedData: PollutedData) {
        // Fixme 2025.5.14: æ±¡æŸ“源的坐标是高德地图坐标系(火星坐标系),而走航数据是WGS84坐标系
        // æŒ‰ç…§åŒºåŸŸæ£€ç´¢å†…部污染源信息
        // 1. é¦–先按照四至范围从数据库初步筛选污染源
//        val polygonTmp = pollutedArea.polygon!!.map {
//            MapUtil.gcj02ToWgs84(it)
//        }
        var result = mutableListOf<SceneInfo>()
        // 1. é¦–先按照四至范围从数据库初步筛选污染源,此处的区域坐标已转换为火星坐标系
        val polygonTmp = pollutedArea.polygon!!
        val fb = MapUtil.calFourBoundaries(polygonTmp)
        val sceneList = sceneInfoRep.findByCoordinateRange(fb)
        // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
        val result = mutableListOf<SceneInfo>()
        sceneList.forEach {
            val point = it!!.longitude.toDouble() to it.latitude.toDouble()
            if (MapUtil.isPointInPolygon(point, polygonTmp)) {
@@ -60,27 +55,91 @@
            }
        }
        findClosestStation(sceneInfoRep, result)
        val closePolygonTmp = pollutedArea.closePolygon!!
        val closeFb = MapUtil.calFourBoundaries(closePolygonTmp)
        val closeSceneList = sceneInfoRep.findByCoordinateRange(closeFb)
        // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
        closeSceneList.forEach {
            val point = it!!.longitude.toDouble() to it.latitude.toDouble()
            if (MapUtil.isPointInPolygon(point, closePolygonTmp)) {
                result.add(it)
            }
        }
        // æ ¹æ®æ±¡æŸ“因子的量级,计算主要的污染场景类型,筛选结果
        val mainSceneType = calSceneType(pollutedData)
        if (mainSceneType != null) {
            this.conclusion = mainSceneType.first
            result = result.filter {
                val r = mainSceneType.second.find { s->
                    s.value == it.typeId.toInt()
                }
                r != null
            }.toMutableList()
        }
//        TODO("按照所选监测因子类型,区分污染源类型")
        this.sceneList = findClosestStation(sceneInfoRep, result)
    }
    /**
     * è®¡ç®—可能的相关污染场景类型
     * è®¡ç®—可能的相关污染场景类型以及推理结论
     */
    private fun calFactorType(factor: FactorFilter.SelectedFactor) {
//        when (factor.main) {
//            FactorType.PM25 -> {}
//
//        }
    @Throws(Exception::class)
    private fun calSceneType(pollutedData: PollutedData): Pair<String, List<SceneType>>? {
        when (pollutedData.selectedFactor?.main) {
            // æ°®æ°§åŒ–合物,一般由于机动车尾气,同步计算CO
            FactorType.NO2 -> {
                val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                return "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to
                        listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
            }
            FactorType.CO -> return null
            FactorType.H2S -> return null
            FactorType.SO2 -> return null
            FactorType.O3 -> return null
            // a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
            // b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
            FactorType.PM25,
            FactorType.PM10,
                -> {
                // è®¡ç®—异常数据的pm2.5占pm10比重的均值
                val percentageAvg = pollutedData.dataList.map {
                    it.pm25!! / it.pm10!!
                }.average()
                return if (percentageAvg > 0.666) {
                    "PM2.5占PM10的比重为${round(percentageAvg * 100)}%,比重较大,污染源以餐饮为主,工地次之" to
                            listOf(SceneType.TYPE1, SceneType.TYPE2, SceneType.TYPE3, SceneType.TYPE14, SceneType.TYPE5)
                } else if (percentageAvg < 0.333) {
                    "PM2.5占PM10的比重为${round(percentageAvg * 100)}%,比重较小,属于大颗粒扬尘污染,污染源以工地为主" to
                            listOf(SceneType.TYPE1, SceneType.TYPE2, SceneType.TYPE3, SceneType.TYPE14, SceneType.TYPE5)
                } else {
                    "PM2.5占PM10的比重为${round(percentageAvg * 100)}%,污染源以餐饮、工地为主" to
                            listOf(SceneType.TYPE1, SceneType.TYPE2, SceneType.TYPE3, SceneType.TYPE14, SceneType.TYPE5)
                }
            }
            // c) VOC较高,同比计算pm2.5的量级,可能存在同步偏高(汽修、加油站), åŒæ­¥è®¡ç®—O3是否有高值
            // d) VOC较高,处于加油站(车辆拥堵情况),CO一般较高, åŒæ­¥è®¡ç®—O3是否有高值
            FactorType.VOC -> {
                val pm25Avg = round(pollutedData.dataList.map { it.pm25!! }.average() * 10) / 10
                val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                val o3Avg = round(pollutedData.dataList.map { it.o3!! }.average() * 10) / 10
                return "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to
                        listOf(SceneType.TYPE6, SceneType.TYPE17, SceneType.TYPE12)
            }
            else -> return null
        }
    }
    /**
     * è®¡ç®—最近的监测站点
     */
    private fun findClosestStation(sceneInfoRep: SceneInfoRep, sceneList: List<SceneInfo>) {
    private fun findClosestStation(sceneInfoRep: SceneInfoRep, sceneList: List<SceneInfo>): List<SceneInfoVo> {
        val res1 = sceneInfoRep.findByArea(AreaVo().apply {
            sceneTypeId = SceneType.TYPE19.value.toString()
        })
@@ -90,10 +149,10 @@
        })
        val res = res1.toMutableList().apply { addAll(res2) }
        this.sceneList = sceneList.map {
        return sceneList.map {
            var minLen = -1.0
            var selectedRes: SceneInfo? = null
            res.forEach { r->
            res.forEach { r ->
                val dis = MapUtil.getDistance(
                    it.longitude.toDouble(),
                    it.latitude.toDouble(),
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt
@@ -31,11 +31,11 @@
    /**
     * åˆ†æžç»“æžœ
     */
    inner class AnalysisResult{
    inner class AnalysisResult {
        // æŒ‰ç…§è¢«æ‰«ææ¬¡æ•°é™åºæŽ’列的污染源列表
        var sortedSceneList: List<Pair<SceneInfo?, Int>>? = null
        var time: Date? = null
        var advice:String?= null
        var advice: String? = null
        var direction: AMapService.AMapDirection? = null
    }
@@ -60,7 +60,7 @@
    private var analysisTimer: Timer? = null
    // å®šæ—¶æ±¡æŸ“分析任务
    private var lastAnalysisOnTimeTask:TimerTask? = null
    private var lastAnalysisOnTimeTask: TimerTask? = null
    // å®šæ—¶æ±¡æŸ“分析任务运行状态
    private var analysisTaskIsRunning = false
@@ -128,8 +128,8 @@
        val statistic = AnalysisStatistic()
        // å…±æœ‰å¤šå°‘相关污染源,哪些污染源被扫描次数较多
        val sceneMap = mutableMapOf<String?, Pair<SceneInfo?, Int>>()
        clueList.forEach {c->
            c.pollutedSource?.sceneList?.forEach { s->
        clueList.forEach { c ->
            c.pollutedSource?.sceneList?.forEach { s ->
                if (!sceneMap.containsKey(s?.guid)) {
                    sceneMap[s?.guid] = s to 1
                } else {
@@ -145,11 +145,12 @@
     * çº¿ç´¢åˆ†æž
     */
    private fun analysis() {
        if (clueList.isEmpty()) return
        val result = AnalysisResult()
        // å…±æœ‰å¤šå°‘相关污染源,哪些污染源被扫描次数较多
        val sceneMap = mutableMapOf<String?, Pair<SceneInfo?, Int>>()
        clueList.forEach {c->
            c.pollutedSource?.sceneList?.forEach { s->
        clueList.forEach { c ->
            c.pollutedSource?.sceneList?.forEach { s ->
                if (!sceneMap.containsKey(s?.guid)) {
                    sceneMap[s?.guid] = s to 1
                } else {
@@ -157,31 +158,34 @@
                }
            }
        }
        val res = sceneMap.entries.sortedBy { it.value.second }
        val res = sceneMap.entries.sortedByDescending { it.value.second }
        result.sortedSceneList = res.map { it.value }
        // å½“前的走航数据的定位和污染源距离是否是逐渐接近,若走航远离了主要污染源,提示用户调整走航路线
        if (!result.sortedSceneList.isNullOrEmpty()) {
            val sT = clueList.first().pollutedData?.startTime
            val closetScene = result.sortedSceneList?.first()
            result.advice = "根据${sT}起的${clueList.size}条最新污染线索,污染源[${closetScene?.first?.name}]被多次溯源,具有较高污染风险,现提供新的走航推荐路线,可经过该污染源。"
            // èµ°èˆªè·¯çº¿è°ƒæ•´å»ºè®®
            result.advice =
                "根据${sT}起的${clueList.size}条最新污染线索,污染源【${closetScene?.first?.name}】被多次溯源,具有较高污染风险,现提供新的走航推荐路线,可经过该污染源。"
            val lastP = realTimeDataList.last()
            // å»ºè®®å¯¹åº”的数据采样时间
            result.time = lastP.dataTime
            if (lastP.longitude != null && lastP.latitude != null &&
                lastP.longitude!! > BigDecimal.ZERO && lastP.latitude!! > BigDecimal.ZERO
                && closetScene?.first?.longitude != null && closetScene.first?.latitude != null &&
                closetScene.first?.longitude!! > BigDecimal.ZERO && closetScene.first?.latitude!! > BigDecimal.ZERO) {
                closetScene.first?.longitude!! > BigDecimal.ZERO && closetScene.first?.latitude!! > BigDecimal.ZERO
            ) {
                val origin = MapUtil.wgs84ToGcj02(lastP.longitude!!.toDouble() to lastP.latitude!!.toDouble())
                val destination = closetScene.first!!.longitude.toDouble() to closetScene.first!!.latitude.toDouble()
                // å»ºè®®çš„走航路线
                result.direction = AMapService.directionDriving(origin, destination)
            }
        }
        result.time = realTimeDataList.last().dataTime
        // çº¿ç´¢åˆ†æžå®ŒæˆåŽï¼Œç§»åŠ¨è‡³åŽ†å²çº¿ç´¢åˆ—è¡¨
        historyClueList.addAll(clueList)
        clueList.clear()
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSourceTest.kt
@@ -13,21 +13,22 @@
@SpringBootTest
class PollutedSourceTest {
 @Autowired
 lateinit var sceneInfoRep: SceneInfoRep
    @Autowired
    lateinit var sceneInfoRep: SceneInfoRep
 @Test
 fun foo1() {
  val source = PollutedSource()
  val pollutedArea = PollutedArea().apply {
   polygon = listOf(
    121.421521 to 31.195457,
    121.421721 to 31.195457,
    121.421521 to 31.195257,
    121.421721 to 31.195257,
   )
  }
  source.searchScenes(pollutedArea, sceneInfoRep, FactorFilter.SelectedFactor(FactorType.VOC))
    @Test
    fun foo1() {
        val source = PollutedSource()
        val pollutedData = PollutedData()
        val pollutedArea = PollutedArea().apply {
            polygon = listOf(
                121.421521 to 31.195457,
                121.421721 to 31.195457,
                121.421521 to 31.195257,
                121.421721 to 31.195257,
            )
        }
        source.searchScenes(pollutedArea, sceneInfoRep, pollutedData)
 }
    }
}