Riku
2025-06-02 e731486b50c4ea6e2d28f302df449b4bd0b2be57
1. 新增走航动态溯源功能
已修改11个文件
已添加7个文件
663 ■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousSingle.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRTExcWindLevel.kt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/bean/SceneInfoVo.kt 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/eunm/SceneType.kt 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/decoder/UnderwayWebSocketParser.kt 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/handler/UnderwayWebSocketServerHandler.kt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/processor/UnderwayProcessor.kt 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/sender/MsgType.kt 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/sender/UnderwayWebSocketSender.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/sender/WebSocketMessage.kt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/dataprocess/DataProcessTest.kt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSourceTest.kt 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousSingle.kt
@@ -20,7 +20,7 @@
                    it.startData = data
                }
                // åˆ¤æ–­ç›¸é‚»æ•°æ®æ˜¯å¦è¿žç»­å¹¶ä¸”是否满足异常判断
                if (!isContinue || needCut(it)) {
                if (!isContinue || needCut(it, hasException[f])) {
                    recordException(s, it, data)
                } else {
                    if (hasException[f] == true) {
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
package com.flightfeather.uav.biz.sourcetrace
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionAnalysis
import com.flightfeather.uav.biz.sourcetrace.RealTimeAnalysisConfig
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.biz.sourcetrace.exceptiontype.*
import com.flightfeather.uav.biz.sourcetrace.model.PollutedClue
import com.flightfeather.uav.biz.sourcetrace.model.PollutedSummary
import com.flightfeather.uav.common.utils.GsonUtils
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.socket.eunm.FactorType
import com.flightfeather.uav.socket.sender.MsgType
import com.flightfeather.uav.socket.sender.UnderwayWebSocketSender
import java.util.*
/**
 * æ±¡æŸ“线索控制器
 * @date 2025/5/27
 * @author feiyu02
 */
class SourceTraceController {
    /**
     * 5. æ±¡æŸ“源的被扫描次数
     * æ¯ä¸€åˆ»é’Ÿå¯¹åŽ†å²çº¿ç´¢è¿›è¡Œç»Ÿè®¡ï¼Œæå‡ºä¼šå•†å»ºè®®ï¼ˆç¦»æ±¡æŸ“æºè¾ƒè¿œã€æ±¡æŸ“æºæ•°é‡ã€å‡ºçŽ°æ¬¡æ•°ï¼‰ã€èµ°èˆªè·¯çº¿è°ƒæ•´å»ºè®®ï¼ˆç¦»æ±¡æŸ“æºè¾ƒè¿‘ã€èµ°èˆªè½¨è¿¹æœªæŽ¥è¿‘æº¯æºåœºæ™¯ï¼‰
     */
    constructor(sceneInfoRep: SceneInfoRep, factorFilter: FactorFilter?) {
        this.sceneInfoRep = sceneInfoRep
        this.config = if (factorFilter != null)
            RTExcWindLevelConfig(factorFilter)
        else
            RTExcWindLevelConfig(
                FactorFilter.builder()
//                .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)}
        newTask()
    }
    constructor(sceneInfoRep: SceneInfoRep) : this(sceneInfoRep, null)
    private val pollutedSummary:PollutedSummary
    private val sceneInfoRep: SceneInfoRep
    private val config: RTExcWindLevelConfig
    private val taskList = mutableListOf<BaseExceptionAnalysis<RTExcWindLevelConfig, PollutedClue>>()
    fun initTask() {
        taskList.clear()
        newTask()
        pollutedSummary.clear()
    }
    private fun newTask() {
        taskList.apply {
            add(RTExcWindLevel1(config) { exceptionCallback(it) }.also { it.init() })
            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() })
        }
    }
    /**
     * è®¡ç®—新的一条实时走航数据
     */
    fun addOneData(data: BaseRealTimeData) {
        // è®¡ç®—异常
        taskList.forEach { it.onNextData(data) }
        pollutedSummary.refreshLatestMonitorData(data)
        // é™å®šæ—¶é—´å†…没有新数据传入,则结束当前的计算
    }
    /**
     * è¶…时处理,较长时间没有新数据进入,进行初始化操作
     */
    private fun dealOnTimeout() {
        val timer = Timer(true)
        timer.schedule(object : TimerTask() {
            override fun run() {
                TODO("Not yet implemented")
            }
        }, 60 * 1000)
        timer.cancel()
    }
    // æ•°æ®çªå˜å¼‚常回调
    private fun exceptionCallback(ex: PollutedClue) {
        // æº¯æºæ±¡æŸ“源信息
        ex.searchScenes(sceneInfoRep)
        // è®°å½•污染线索
        pollutedSummary.addClue(ex)
        // å¹¿æ’­æ±¡æŸ“溯源异常结果
        UnderwayWebSocketSender.broadcast(MsgType.PolClue.value, ex)
    }
    private fun summaryCallback(ex: PollutedSummary.AnalysisResult) {
        // å¹¿æ’­æ±¡æŸ“溯源异常结果
        UnderwayWebSocketSender.broadcast(MsgType.AnaResult.value, ex)
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt
@@ -18,9 +18,9 @@
    )
    // é™å®šè·ç¦»å†…(单位:米)
    var distanceLimit = 1000
    var distanceLimit = 3000
    // é™å®šæ—¶é—´å†…(单位:分钟)
    var timeLimit = 2
    var timeLimit = 3
    // 0 - 1级风
    var windLevelCondition1 = WindLevelCondition(
@@ -52,4 +52,9 @@
    // æº¯æºæ‰©æ•£åç§»è§’度(单位:度)
    var sourceTraceDegOffset = 120.0
    // å®šæ—¶çº¿ç´¢åˆ†æžæ—¶é—´é—´éš”(单位:分钟)
    var analysisPeriod = 15
    // å®šæ—¶åˆ†æžé—´éš”中,立即进行线索分析的最小线索量(单位:个)
    var analysisCount = 3
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/BaseRTExcWindLevel.kt
@@ -39,6 +39,7 @@
    override fun judgeException(p: BaseRealTimeData?, n: BaseRealTimeData): MutableMap<FactorType, Boolean> {
        val res = mutableMapOf<FactorType, Boolean>()
        println()
        config.factorFilter.mainList().forEach { f ->
            if (p?.getByFactorType(f) == null || n.getByFactorType(f) == null || n.windSpeed == null) {
                res[f] = (false)
@@ -48,17 +49,20 @@
            val con = windLevelCondition
            if (n.windSpeed!! in con.windSpeed.first..con.windSpeed.second) {
                println("风速:${n.windSpeed},[${con.windSpeed.first} - ${con.windSpeed.second}]")
                val pValue = p.getByFactorType(f)!!
                val nValue = n.getByFactorType(f)!!
                // è®¡ç®—后一个数据相比于前一个数据的变化率
                val r = (nValue - pValue) / pValue
                val b1 = r >= con.mutationRate.first
                println("因子:${f.des},幅度:${r},限定:${con.mutationRate.first},${b1}")
                res[f] = b1
            } else {
                res[f] = false
            }
        }
        return res
    }
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt
@@ -91,7 +91,7 @@
        if (distanceRange.first == .0) {
            result.add(center)
        } else {
            // ä»Žå¼€å§‹è§’度循环计算坐标点值结束角度,步长1°
            // ä»Žå¼€å§‹è§’度循环计算坐标点至结束角度,步长1°
            var startDeg = sDeg
            while (startDeg <= eDeg) {
                val p = MapUtil.getPointByLen(center, distanceRange.first, startDeg * PI / 180)
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt
@@ -15,16 +15,6 @@
class PollutedData() {
    /**
     *
     * 1. è½¯é£Ž1.5m/s及以下,
     *     å‰åŽå€¼ä¸Šå‡å¹…度在50%以上1次,认为是临近发生(50ç±³)
     *     å‰åŽå€¼ä¸Šå‡å¹…度在20%以上1次,认为是远距离发生(50ç±³ - 500米)
     *     1.5 m/s及以下,静稳天气,临近发生(50ç±³)
     * 2. 1.6 - 7.9 m/s,前后值上升幅度在20%以上3次,认为是远距离发生(50ç±³ - 1公里)
     * 3. 8 - 13.8 m/s ä»¥ä¸Šï¼Œå‰åŽå€¼ä¸Šå‡å¹…度在10%以上3次,认为是远距离发生(50ç±³ - 2公里)
     */
    /**
     * 9. å…³è”因子
     *     a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
     *     b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
@@ -62,6 +52,8 @@
            dataList.add(it)
            dataVoList.add(it.toDataVo())
        }
        calPer()
    }
    var deviceCode: String? = null
@@ -86,6 +78,8 @@
    // å› å­é‡çº§å˜åŒ–幅度
    var percentage: Double? = null
    // å› å­é‡çº§å¹³å‡å˜åŒ–幅度
    var avgPer: Double? = null
    // å‘生次数
    var times: Int? = null
@@ -93,4 +87,14 @@
    // å¼‚常监测数据
    var dataList: MutableList<BaseRealTimeData> = mutableListOf()
    var dataVoList: MutableList<DataVo> = mutableListOf()
    private fun calPer() {
        if (dataList.size < 2) return
        var total = .0
        for (i in 0 until dataList.size - 1) {
            total += dataList[i].getByFactorType(selectedFactor!!.main)!!
        }
        avgPer = total / (dataList.size - 1)
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -4,7 +4,13 @@
import com.flightfeather.uav.common.utils.MapUtil
import com.flightfeather.uav.domain.entity.SceneInfo
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.lightshare.bean.AreaVo
import com.flightfeather.uav.lightshare.bean.SceneInfoVo
import com.flightfeather.uav.lightshare.eunm.SceneType
import com.flightfeather.uav.socket.eunm.FactorType
import org.springframework.beans.BeanUtils
import org.springframework.web.context.ContextLoader
import kotlin.math.min
/**
 * æ±¡æŸ“来源
@@ -20,7 +26,11 @@
     */
    // æº¯æºä¼ä¸š
    var sceneList:List<SceneInfo?>? = null
    var sceneList:List<SceneInfoVo?>? = null
    init {
    }
    fun searchScenes(pollutedArea: PollutedArea, factor: FactorFilter.SelectedFactor) {
        ContextLoader.getCurrentWebApplicationContext()?.getBean(SceneInfoRep::class.java)?.run {
@@ -35,9 +45,10 @@
        // Fixme 2025.5.14: æ±¡æŸ“源的坐标是高德地图坐标系(火星坐标系),而走航数据是WGS84坐标系
        // æŒ‰ç…§åŒºåŸŸæ£€ç´¢å†…部污染源信息
        // 1. é¦–先按照四至范围从数据库初步筛选污染源,需要先将坐标转换为gcj02(火星坐标系),因为污染源场景信息都为此坐标系
        val polygonTmp = pollutedArea.polygon!!.map {
            MapUtil.gcj02ToWgs84(it)
        }
//        val polygonTmp = pollutedArea.polygon!!.map {
//            MapUtil.gcj02ToWgs84(it)
//        }
        val polygonTmp = pollutedArea.polygon!!
        val fb = MapUtil.calFourBoundaries(polygonTmp)
        val sceneList = sceneInfoRep.findByCoordinateRange(fb)
        // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
@@ -49,9 +60,57 @@
            }
        }
        this.sceneList = result
        findClosestStation(sceneInfoRep, result)
        TODO("按照所选监测因子类型,区分污染源类型")
//        TODO("按照所选监测因子类型,区分污染源类型")
    }
    /**
     * è®¡ç®—可能的相关污染场景类型
     */
    private fun calFactorType(factor: FactorFilter.SelectedFactor) {
//        when (factor.main) {
//            FactorType.PM25 -> {}
//
//        }
    }
    /**
     * è®¡ç®—最近的监测站点
     */
    private fun findClosestStation(sceneInfoRep: SceneInfoRep, sceneList: List<SceneInfo>) {
        val res1 = sceneInfoRep.findByArea(AreaVo().apply {
            sceneTypeId = SceneType.TYPE19.value.toString()
        })
        val res2 = sceneInfoRep.findByArea(AreaVo().apply {
            sceneTypeId = SceneType.TYPE20.value.toString()
        })
        val res = res1.toMutableList().apply { addAll(res2) }
        this.sceneList = sceneList.map {
            var minLen = -1.0
            var selectedRes: SceneInfo? = null
            res.forEach { r->
                val dis = MapUtil.getDistance(
                    it.longitude.toDouble(),
                    it.latitude.toDouble(),
                    r!!.longitude.toDouble(),
                    r.latitude.toDouble()
                )
                if (minLen < 0 || dis < minLen) {
                    minLen = dis
                    selectedRes = r
                }
            }
            val vo = SceneInfoVo()
            BeanUtils.copyProperties(it, vo)
            vo.closestStation = selectedRes
            vo.length = minLen
            return@map vo
        }
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt
@@ -1,16 +1,14 @@
package com.flightfeather.uav.biz.sourcetrace.model
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionAnalysis
import com.flightfeather.uav.biz.sourcetrace.RealTimeAnalysisConfig
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.biz.sourcetrace.exceptiontype.*
import com.flightfeather.uav.common.utils.GsonUtils
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.socket.eunm.FactorType
import com.flightfeather.uav.socket.sender.UnderwayWebSocketSender
import java.util.*
import com.flightfeather.uav.domain.entity.SceneInfo
import java.time.LocalDateTime
import java.util.Timer
import java.util.TimerTask
// å¼‚常数据生成回调类
typealias NewPolluteSummaryCallback = (ex: PollutedSummary.AnalysisResult) -> Unit
/**
 * æ±¡æŸ“情况汇总
@@ -18,7 +16,7 @@
 * @date 2025/5/27
 * @author feiyu02
 */
class PollutedSummary {
class PollutedSummary(private val config: RTExcWindLevelConfig, private val callback: NewPolluteSummaryCallback) {
    /**
@@ -26,75 +24,152 @@
     * æ¯ä¸€åˆ»é’Ÿå¯¹åŽ†å²çº¿ç´¢è¿›è¡Œç»Ÿè®¡ï¼Œæå‡ºä¼šå•†å»ºè®®ï¼ˆç¦»æ±¡æŸ“æºè¾ƒè¿œã€æ±¡æŸ“æºæ•°é‡ã€å‡ºçŽ°æ¬¡æ•°ï¼‰ã€èµ°èˆªè·¯çº¿è°ƒæ•´å»ºè®®ï¼ˆç¦»æ±¡æŸ“æºè¾ƒè¿‘ã€èµ°èˆªè½¨è¿¹æœªæŽ¥è¿‘æº¯æºåœºæ™¯ï¼‰
     */
    constructor(sceneInfoRep: SceneInfoRep, factorFilter: FactorFilter?) {
        this.sceneInfoRep = sceneInfoRep
        this.config = if (factorFilter != null)
            RTExcWindLevelConfig(factorFilter)
        else
            RTExcWindLevelConfig(
                FactorFilter.builder()
//                .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()
            )
        initTask()
    }
    constructor(sceneInfoRep: SceneInfoRep) : this(sceneInfoRep, null)
    // æ±¡æŸ“线索
    var clueList = mutableListOf<PollutedClue>()
    private val sceneInfoRep: SceneInfoRep
    private val config: RTExcWindLevelConfig
    private val taskList = mutableListOf<BaseExceptionAnalysis<RTExcWindLevelConfig, PollutedClue>>()
    fun initTask() {
        taskList.clear()
        taskList.apply {
            add(RTExcWindLevel1(config) { exceptionCallback(it) }.also { it.init() })
            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() })
        }
    /**
     * åˆ†æžç»“æžœ
     */
    inner class AnalysisResult{
        // æŒ‰ç…§è¢«æ‰«ææ¬¡æ•°é™åºæŽ’列的污染源列表
        var sortedSceneList: List<Pair<SceneInfo?, Int>>? = null
    }
    /**
     * è®¡ç®—新的一条实时走航数据
     * å®žæ—¶ç»Ÿè®¡
     */
    fun addOneData(data: BaseRealTimeData) {
        // è®¡ç®—异常
        taskList.forEach { it.onNextData(data) }
        // é™å®šæ—¶é—´å†…没有新数据传入,则结束当前的计算
    inner class AnalysisStatistic {
        // æŒ‰ç…§è¢«æ‰«ææ¬¡æ•°é™åºæŽ’列的污染源列表
        var sortedSceneList: List<Pair<SceneInfo?, Int>>? = null
    }
    /**
     * è¶…时处理,较长时间没有新数据进入,进行初始化操作
     */
    private fun dealOnTimeout() {
        val timer = Timer(true)
        timer.schedule(object : TimerTask() {
    // æœ€æ–°å®žæ—¶èµ°èˆªç›‘测数据
    val realTimeDataList = mutableListOf<BaseRealTimeData>()
    // æœªåˆ†æžçš„æ±¡æŸ“线索
    val clueList = mutableListOf<PollutedClue>()
    // å·²åˆ†æžçš„æ±¡æŸ“线索
    private val historyClueList = mutableListOf<PollutedClue>()
    // å®šæ—¶æ±¡æŸ“分析任务控制
    private var analysisTimer: Timer? = null
    // å®šæ—¶æ±¡æŸ“分析任务
    private val analysisOnTimeTask = object : TimerTask() {
            override fun run() {
                TODO("Not yet implemented")
            // è®°å½•任务运行状态
            analysisTaskIsRunning = true
            analysis()
            // è®°å½•上一次的任务结束时间
            lastAnalysisTime = LocalDateTime.now()
            analysisTaskIsRunning = false
            }
        }, 60 * 1000)
        timer.cancel()
    }
    // æ•°æ®çªå˜å¼‚常回调
    private fun exceptionCallback(ex: PollutedClue) {
        // æº¯æºæ±¡æŸ“源信息
        ex.searchScenes(sceneInfoRep)
        clueList
        // å¹¿æ’­æ±¡æŸ“溯源异常结果
        UnderwayWebSocketSender.broadcast(GsonUtils.gson.toJson(ex))
    // å®šæ—¶æ±¡æŸ“分析任务运行状态
    private var analysisTaskIsRunning = false
    // ä¸Šä¸€æ¬¡å®šæ—¶æ±¡æŸ“分析任务结束时间
    private lateinit var lastAnalysisTime: LocalDateTime
    init {
        clear()
    }
    // æ–°å¢žä¸€æ¡æ±¡æŸ“线索
    fun addClue(pollutedClue: PollutedClue) {
        clueList.add(pollutedClue)
//        realTimeSummary()
        analysisOnClueCount()
    }
    // åˆ·æ–°å½“前最新的走航监测数据
    fun refreshLatestMonitorData(data: BaseRealTimeData) {
//        realTimeDataList.clear()
        realTimeDataList.add(data)
    }
    fun clear() {
        realTimeDataList.clear()
        clueList.clear()
        historyClueList.clear()
        analysisTimer?.cancel()
        analysisTimer = null
        analysisTaskIsRunning = false
        lastAnalysisTime = LocalDateTime.now()
        resetAnalysisOnTime()
    }
    /**
     * é‡ç½®å®šæ—¶åˆ†æžçº¿ç´¢ä»»åŠ¡
     */
    private fun resetAnalysisOnTime() {
        // å–消原有的分析任务计时
        analysisTimer?.cancel()
        // ä»¥å½“前时间为起点,重新开始新的一轮等待计时
        analysisTimer = Timer()
        val period = config.analysisPeriod * 60 * 1000L
        analysisTimer?.schedule(analysisOnTimeTask, period, period)
    }
    /**
     * åœ¨å®šæ—¶æ±¡æŸ“线索分析任务等待周期时间内,若污染线索量超过设定值,直接触发分析线索任务
     * å¹¶é‡ç½®å®šæ—¶åˆ†æžä»»åŠ¡
     */
    private fun analysisOnClueCount() {
        if (clueList.size >= config.analysisCount && !analysisTaskIsRunning) {
            analysisOnTimeTask.run()
            resetAnalysisOnTime()
        }
    }
    /**
     * å®žæ—¶çº¿ç´¢ç»Ÿè®¡
     */
    private fun realTimeSummary() {
        val statistic = AnalysisStatistic()
        // å…±æœ‰å¤šå°‘相关污染源,哪些污染源被扫描次数较多
        val sceneMap = mutableMapOf<String?, Pair<SceneInfo?, Int>>()
        clueList.forEach {c->
            c.pollutedSource?.sceneList?.forEach { s->
                if (!sceneMap.containsKey(s?.guid)) {
                    sceneMap[s?.guid] = s to 1
                } else {
                    sceneMap[s?.guid] = s to (sceneMap[s?.guid]?.second!! + 1)
                }
            }
        }
        val res = sceneMap.entries.sortedBy { it.value.second }
        statistic.sortedSceneList = res.map { it.value }
    }
    /**
     * çº¿ç´¢åˆ†æž
     */
    private fun analysis() {
        val result = AnalysisResult()
        // å…±æœ‰å¤šå°‘相关污染源,哪些污染源被扫描次数较多
        val sceneMap = mutableMapOf<String?, Pair<SceneInfo?, Int>>()
        clueList.forEach {c->
            c.pollutedSource?.sceneList?.forEach { s->
                if (!sceneMap.containsKey(s?.guid)) {
                    sceneMap[s?.guid] = s to 1
                } else {
                    sceneMap[s?.guid] = s to (sceneMap[s?.guid]?.second!! + 1)
                }
            }
        }
        val res = sceneMap.entries.sortedBy { it.value.second }
        result.sortedSceneList = res.map { it.value }
        // å½“前的走航数据的定位和污染源距离是否是逐渐接近,若走航远离了主要污染源,提示用户调整走航路线
        // çº¿ç´¢åˆ†æžå®ŒæˆåŽï¼Œç§»åŠ¨è‡³åŽ†å²çº¿ç´¢åˆ—è¡¨
        historyClueList.addAll(clueList)
        clueList.clear()
        realTimeDataList.clear()
        callback(result)
//        TODO()
    }
}
src/main/kotlin/com/flightfeather/uav/lightshare/bean/SceneInfoVo.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.flightfeather.uav.lightshare.bean
import com.flightfeather.uav.domain.entity.SceneInfo
class SceneInfoVo : SceneInfo() {
    // æœ€ä¸´è¿‘的监测点位
    var closestStation: SceneInfo? = null
    // è·ç¦»ï¼ˆå•位:米))
    var length: Double? = null
}
src/main/kotlin/com/flightfeather/uav/lightshare/eunm/SceneType.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.flightfeather.uav.lightshare.eunm
/**
 * åœºæ™¯ç±»åž‹
 * @date 2025/6/2
 * @author feiyu02
 */
enum class SceneType(val value: Int, val des: String) {
    TYPE1(1, "工地"),
    TYPE2(2, "码头"),
    TYPE3(3, "水泥搅拌站"),
    TYPE4(4, "工业企业"),
    TYPE5(5, "餐饮"),
    TYPE6(6, "汽修"),
    TYPE7(7, "降尘点"),
    TYPE8(8, "空气质量监测点"),
    TYPE9(9, "道路扬尘监测点"),
    TYPE10(10, "道路"),
    TYPE11(11, "河流断面"),
    TYPE12(12, "工业园区"),
    TYPE13(13, "无固定场景"),
    TYPE14(14, "堆场"),
    TYPE15(15, "实验室"),
    TYPE16(16, "精品小区"),
    TYPE17(17, "加油站"),
    TYPE18(18, "商业体"),
    TYPE19(19, "国控点"),
    TYPE20(20, "市控点"),
}
src/main/kotlin/com/flightfeather/uav/socket/decoder/UnderwayWebSocketParser.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package com.flightfeather.uav.socket.decoder
import com.flightfeather.uav.common.utils.GsonUtils
import com.flightfeather.uav.socket.sender.WebSocketMessage
import org.springframework.util.StringUtils
object UnderwayWebSocketParser {
    const val START_STR: String = "##"
    const val SPLIT_STR: String = "&&"
    const val END_STR: String = "%%"
    /**
     * æ¶ˆæ¯æ ¼å¼æ ¡éªŒ
     * @param message æ¶ˆæ¯
     * @return æ ¼å¼æ˜¯å¦åˆè§„
     */
    private fun verificationMessage(message: String): Boolean {
        if (message.isEmpty()) {
            return false
        }
        if (!message.startsWith(START_STR)) {
            return false
        }
        if (!message.endsWith(END_STR)) {
            return false
        }
        return true
    }
    /**
     * è§£æžå‡ºç±»åž‹å’Œå†…容
     * @param message socket消息中的data字段
     * @return è§£æžç»“果,如果格式不正确则返回null
     */
    fun decodeMessage(message: String): WebSocketMessage {
        if (!verificationMessage(message)) {
            // å‘挥一个不会被处理的消息
            return WebSocketMessage(-1, "")
        }
        val webSocketMessage = WebSocketMessage()
        val parts: Array<String> = message.substring(START_STR.length, message.length - END_STR.length)
            .split(SPLIT_STR.toRegex())
            .dropLastWhile { it.isEmpty() }.toTypedArray()
        webSocketMessage.type = parts[0].toInt()
        webSocketMessage.content = GsonUtils.gson.fromJson(parts[1], Any::class.java)
        return webSocketMessage
    }
    /**
     * ç”ŸæˆæŒ‡å®šæ ¼å¼çš„æ¶ˆæ¯å­—符串
     * @return ç”Ÿæˆçš„æ¶ˆæ¯å­—符串
     */
    fun encodeMessage(webSocketMessage: WebSocketMessage): String {
        return START_STR + webSocketMessage.type + SPLIT_STR +
                GsonUtils.gson.toJson(webSocketMessage.content, webSocketMessage.content?.javaClass) +
                END_STR
    }
}
src/main/kotlin/com/flightfeather/uav/socket/handler/UnderwayWebSocketServerHandler.kt
@@ -1,16 +1,13 @@
package com.flightfeather.uav.socket.handler
import com.flightfeather.uav.biz.sourcetrace.RealTimeExceptionAnalysisController
import com.flightfeather.uav.common.api2word.utils.JsonUtils
import com.flightfeather.uav.biz.sourcetrace.SourceTraceController
import com.flightfeather.uav.common.utils.GsonUtils
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.lightshare.bean.DataVo
import com.flightfeather.uav.socket.sender.UnderwayWebSocketSender
import com.google.gson.JsonSyntaxException
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame
import org.springframework.stereotype.Component
/**
 *
@@ -19,7 +16,7 @@
 */
class UnderwayWebSocketServerHandler(sceneInfoRep: SceneInfoRep) : BaseHandler() {
    private val realTimeExceptionAnalysisController = RealTimeExceptionAnalysisController(sceneInfoRep)
    private val sourceTraceController = SourceTraceController(sceneInfoRep)
    override var tag: String = "UAV-WS"
@@ -41,11 +38,11 @@
                // Test
                try {
                    if (msgTxt == "start") {
                        realTimeExceptionAnalysisController.initTask()
                        sourceTraceController.initTask()
                    } else {
                        val data = GsonUtils.parserJsonToArrayBeans(msgTxt, DataVo::class.java)
                        data.forEach {
                            realTimeExceptionAnalysisController.addOneData(
                            sourceTraceController.addOneData(
                                it.toBaseRealTimeData(BaseRealTimeData::class.java)
                            )
                        }
src/main/kotlin/com/flightfeather/uav/socket/processor/UnderwayProcessor.kt
@@ -1,27 +1,19 @@
package com.flightfeather.uav.socket.processor
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.sourcetrace.RealTimeExceptionAnalysisController
import com.flightfeather.uav.common.location.LocationRoadNearby
import com.flightfeather.uav.biz.sourcetrace.SourceTraceController
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.model.epw.EPWDataPrep
import com.flightfeather.uav.domain.repository.AirDataRep
import com.flightfeather.uav.domain.repository.RealTimeDataRep
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.domain.repository.SegmentInfoRep
import com.flightfeather.uav.socket.bean.AirDataPackage
import com.flightfeather.uav.socket.decoder.AirDataDecoder
import com.flightfeather.uav.socket.decoder.DataPackageDecoder
import com.flightfeather.uav.socket.eunm.AirCommandUnit
import com.flightfeather.uav.socket.eunm.FactorType
import com.flightfeather.uav.socket.eunm.UWDeviceType
import com.flightfeather.uav.socket.handler.UnderwayWebSocketServerHandler
import io.netty.channel.ChannelHandlerContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.text.SimpleDateFormat
import java.util.*
import javax.annotation.PostConstruct
/**
 * å¤„理socket接收的消息
@@ -46,7 +38,7 @@
    private val dataProcessMap = mutableMapOf<String?, EPWDataPrep>()
    // å®žæ—¶èµ°èˆªæ±¡æŸ“溯源处理器
    private val realTimeExceptionAnalysisMap = mutableMapOf<String?, RealTimeExceptionAnalysisController>()
    private val sourceTraceMap = mutableMapOf<String?, SourceTraceController>()
    override var tag: String = "走航监测"
@@ -60,11 +52,11 @@
            saveToTxt(msg)
            saveToDataBase(packageData)?.takeIf { it.isNotEmpty() }?.get(0)?.let {
                // æ¯å°è®¾å¤‡æœ‰å„自单独的异常数据处理器
                if (!realTimeExceptionAnalysisMap.containsKey(it.deviceCode)) {
                    realTimeExceptionAnalysisMap[it.deviceCode] = RealTimeExceptionAnalysisController(sceneInfoRep)
                if (!sourceTraceMap.containsKey(it.deviceCode)) {
                    sourceTraceMap[it.deviceCode] = SourceTraceController(sceneInfoRep)
                }
                // å°†èµ°èˆªæ•°æ®ä¼ å…¥å¼‚常处理器
                realTimeExceptionAnalysisMap[it.deviceCode]?.addOneData(it)
                sourceTraceMap[it.deviceCode]?.addOneData(it)
            }
        } else {
src/main/kotlin/com/flightfeather/uav/socket/sender/MsgType.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.flightfeather.uav.socket.sender
import com.flightfeather.uav.biz.sourcetrace.model.PollutedClue
import com.flightfeather.uav.biz.sourcetrace.model.PollutedSummary
enum class MsgType(val value: Int) {
    /**
     * æ±¡æŸ“线索
     * @see [PollutedClue]
     */
    PolClue(1),
    /**
     * æ±¡æŸ“分析结果
     * @see [PollutedSummary.AnalysisResult]
     */
    AnaResult(2),
}
src/main/kotlin/com/flightfeather/uav/socket/sender/UnderwayWebSocketSender.kt
@@ -1,5 +1,6 @@
package com.flightfeather.uav.socket.sender
import com.flightfeather.uav.socket.decoder.UnderwayWebSocketParser
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame
@@ -28,6 +29,11 @@
    }
    fun broadcast(type:Int, content:Any) {
        val msg = UnderwayWebSocketParser.encodeMessage(WebSocketMessage(type,content))
        broadcast(msg)
    }
    fun broadcast(msg: String) {
        sessionPool.forEach { (t, u) ->
            u?.channel()?.writeAndFlush(TextWebSocketFrame(msg))
src/main/kotlin/com/flightfeather/uav/socket/sender/WebSocketMessage.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.flightfeather.uav.socket.sender
class WebSocketMessage {
    /**
     * æ¶ˆæ¯ç±»åž‹
     */
    var type: Int = 0
    /**
     * æ¶ˆæ¯å†…容
     */
    var content: Any? = null
    constructor()
    constructor(type: Int, content: Any?) {
        this.type = type
        this.content = content
    }
}
src/test/kotlin/com/flightfeather/uav/biz/dataprocess/DataProcessTest.kt
@@ -1,12 +1,15 @@
package com.flightfeather.uav.biz.dataprocess
import com.flightfeather.uav.domain.mapper.RealTimeDataMapper
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.lightshare.bean.AreaVo
import com.flightfeather.uav.lightshare.service.RealTimeDataService
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.web.context.ContextLoader
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
@@ -56,4 +59,12 @@
        process.outPutDailyVariation()
        process.done()
    }
    @Test
    fun foo2() {
        ContextLoader.getCurrentWebApplicationContext()?.getBean(SceneInfoRep::class.java)?.run {
            val res =  this.findByArea(AreaVo().apply { sceneTypeId = "20" })
            res.forEach { println(it?.name) }
        }
    }
}
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSourceTest.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.flightfeather.uav.biz.sourcetrace.model
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.domain.repository.SceneInfoRep
import com.flightfeather.uav.socket.eunm.FactorType
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
@RunWith(SpringRunner::class)
@SpringBootTest
class PollutedSourceTest {
 @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))
 }
}