2025.12.18
1. 动态溯源相关分析逻辑调整;
2. 走航报告接口参数调整;
| | |
| | | |
| | | pollutedClues.forEach { pollutedClue -> |
| | | if (pollutedClue == null) return@forEach |
| | | val dataList = pollutedClue.pollutedData?.dataList ?: emptyList() |
| | | if (dataList.isEmpty()) return@forEach |
| | | |
| | | // 计算单个PollutedClue的均值经纬度 |
| | | val avgData = dataList.avg() |
| | | val wgs84Lng = avgData.longitude?.toDouble() ?: return@forEach |
| | | val wgs84Lat = avgData.latitude?.toDouble() ?: return@forEach |
| | | val wgs84Center = pollutedClue.pollutedData?.getExceptionCenter() ?: return@forEach |
| | | |
| | | // 坐标转换 |
| | | val gcj02Point = MapUtil.wgs84ToGcj02(wgs84Lng to wgs84Lat) |
| | | val gcj02Point = MapUtil.wgs84ToGcj02(wgs84Center) |
| | | |
| | | // 查找最近场景 |
| | | var minDistance = Double.MAX_VALUE |
| | |
| | | package com.flightfeather.uav.biz.sourcetrace.model |
| | | |
| | | import com.flightfeather.uav.biz.FactorFilter |
| | | import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag |
| | | import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig |
| | | import com.flightfeather.uav.common.net.AMapService |
| | | import com.flightfeather.uav.common.utils.MapUtil |
| | |
| | | */ |
| | | |
| | | constructor( |
| | | historyData: List<BaseRealTimeData>, |
| | | exceptionData: List<BaseRealTimeData>, |
| | | pollutedData: PollutedData, |
| | | config: RTExcWindLevelConfig, |
| | | windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?, |
| | | ) : this() { |
| | | distanceType = windLevelCondition?.distanceType |
| | | distanceRange = distanceType?.disRange |
| | | distanceDes = distanceType?.des |
| | | windLevelCondition?.let { sourceTrace(historyData, exceptionData, config, it) } |
| | | windLevelCondition?.let { sourceTrace(pollutedData, config, it) } |
| | | } |
| | | |
| | | // 所属街镇 |
| | |
| | | * 反向溯源 |
| | | */ |
| | | private fun sourceTrace( |
| | | historyData: List<BaseRealTimeData>, |
| | | exceptionData: List<BaseRealTimeData>, |
| | | pollutedData: PollutedData, |
| | | config: RTExcWindLevelConfig, |
| | | windLevelCondition: RTExcWindLevelConfig.WindLevelCondition, |
| | | ) { |
| | | val avgData = if (exceptionData.size == 1) { |
| | | exceptionData.first() |
| | | } else { |
| | | exceptionData.avg() |
| | | } |
| | | |
| | | val avgData = pollutedData.getExceptionAvgData() |
| | | val pair = avgData.longitude!!.toDouble() to avgData.latitude!!.toDouble() |
| | | |
| | | polygon = calSector( |
| | | avgData.windDirection?.toDouble() ?: .0, |
| | | pair, |
| | |
| | | MapUtil.wgs84ToGcj02(it) |
| | | } |
| | | |
| | | closePolygon = closeSourceTrace(historyData, pair).map { |
| | | closePolygon = closeSourceTrace(pair).map { |
| | | // 将坐标转换为gcj02(火星坐标系),因为污染源场景信息都为此坐标系 |
| | | MapUtil.wgs84ToGcj02(it) |
| | | } |
| | |
| | | return result |
| | | } |
| | | |
| | | private fun closeSourceTrace( |
| | | historyData: List<BaseRealTimeData>, |
| | | center: Pair<Double, Double>, |
| | | ): List<Pair<Double, Double>> { |
| | | private fun closeSourceTrace(center: Pair<Double, Double>): List<Pair<Double, Double>> { |
| | | val result = mutableListOf<Pair<Double, Double>>() |
| | | var startDeg = 0 |
| | | while (startDeg <= 360) { |
| | |
| | | ) : this() { |
| | | if (exceptions.isEmpty() || exceptions[0].second.exceptionData.isEmpty()) return |
| | | deviceCode = exceptions[0].second.startData?.deviceCode |
| | | var startData: BaseRealTimeData? = null |
| | | var endData: BaseRealTimeData? = null |
| | | var exceptionData = mutableListOf<BaseRealTimeData>() |
| | | var historyData = mutableListOf<BaseRealTimeData>() |
| | | exceptions.forEach { e -> |
| | | // 将采样时间最早的作为开始数据 |
| | | if (startData == null) { |
| | | startData = e.second.startData |
| | | } else { |
| | | if (e.second.startData?.dataTime!! < startData!!.dataTime) { |
| | | startData = e.second.startData |
| | | } |
| | | } |
| | | |
| | | // 将采样时间最晚的作为结束数据 |
| | | if (endData == null) { |
| | | endData = e.second.endData |
| | | } else { |
| | | if (e.second.endData?.dataTime!! > endData!!.dataTime) { |
| | | endData = e.second.endData |
| | | } |
| | | } |
| | | |
| | | // 将所有异常数据去重合并 |
| | | if (exceptionData.isEmpty()) { |
| | | exceptionData = e.second.exceptionData |
| | | } else { |
| | | e.second.exceptionData.forEach { |
| | | if (exceptionData.find { d -> d.dataTime == it.dataTime } == null) { |
| | | exceptionData.add(it) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 将所有历史数据去重合并 |
| | | if (historyData.isEmpty()) { |
| | | historyData = e.second.historyData |
| | | } else { |
| | | e.second.historyData.forEach { |
| | | if (historyData.find { d -> d.dataTime == it.dataTime } == null) { |
| | | historyData.add(it) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // 按照采样时间升序排列 |
| | | exceptionData.sortBy { it.dataTime } |
| | | historyData.sortBy { it.dataTime } |
| | | |
| | | // 获取去重后的监测因子类型 |
| | | val factorList = exceptions.map { it.first }.distinct() |
| | | pollutedData = PollutedData( |
| | | startData!!, endData, factorList, exceptionData, historyData, eType, windLevelCondition |
| | | ) |
| | | pollutedArea = PollutedArea(historyData, exceptionData, config, windLevelCondition) |
| | | pollutedData = PollutedData(exceptions, eType) |
| | | pollutedArea = PollutedArea(pollutedData!!, config, windLevelCondition) |
| | | |
| | | } |
| | | |
| | |
| | | package com.flightfeather.uav.biz.sourcetrace.model |
| | | |
| | | import com.flightfeather.uav.biz.FactorFilter |
| | | 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 |
| | | import com.flightfeather.uav.domain.entity.BaseRealTimeData |
| | | import com.flightfeather.uav.domain.entity.avg |
| | | import com.flightfeather.uav.lightshare.bean.DataVo |
| | | import com.flightfeather.uav.socket.eunm.FactorType |
| | | import java.util.Date |
| | |
| | | */ |
| | | class PollutedData() { |
| | | |
| | | companion object { |
| | | // 默认数据采样时间间隔,单位:秒 |
| | | const val DEFAULT_PERIOD = 4 |
| | | } |
| | | |
| | | /** |
| | | * 异常数据分组情况统计 |
| | | */ |
| | | inner class ExcGroup{ |
| | | constructor(dataIndexList: List<Int>, factorType: FactorType){ |
| | | this.dataIndexList = dataIndexList |
| | | this.factorType = factorType |
| | | val first = getFirstDataValue()?.toDouble() |
| | | val last = getLastDataValue()?.toDouble() |
| | | if (first != null && last != null) { |
| | | per = round((last - first) / first * 100) / 100 |
| | | rate = round((last - first) / DEFAULT_PERIOD * 100) / 100 |
| | | } |
| | | } |
| | | var factorType: FactorType? = null |
| | | /** |
| | | * 异常数据对应历史数据[historyDataList]中的索引值 |
| | | */ |
| | | var dataIndexList: List<Int>? = null |
| | | // 变化幅度 |
| | | var per: Double? = null |
| | | // 变化速率 |
| | | var rate: Double? = null |
| | | |
| | | /** |
| | | * 获取异常数据的第一个数据 |
| | | * !!!!第一个数据其实是首个异常数据的前一个数据值!!!! |
| | | */ |
| | | fun getFirstData(): BaseRealTimeData? { |
| | | return dataIndexList?.firstOrNull()?.let { |
| | | val i = if (it > 0) it - 1 else it |
| | | historyDataList[i].toBaseRealTimeData(BaseRealTimeData::class.java) |
| | | } |
| | | } |
| | | fun getFirstDataValue(): Float? { |
| | | return getFirstData()?.getByFactorType(factorType) |
| | | } |
| | | |
| | | /** |
| | | * 获取异常数据的最后一个数据 |
| | | */ |
| | | fun getLastData(): BaseRealTimeData? { |
| | | return dataIndexList?.lastOrNull()?.let { |
| | | historyDataList[it].toBaseRealTimeData(BaseRealTimeData::class.java) |
| | | } |
| | | } |
| | | fun getLastDataValue(): Float? { |
| | | return getLastData()?.getByFactorType(factorType) |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 各监测因子异常统计信息 |
| | | */ |
| | | inner class Statistic(){ |
| | | var factorId: Int? = null |
| | | var factorName: String? = null |
| | | var subFactorId: List<Int>? = null |
| | | var subFactorName: List<String>? = null |
| | | var selectedFactor: FactorFilter.SelectedFactor? = null |
| | | |
| | | /** |
| | | * 异常数据对应历史数据[historyDataList]中的索引值 |
| | | */ |
| | | var dataIndexList: List<Int>? = null |
| | | |
| | | // 因子量级平均变化幅度 |
| | | var avgPer: Double? = null |
| | |
| | | var avg: Double? = null |
| | | var min: Double? = null |
| | | var max: Double? = null |
| | | |
| | | var excGroup: List<ExcGroup>? = null |
| | | |
| | | /** |
| | | * 获取异常数据 |
| | | */ |
| | | fun getExceptionData(): List<BaseRealTimeData>? { |
| | | return dataIndexList?.map { historyDataList[it].toBaseRealTimeData(BaseRealTimeData::class.java) } |
| | | } |
| | | |
| | | constructor( |
| | | start: BaseRealTimeData, |
| | | end: BaseRealTimeData?, |
| | | factorList: List<FactorFilter.SelectedFactor>, |
| | | exceptionData: List<BaseRealTimeData>, |
| | | historyData: List<BaseRealTimeData>, |
| | | eType: ExceptionType, |
| | | windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?, |
| | | ) : this() { |
| | | /** |
| | | * 获取异常数据分段情况 |
| | | * 将连续的异常数据分为一组 |
| | | */ |
| | | fun getExceptionDataGroup(): List<List<Int>> { |
| | | val res = mutableListOf<MutableList<Int>>() |
| | | var curGroup = mutableListOf<Int>() |
| | | var lastIndex = -2 |
| | | dataIndexList?.forEach { |
| | | if (curGroup.isEmpty()) { |
| | | curGroup.add(it) |
| | | } else if (it - lastIndex == 1) { |
| | | curGroup.add(it) |
| | | } else { |
| | | res.add(curGroup) |
| | | curGroup = mutableListOf(it) |
| | | } |
| | | lastIndex = it |
| | | } |
| | | if (curGroup.isNotEmpty()) { |
| | | res.add(curGroup) |
| | | } |
| | | return res |
| | | } |
| | | } |
| | | |
| | | constructor(exceptions: List<Pair<FactorFilter.SelectedFactor, ExceptionTag>>, eType: ExceptionType,) : this() { |
| | | // 遍历所有的因子的异常,整合统一的异常结果,具体如下 |
| | | var startData: BaseRealTimeData? = null |
| | | var endData: BaseRealTimeData? = null |
| | | var historyData = mutableListOf<BaseRealTimeData>() |
| | | var _times = 0 |
| | | exceptions.forEach { e -> |
| | | // 将采样时间最早的数据作为开始数据 |
| | | if (startData == null) { |
| | | startData = e.second.startData |
| | | } else { |
| | | if (e.second.startData?.dataTime!! < startData!!.dataTime) { |
| | | startData = e.second.startData |
| | | } |
| | | } |
| | | |
| | | // 将采样时间最晚的作为结束数据 |
| | | if (endData == null) { |
| | | endData = e.second.endData |
| | | } else { |
| | | if (e.second.endData?.dataTime!! > endData!!.dataTime) { |
| | | endData = e.second.endData |
| | | } |
| | | } |
| | | // 将所有历史数据去重合并 |
| | | if (historyData.isEmpty()) { |
| | | historyData = e.second.historyData |
| | | } else { |
| | | e.second.historyData.forEach { |
| | | if (historyData.find { d -> d.dataTime == it.dataTime } == null) { |
| | | historyData.add(it) |
| | | } |
| | | } |
| | | } |
| | | |
| | | _times += e.second.exceptionData.size |
| | | } |
| | | // 按照采样时间升序排列 |
| | | historyData.sortBy { it.dataTime } |
| | | |
| | | exception = eType.des |
| | | exceptionType = eType.value |
| | | |
| | | startTime = start.dataTime |
| | | endTime = end?.dataTime |
| | | // startData = start.getByFactorType(factor.main) |
| | | // endData = end?.getByFactorType(factor.main) ?: startData |
| | | // startData = start |
| | | // endData = end |
| | | |
| | | windSpeed = exceptionData.first().windSpeed?.toDouble() |
| | | times = windLevelCondition?.countLimit |
| | | |
| | | dataList.add(start) |
| | | exceptionData.forEach { |
| | | dataList.add(it) |
| | | } |
| | | dataVoList.addAll(dataList.map { it.toDataVo() }) |
| | | startTime = startData?.dataTime |
| | | endTime = endData?.dataTime |
| | | windSpeed = historyData.avg().windSpeed?.toDouble() |
| | | times = _times |
| | | historyDataList.addAll(historyData.map { it.toDataVo() }) |
| | | |
| | | // 再次整合异常数据,分别计算各因子的异常结果统计 |
| | | exceptions.forEach {e -> |
| | | statisticMap[e.first.main] = Statistic().apply { |
| | | factorId = e.first.main.value |
| | | factorName = e.first.main.des |
| | | subFactorId = e.first.subs.map { it.value } |
| | | subFactorName = e.first.subs.map { it.des } |
| | | selectedFactor = e.first |
| | | dataIndexList = e.second.exceptionData.map { |
| | | historyDataList.indexOfFirst { d -> |
| | | d.time == DateUtil.instance.dateToString(it.dataTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS) |
| | | } |
| | | } |
| | | |
| | | factorList.forEach { f-> |
| | | statisticMap[f.main] = Statistic().apply { |
| | | factorId = f.main.value |
| | | factorName = f.main.des |
| | | subFactorId = f.subs.map { it.value } |
| | | subFactorName = f.subs.map { it.des } |
| | | selectedFactor = f |
| | | |
| | | avgPer = calPer(f.main) |
| | | avgRate = calRate(f.main) |
| | | |
| | | val s = dataSummary(dataList, f.main) |
| | | val s = dataSummary(e.second.exceptionData, e.first.main) |
| | | avg = s.first |
| | | min = s.second |
| | | max = s.third |
| | | |
| | | excGroup = getExceptionDataGroup().map { ExcGroup(it, e.first.main) } |
| | | avgPer = excGroup?.mapNotNull { it.per }?.average() |
| | | avgRate = excGroup?.mapNotNull { it.rate }?.average() |
| | | } |
| | | } |
| | | } |
| | | |
| | | var deviceCode: String? = null |
| | | |
| | | var exception: String? = null |
| | | var exceptionType: Int? = null |
| | | |
| | | var startTime: Date? = null |
| | | var endTime: Date? = null |
| | | |
| | | // var startData: BaseRealTimeData? = null |
| | | // var endData: BaseRealTimeData? = null |
| | | |
| | | // 风速 |
| | | var windSpeed: Double? = null |
| | | |
| | | // 发生次数 |
| | | var times: Int? = null |
| | | |
| | | var historyDataList = mutableListOf<DataVo>() |
| | | // 异常监测数据,包含单次异常中所有发生了异常的数据值(可能不是时间连续的数据) |
| | | var dataList: MutableList<BaseRealTimeData> = mutableListOf() |
| | | var dataVoList: MutableList<DataVo> = mutableListOf() |
| | | |
| | | // var dataList: MutableList<BaseRealTimeData> = mutableListOf() |
| | | // var dataVoList: MutableList<DataVo> = mutableListOf() |
| | | var statisticMap = mutableMapOf<FactorType, Statistic>() |
| | | |
| | | /** |
| | | * 获取所有异常因子名称 |
| | | */ |
| | | fun toFactorNames(): String { |
| | | val factors = statisticMap.entries.map { it.key }.sortedBy { it.value }.joinToString(";") { it.des } |
| | | return factors |
| | | } |
| | | |
| | | private fun calPer(factorType: FactorType): Double? { |
| | | val list = dataList |
| | | if (list.size < 2) return null |
| | | fun getExceptionAvgData(): BaseRealTimeData { |
| | | val exceptionDataList = statisticMap.flatMap { it.value.getExceptionData() ?: emptyList() } |
| | | val avgData = exceptionDataList.avg() |
| | | return avgData |
| | | } |
| | | /** |
| | | * 获取异常数据中心坐标(异常数据中经度纬度的平均值) |
| | | */ |
| | | fun getExceptionCenter(): Pair<Double, Double>? { |
| | | val avgData = getExceptionAvgData() |
| | | val wgs84Lng = avgData.longitude?.toDouble() |
| | | val wgs84Lat = avgData.latitude?.toDouble() |
| | | return if (wgs84Lng == null || wgs84Lat == null) null else Pair(wgs84Lng, wgs84Lat) |
| | | } |
| | | |
| | | private fun calPer(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Double? { |
| | | if (exceptionData.size < 2) return null |
| | | |
| | | var total = .0 |
| | | for (i in 0 until list.size - 1) { |
| | | val p = list[i].getByFactorType(factorType) ?: .0f |
| | | val n = list[i + 1].getByFactorType(factorType) ?: .0f |
| | | for (i in 0 until exceptionData.size - 1) { |
| | | val p = exceptionData[i]?.getByFactorType(factorType) ?: .0f |
| | | val n = exceptionData[i + 1]?.getByFactorType(factorType) ?: .0f |
| | | total += (n - p) / p |
| | | } |
| | | return total / (list.size - 1) |
| | | return total / (exceptionData.size - 1) |
| | | } |
| | | |
| | | private fun calRate(factorType: FactorType): Double? { |
| | | val list = dataList |
| | | if (list.size < 2) return null |
| | | private fun calRate(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Double? { |
| | | if (exceptionData.size < 2) return null |
| | | |
| | | var total = .0 |
| | | for (i in 0 until list.size - 1) { |
| | | val p = list[i].getByFactorType(factorType) ?: .0f |
| | | val n = list[i + 1].getByFactorType(factorType) ?: .0f |
| | | for (i in 0 until exceptionData.size - 1) { |
| | | val p = exceptionData[i]?.getByFactorType(factorType) ?: .0f |
| | | val n = exceptionData[i + 1]?.getByFactorType(factorType) ?: .0f |
| | | total += (n - p) / 4 |
| | | } |
| | | return total / (list.size - 1) |
| | | return total / (exceptionData.size - 1) |
| | | } |
| | | |
| | | private fun dataSummary(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Triple<Double, Double, |
| | |
| | | import com.flightfeather.uav.socket.eunm.FactorType |
| | | import org.springframework.beans.BeanUtils |
| | | import org.springframework.web.context.ContextLoader |
| | | import kotlin.math.round |
| | | |
| | | /** |
| | | * 污染来源 |
| | |
| | | |
| | | // 5. 根据污染因子的量级,计算主要的污染场景类型,筛选结果 |
| | | val mainSceneType = calSceneType(pollutedData) |
| | | if (mainSceneType != null) { |
| | | result = result.filter { |
| | | val r = mainSceneType.second.find { s -> |
| | | val r = mainSceneType.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>>? { |
| | | var des: String? = null |
| | | private fun calSceneType(pollutedData: PollutedData): List<SceneType> { |
| | | val sceneTypes = mutableListOf<SceneType>() |
| | | pollutedData.statisticMap.entries.forEach { s -> |
| | | val res = when (s.key) { |
| | |
| | | FactorType.NO, |
| | | FactorType.NO2, |
| | | -> { |
| | | // val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000 |
| | | val coAvg = round(pollutedData.statisticMap[FactorType.CO]?.avg ?: .0) / 1000 |
| | | "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to |
| | | listOf(SceneType.TYPE1, SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17) |
| | | } |
| | | |
| | | FactorType.CO -> "" to listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17) |
| | | |
| | | FactorType.CO -> listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17) |
| | | 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 |
| | | val pm25Avg = round((pollutedData.statisticMap[FactorType.PM25]?.avg ?: .0) * 10) / 10 |
| | | val pm10Avg = round((pollutedData.statisticMap[FactorType.PM10]?.avg ?: .0) * 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, |
| | | SceneType.TYPE18 |
| | | ) |
| | | } else if (percentageAvg < 0.333) { |
| | | "${str},比重较小,属于大颗粒扬尘污染,污染源以工地为主" to |
| | | listOf( |
| | | SceneType.TYPE1, |
| | | SceneType.TYPE2, |
| | | SceneType.TYPE3, |
| | | SceneType.TYPE14, |
| | | SceneType.TYPE5, |
| | | SceneType.TYPE18 |
| | | ) |
| | | } else { |
| | | "${str},污染源以餐饮、工地为主" to |
| | | listOf( |
| | | SceneType.TYPE1, |
| | | SceneType.TYPE2, |
| | |
| | | SceneType.TYPE18 |
| | | ) |
| | | } |
| | | } |
| | | // 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 |
| | | val pm25Avg = round((pollutedData.statisticMap[FactorType.PM25]?.avg ?: .0)) / 10 |
| | | val coAvg = round((pollutedData.statisticMap[FactorType.CO]?.avg ?: .0)) / 1000 |
| | | val o3Avg = round((pollutedData.statisticMap[FactorType.O3]?.avg ?: .0)) / 10 |
| | | "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to |
| | | listOf(SceneType.TYPE5, SceneType.TYPE6, SceneType.TYPE17, SceneType.TYPE12, SceneType.TYPE18) |
| | | } |
| | | |
| | | else -> null |
| | | } |
| | | des = res?.first |
| | | res?.second?.let { sceneTypes.addAll(it) } |
| | | res?.let { sceneTypes.addAll(it) } |
| | | } |
| | | return (des ?: "") to sceneTypes |
| | | return sceneTypes.distinct() |
| | | } |
| | | |
| | | /** |
| | |
| | | val et = DateUtil.instance.getTime(pollutedData.endTime) |
| | | |
| | | // 1. 描述异常发生的时间和异常类型 |
| | | var txt = "在${st}至${et}之间,出现${pollutedData.exception}" |
| | | var txt = "在${st}至${et}之间,出现${pollutedData.exception}${pollutedData.times}次" |
| | | |
| | | // 2. 描述异常数据的变化情况 |
| | | // 异常数据长度应该大于1,首个值是异常开始数据的前一个正常值,后续为异常数据值(但不一定时间连续) |
| | | if (pollutedData.dataList.size > 1) { |
| | | val historyDataList = pollutedData.historyDataList.map { it.toBaseRealTimeData(BaseRealTimeData::class.java) } |
| | | when (pollutedData.exceptionType) { |
| | | // 量级突变 |
| | | ExceptionType.TYPE4.value -> { |
| | | val exceptionPair = mutableListOf<Pair<BaseRealTimeData, BaseRealTimeData>>() |
| | | pollutedData.dataList.forEachIndexed { index, baseRealTimeData -> |
| | | if (index == 0) return@forEachIndexed |
| | | val preIndex = historyDataList.indexOfFirst { |
| | | it.dataTime == baseRealTimeData.dataTime |
| | | } |
| | | exceptionPair.add( |
| | | (if (preIndex - 1 < 0) historyDataList[0] else historyDataList[preIndex - 1]) |
| | | to baseRealTimeData |
| | | ) |
| | | } |
| | | val statArr = mutableListOf<String>() |
| | | pollutedData.statisticMap.entries.forEach { s -> |
| | | val txtArr = mutableListOf<String>() |
| | | exceptionPair.forEach exception@{ p -> |
| | | val preValue = p.first.getByFactorType(s.key) |
| | | val curValue = p.second.getByFactorType(s.key) |
| | | if (preValue == null || curValue == null) return@exception |
| | | val r = round((curValue - preValue) / preValue * 100) |
| | | txtArr.add("从${preValue}μg/m³突变至${curValue}μg/m³,变化率为${r}%") |
| | | s.value.excGroup?.forEach exception@{ p -> |
| | | val preValue = p.getFirstDataValue() |
| | | val curValue = p.getLastDataValue() |
| | | val per = p.per?.times(100) |
| | | val rate = p.rate |
| | | if (preValue == null || curValue == null || per == null) return@exception |
| | | when (pollutedData.exceptionType) { |
| | | // 量级突变 |
| | | ExceptionType.TYPE4.value -> { |
| | | txtArr.add("从${preValue}μg/m³突变至${curValue}μg/m³,变化率为${per}%") |
| | | } |
| | | // 快速上升 |
| | | ExceptionType.TYPE9.value -> { |
| | | txtArr.add("从${preValue}μg/m³快速上升至${curValue}μg/m³,变化速率为${rate}μg/m³/秒,变化率为${per}%") |
| | | } |
| | | } |
| | | } |
| | | statArr.add("${s.key.getTxt()}量级${txtArr.joinToString(",")}") |
| | | } |
| | | txt += ",${statArr.joinToString(";")}" |
| | | } |
| | | // 快速上升 |
| | | ExceptionType.TYPE9.value -> { |
| | | pollutedData.statisticMap.entries.forEach { s -> |
| | | val preValue = pollutedData.dataList.first().getByFactorType(s.key) |
| | | val curValue = pollutedData.dataList.last().getByFactorType(s.key) |
| | | if (preValue == null || curValue == null) return@forEach |
| | | val r = round((curValue - preValue) / preValue * 100) |
| | | txt += ",${s.key.getTxt()}从${preValue}μg/m³快速上升至${curValue}μg/m³,变化率为${r}%" |
| | | } |
| | | } |
| | | } |
| | | } else { |
| | | pollutedData.statisticMap.entries.forEach { s -> |
| | | txt += ",${s.key.des}最低值为${s.value.min}μg/m³,最高值为${s.value.max}μg/m³,均值为${s.value.avg}μg/m³" |
| | | } |
| | | } |
| | | |
| | | // 3. 描述发现的风险源情况 |
| | | if (sceneList.isEmpty()) { |
| | |
| | | import com.fasterxml.jackson.annotation.JsonFormat |
| | | import io.swagger.annotations.ApiModel |
| | | import io.swagger.annotations.ApiModelProperty |
| | | import io.swagger.annotations.ApiParam |
| | | import org.springframework.format.annotation.DateTimeFormat |
| | | import org.springframework.web.bind.annotation.RequestParam |
| | | import java.time.LocalDateTime |
| | | |
| | | /** |
| | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | @ApiModelProperty(value = "结束时间") |
| | | var endTime: LocalDateTime? = null |
| | | |
| | | @ApiModelProperty(value = "数据颗粒度", allowableValues = "SECOND, MINUTE, HOUR") |
| | | var granularity: String? = null |
| | | |
| | | @ApiParam("需要统计的监测因子", example = "NO2, CO") |
| | | var factorTypes: List<String>? = null |
| | | } |
| | |
| | | * @param missionCode 走航任务编号,用于唯一标识特定的走航任务 |
| | | * @return 异常结果列表,每个元素包含异常类型、位置和详细信息 |
| | | */ |
| | | @Deprecated("该接口已废弃,使用/report/missionSummary/one接口") |
| | | fun pollutionTrace(missionCode: String): List<ExceptionResult> |
| | | |
| | | /** |
| | |
| | | if (removeNoPollutedSource) { |
| | | clues.removeIf { it?.pollutedSource?.sceneList.isNullOrEmpty() } |
| | | } |
| | | // val keyScenes = sceneInfoRep.findBySceneTypes( |
| | | // listOf( |
| | | // SceneType.TYPE19.value, |
| | | // SceneType.TYPE20.value, |
| | | // SceneType.TYPE21.value |
| | | // ) |
| | | // ) |
| | | return MissionRiskArea().generateClueByRiskArea(clues) |
| | | } |
| | | |
| | |
| | | @RequestMapping("air/analysis") |
| | | class DataAnalysisController(private val dataAnalysisService: DataAnalysisService) { |
| | | |
| | | @Deprecated("该接口已废弃,使用/report/missionSummary/one接口") |
| | | @ApiOperation(value = "污染溯源分析") |
| | | @GetMapping("/pollution/trace") |
| | | fun pollutionTrace( |
| | | @ApiParam("走航任务编号") @RequestParam missionCode: String, |
| | | // @RequestParam("page", required = false) page: Int?, |
| | | // @RequestParam("per_page", required = false) perPage: Int?, |
| | | ) = resPack { dataAnalysisService.pollutionTrace(missionCode) } |
| | | |
| | | @ApiOperation(value = "获取历史污染溯源结果") |
| | |
| | | @ApiOperation(value = "生成走航任务汇总统计") |
| | | @PostMapping("/report/missionSummary") |
| | | fun generateMissionSummary( |
| | | @ApiParam("开始时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | startTime: LocalDateTime, |
| | | @ApiParam("结束时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | endTime: LocalDateTime, |
| | | @ApiParam("区域") @RequestBody areaVo: AreaVo, |
| | | @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption, |
| | | ) = resPack { |
| | | if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null) |
| | | throw BizException("参数错误, startTime, endTime, area不能为空") |
| | | val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val areaVo = analysisOption.area!! |
| | | |
| | | dataAnalysisService.generateMissionSummary( |
| | | Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | | Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | |
| | | @ApiOperation(value = "生成走航任务清单") |
| | | @PostMapping("/report/missionList") |
| | | fun generateMissionList( |
| | | @ApiParam("开始时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | startTime: LocalDateTime, |
| | | @ApiParam("结束时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | endTime: LocalDateTime, |
| | | @ApiParam("区域") @RequestBody areaVo: AreaVo, |
| | | @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption, |
| | | ) = resPack { |
| | | if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null) |
| | | throw BizException("参数错误, startTime, endTime, area不能为空") |
| | | val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val areaVo = analysisOption.area!! |
| | | |
| | | dataAnalysisService.generateMissionList( |
| | | Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | | Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | |
| | | @ApiOperation(value = "生成走航任务详情") |
| | | @PostMapping("/report/missionDetail") |
| | | fun generateMissionDetail( |
| | | @ApiParam("开始时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | startTime: LocalDateTime, |
| | | @ApiParam("结束时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | endTime: LocalDateTime, |
| | | @ApiParam("数据颗粒度", allowableValues = "SECOND, MINUTE, HOUR") @RequestParam(required = false) |
| | | granularity: String?, |
| | | @ApiParam("区域") @RequestBody areaVo: AreaVo, |
| | | @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption, |
| | | ) = resPack { |
| | | if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null) |
| | | throw BizException("参数错误, startTime, endTime, area不能为空") |
| | | val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val areaVo = analysisOption.area!! |
| | | val granularity = analysisOption.granularity |
| | | |
| | | dataAnalysisService.generateMissionDetail( |
| | | Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | | Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | |
| | | @ApiOperation(value = "走航典型隐患区域统计") |
| | | @PostMapping("/report/clueByRiskArea") |
| | | fun generateClueByRiskArea( |
| | | @ApiParam("区域") @RequestBody analysisOption: AnalysisOption, |
| | | @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption, |
| | | ) = resPack { |
| | | if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null) |
| | | throw BizException("参数错误, startTime, endTime, areaVo不能为空") |
| | | throw BizException("参数错误, startTime, endTime, area不能为空") |
| | | val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val areaVo = analysisOption.area!! |
| | |
| | | @ApiOperation(value = "叠加融合分析") |
| | | @PostMapping("/report/gridFusion") |
| | | fun generateGridFusion( |
| | | @ApiParam("开始时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | startTime: LocalDateTime, |
| | | @ApiParam("结束时间") @RequestParam |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | endTime: LocalDateTime, |
| | | @ApiParam("需要统计的监测因子", example = "NO2, CO") @RequestParam |
| | | factorTypes: String, |
| | | @ApiParam("区域") @RequestBody areaVo: AreaVo, |
| | | @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption, |
| | | ) = resPack { |
| | | if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null || analysisOption.factorTypes == null) |
| | | throw BizException("参数错误, startTime, endTime, area, factorTypes不能为空") |
| | | val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant() |
| | | val areaVo = analysisOption.area!! |
| | | val factorTypes = analysisOption.factorTypes!!.map { FactorType.valueOf(it) } |
| | | |
| | | dataAnalysisService.generateGridFusion( |
| | | factorTypes.split(",").map { FactorType.valueOf(it) }, |
| | | factorTypes, |
| | | Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | | Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()), |
| | | areaVo |
| | |
| | | @Test |
| | | fun autoSourceTrace() { |
| | | val sourceTraceController = SourceTraceController(sceneInfoRep, sourceTraceRep, true) |
| | | // val mCode = listOf( |
| | | // "SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)", |
| | | // "SH-CN-20240830(04)", "SH-CN-20240823", |
| | | // "SH-CN-20240723(02)", |
| | | //// "SH-CN-20250723(01)" |
| | | // ) |
| | | // val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | // val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val startTime = LocalDateTime.of(2025, 11, 2, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = LocalDateTime.of(2025, 11, 2, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val startTime = LocalDateTime.of(2025, 12, 11, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = LocalDateTime.of(2025, 12, 11, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val missions = missionMapper.selectByExample(Example(Mission::class.java).apply { |
| | | createCriteria().andBetween("startTime", startTime, endTime) |
| | | }) |
| | |
| | | } |
| | | sourceTraceController.initTask() |
| | | } |
| | | // val missions = missionMapper.selectByExample(Example(Mission::class.java).apply { |
| | | // createCriteria().andEqualTo("deviceType", "0a") |
| | | // .andLessThanOrEqualTo("startTime", "2024-11-07 15:00:00") |
| | | // orderBy("startTime").desc() |
| | | // }) |
| | | // missions.forEach { m -> |
| | | // val rtData = realTimeDataService.getSecondData( |
| | | // m?.deviceType, |
| | | // m?.deviceCode, |
| | | // DateUtil.instance.dateToString(m?.startTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS), |
| | | // DateUtil.instance.dateToString(m?.endTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS), |
| | | // null, |
| | | // 1, |
| | | // 10000 |
| | | // ) |
| | | // rtData.data?.forEach { d -> |
| | | // val rtdVehicle = d.toBaseRealTimeData(RealTimeDataVehicle::class.java) |
| | | //// Thread.sleep(500) |
| | | // sourceTraceController.addOneData(rtdVehicle) |
| | | // } |
| | | // sourceTraceController.initTask() |
| | | // } |
| | | // val rtData = realTimeDataService.getSecondData( |
| | | // "0a", |
| | | // "0a0000000001", |
| | | // "2024-08-23 12:30:23", |
| | | // "2024-08-23 15:12:56", |
| | | // null, |
| | | // 1, |
| | | // 10000 |
| | | // ) |
| | | // rtData.data?.forEach { d -> |
| | | // val rtdVehicle = d.toBaseRealTimeData(RealTimeDataVehicle::class.java) |
| | | //// Thread.sleep(500) |
| | | // sourceTraceController.addOneData(rtdVehicle) |
| | | // } |
| | | |
| | | } |
| | | |
| | | @Test |
| | | fun deleteSourceTrace() { |
| | | // val mCode = listOf( |
| | | // "SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)", |
| | | // "SH-CN-20240830(04)", "SH-CN-20240823", |
| | | // "SH-CN-20240723(02)", |
| | | //// "SH-CN-20250723(01)" |
| | | // ) |
| | | // val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | // val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val startTime = LocalDateTime.of(2025, 11, 2, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = LocalDateTime.of(2025, 11, 2, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val startTime = LocalDateTime.of(2025, 12, 11, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant() |
| | | val endTime = LocalDateTime.of(2025, 12, 11, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant() |
| | | val missions = missionMapper.selectByExample(Example(Mission::class.java).apply { |
| | | createCriteria().andBetween("startTime", startTime, endTime) |
| | | }) |