feiyu02
2025-09-02 8c6e742562d0c8647e0ee8deff01a3eb176d677b
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package com.flightfeather.uav.biz.sourcetrace.model
 
import com.flightfeather.uav.common.utils.DateUtil
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
        this.sceneList = emptyList()
 
        if (polygonTmp != null) {
            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)
            closeSceneList.forEach {
                val point = it!!.longitude.toDouble() to it.latitude.toDouble()
                if (MapUtil.isPointInPolygon(point, closePolygonTmp)) {
                    result.add(it)
                }
            }
            // 去重
            result = result.distinctBy { it.guid }.toMutableList()
 
            // 根据污染因子的量级,计算主要的污染场景类型,筛选结果
            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)
        }
 
        val txt = summaryTxt(pollutedData, this.sceneList!!)
        this.conclusion = txt
    }
 
    /**
     * 计算可能的相关污染场景类型以及推理结论
     */
    @Throws(Exception::class)
    private fun calSceneType(pollutedData: PollutedData): Pair<String, List<SceneType>>? {
        var des: String? = null
        val sceneTypes = mutableListOf<SceneType>()
        pollutedData.statisticMap.entries.forEach { s ->
            val res = when (s.key) {
                // 氮氧化合物,一般由于机动车尾气,同步计算CO
                FactorType.NO2 -> {
                    val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                    "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                }
 
                FactorType.CO -> null
 
                FactorType.H2S -> null
 
                FactorType.SO2 -> null
 
                FactorType.O3 -> null
                // a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
                // b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
                FactorType.PM25,
                FactorType.PM10,
                    -> {
                    val pm25Avg = round(pollutedData.dataList.map { it.pm25!! }.average() * 10) / 10
                    val pm10Avg = round(pollutedData.dataList.map { it.pm10!! }.average() * 10) / 10
                    // 计算异常数据的pm2.5占pm10比重的均值
                    val percentageAvg = pollutedData.dataList.map {
                        it.pm25!! / it.pm10!!
                    }.average()
                    val str =
                        "PM2.5量级为${pm25Avg}μg/m³,PM10量级为${pm10Avg}μg/m³,PM2.5占PM10的比重为${round(percentageAvg * 100)}%"
                    if (percentageAvg > 0.666) {
                        "${str},比重较大,污染源以餐饮为主,工地次之" to
                                listOf(
                                    SceneType.TYPE1,
                                    SceneType.TYPE2,
                                    SceneType.TYPE3,
                                    SceneType.TYPE14,
                                    SceneType.TYPE5
                                )
                    } else if (percentageAvg < 0.333) {
                        "${str},比重较小,属于大颗粒扬尘污染,污染源以工地为主" to
                                listOf(
                                    SceneType.TYPE1,
                                    SceneType.TYPE2,
                                    SceneType.TYPE3,
                                    SceneType.TYPE14,
                                    SceneType.TYPE5
                                )
                    } else {
                        "${str},污染源以餐饮、工地为主" 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
                    "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE6, SceneType.TYPE17, SceneType.TYPE12)
                }
 
                else -> null
            }
            des = res?.first
            res?.second?.let { sceneTypes.addAll(it) }
        }
        return (des ?: "") to sceneTypes
    }
 
    /**
     * 计算最近的监测站点
     */
    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
        }
    }
 
    private fun summaryTxt(pollutedData: PollutedData, sceneList: List<SceneInfoVo>): String {
//        pollutedData.exception
//        pollutedData.selectedFactor?.main
        val st = DateUtil.instance.getTime(pollutedData.startTime)
        val et = DateUtil.instance.getTime(pollutedData.endTime)
        var txt =
            "在${st}至${et}之间,出现${pollutedData.exception}"
        pollutedData.statisticMap.entries.forEach { s ->
            txt += ",${s.key.des}最低值为${s.value.min}μg/m³,最高值为${s.value.max}μg/m³,均值为${s.value.avg}μg/m³"
        }
        if (sceneList.isEmpty()) {
            txt += (",可能存在隐藏风险源。")
        } else {
            txt += (",发现${sceneList.size}个风险源,包含")
 
            val sizeMap = mutableMapOf<String, Int>()
            sceneList.forEach {
                if (!sizeMap.containsKey(it.type)) {
                    sizeMap[it.type] = 0
                }
                sizeMap[it.type] = sizeMap[it.type]!! + 1
            }
            sizeMap.forEach { (t, u) ->
                txt += ("${u}个${t},")
            }
            txt = txt.replaceRange(txt.length - 1, txt.length, "。")
        }
 
        return txt
    }
}