Riku
2025-06-11 d3b43d50df28c4fe27c104dcd146d35b2bad4d20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package com.flightfeather.uav.biz.sourcetrace.model
 
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
 
/**
 * 污染来源
 * 系统内部的污染场景、电子地图搜索得到的实际路段路口等标志信息
 * @date 2025/5/27
 * @author feiyu02
 */
class PollutedSource {
 
    /**
     * 溯源清单显示与临近监测站点的距离(国控、市控、网格化监测点)
     *
     */
 
    // 溯源企业
    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, pollutedData: PollutedData) {
        // Fixme 2025.5.14: 污染源的坐标是高德地图坐标系(火星坐标系),而走航数据是WGS84坐标系
        // 按照区域检索内部污染源信息
        var result = mutableListOf<SceneInfo>()
        // 1. 首先按照四至范围从数据库初步筛选污染源,此处的区域坐标已转换为火星坐标系
        val polygonTmp = pollutedArea.polygon!!
        val fb = MapUtil.calFourBoundaries(polygonTmp)
        val sceneList = sceneInfoRep.findByCoordinateRange(fb)
        // 2. 再精确判断是否在反向溯源区域多边形内部
        sceneList.forEach {
            val point = it!!.longitude.toDouble() to it.latitude.toDouble()
            if (MapUtil.isPointInPolygon(point, polygonTmp)) {
                result.add(it)
            }
        }
 
        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()
        }
 
        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
        }
    }
}