Riku
2025-06-11 d3b43d50df28c4fe27c104dcd146d35b2bad4d20
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -1,9 +1,15 @@
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
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.round
/**
 * 污染来源
@@ -19,22 +25,29 @@
     */
    // 溯源企业
    var sceneList:List<SceneInfo?>? = null
    var sceneList: List<SceneInfoVo?>? = null
    // 溯源推理结论
    var conclusion: String? = null
    fun searchScenes(pollutedArea: PollutedArea, pollutedData: PollutedData) {
        ContextLoader.getCurrentWebApplicationContext()?.getBean(SceneInfoRep::class.java)?.run {
            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. 首先按照四至范围从数据库初步筛选污染源,需要先将坐标转换为gcj02(火星坐标系),因为污染源场景信息都为此坐标系
        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)) {
@@ -42,9 +55,121 @@
            }
        }
        this.sceneList = 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)
            }
        }
        TODO("按照所选监测因子类型,区分污染源类型")
        // 根据污染因子的量级,计算主要的污染场景类型,筛选结果
        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()
        }
        this.sceneList = findClosestStation(sceneInfoRep, result)
    }
    /**
     * 计算可能的相关污染场景类型以及推理结论
     */
    @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>): List<SceneInfoVo> {
        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) }
        return 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
        }
    }
}