Browse Source

交通数据统计分析

develop
xiepufeng 1 year ago
parent
commit
03e5b9a26c
  1. 14
      zc-business/src/main/java/com/zc/business/constant/RedisKeyConstants.java
  2. 24
      zc-business/src/main/java/com/zc/business/constant/StakeMarkConstant.java
  3. 60
      zc-business/src/main/java/com/zc/business/controller/DcTrafficStatisticsController.java
  4. 5
      zc-business/src/main/java/com/zc/business/controller/VideoController.java
  5. 20
      zc-business/src/main/java/com/zc/business/domain/DcDevice.java
  6. 59
      zc-business/src/main/java/com/zc/business/domain/DcTrafficMetricsData.java
  7. 34
      zc-business/src/main/java/com/zc/business/domain/DcTrafficVolumeForecast.java
  8. 8
      zc-business/src/main/java/com/zc/business/enums/CameraDirectionEnum.java
  9. 98
      zc-business/src/main/java/com/zc/business/enums/ChannelCongestionLevelEnum.java
  10. 4
      zc-business/src/main/java/com/zc/business/enums/DeviceDataCategoryEnum.java
  11. 4
      zc-business/src/main/java/com/zc/business/enums/LaneDirection.java
  12. 48
      zc-business/src/main/java/com/zc/business/enums/VehicleLoadSaturationFactorEnum.java
  13. 18
      zc-business/src/main/java/com/zc/business/mapper/DcTrafficVolumeForecastMapper.java
  14. 44
      zc-business/src/main/java/com/zc/business/request/DcTrafficMetricsDataRequest.java
  15. 23
      zc-business/src/main/java/com/zc/business/service/DcTrafficStatisticsService.java
  16. 7
      zc-business/src/main/java/com/zc/business/service/DcTrafficVolumeForecastService.java
  17. 505
      zc-business/src/main/java/com/zc/business/service/impl/DcTrafficStatisticsServiceImpl.java
  18. 19
      zc-business/src/main/java/com/zc/business/service/impl/DcTrafficVolumeForecastServiceImpl.java
  19. 79
      zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java
  20. 610
      zc-business/src/main/java/com/zc/business/statistics/handler/TrafficAnalysis.java
  21. 495
      zc-business/src/main/java/com/zc/business/statistics/handler/TrafficStatistics.java
  22. 54
      zc-business/src/main/java/com/zc/business/utils/AlgorithmUtils.java
  23. 37
      zc-business/src/main/java/com/zc/business/utils/StakeMarkUtils.java

14
zc-business/src/main/java/com/zc/business/constant/RedisKeyConstants.java

@ -15,4 +15,18 @@ public class RedisKeyConstants
* 路段
*/
public static final String DC_ROAD_SECTION = "dc:roadSection";
/**
* 设备实时交通统计
*/
private static final String DC_DEVICES_TRAFFIC_STATISTICS = "dc:devicesTrafficStatistics";
/**
* 获取实时交通统计key
* @param direction 方向
* @return key
*/
public static String getDcDevicesTrafficStatisticsKey(Byte direction) {
return DC_DEVICES_TRAFFIC_STATISTICS + ":" + direction;
}
}

24
zc-business/src/main/java/com/zc/business/constant/StakeMarkConstant.java

@ -0,0 +1,24 @@
package com.zc.business.constant;
/**
* StakeMarkConstant类用于定义一些桩号常量
* @author xiepufeng
*/
public class StakeMarkConstant {
// 定义最大桩号常量
public static final int MAX_STAKE_MARK = 208979;
// 定义最小桩号常量
public static final int MIN_STAKE_MARK = 54394;
/**
* 计算道路长度
* @return 道路长度单位
*/
public static int calculateRoadLength() {
return MAX_STAKE_MARK - MIN_STAKE_MARK;
}
}

60
zc-business/src/main/java/com/zc/business/controller/DcTrafficStatisticsController.java

@ -0,0 +1,60 @@
package com.zc.business.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.zc.business.domain.DcTrafficMetricsData;
import com.zc.business.request.DcTrafficMetricsDataRequest;
import com.zc.business.service.DcTrafficStatisticsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 交通数据统计
*
* @author xiepufeng
*/
@Api(tags = "交通数据统计")
@RestController
@RequestMapping("/business/traffic-statistics")
public class DcTrafficStatisticsController {
@Autowired
private DcTrafficStatisticsService dcTrafficStatisticsService;
/**
* 获取当前交通特征指数
*
* @param request 请求参数封装了获取交通指标所需的数据和条件
* @return 返回当前交通特征指数的数据结果使用AjaxResult包装
*/
@ApiOperation("获取当前交通特征指数")
@GetMapping("/current/metrics")
public AjaxResult currentTrafficMetrics(DcTrafficMetricsDataRequest request){
// 调用服务层方法,获取当前交通指标数据
DcTrafficMetricsData dcTrafficMetricsData = dcTrafficStatisticsService.currentTrafficMetrics(request);
// 将获取到的交通指标数据封装为成功的结果并返回
return AjaxResult.success(dcTrafficMetricsData);
}
/**
* 获取历史交通特征指数
*
* @param request 请求参数包含需要查询的历史交通特征指数的详细信息
* @return 返回一个AjaxResult对象其中包含了查询结果的成功状态和历史交通特征指数数据列表
*/
@ApiOperation("获取历史交通特征指数")
@GetMapping("/history/metrics")
public AjaxResult historyTrafficMetrics(DcTrafficMetricsDataRequest request){
// 调用服务层方法,查询历史交通特征指数数据
List<DcTrafficMetricsData> dcTrafficMetricsDataList = dcTrafficStatisticsService.historyTrafficMetrics(request);
// 将查询结果封装成成功响应并返回
return AjaxResult.success(dcTrafficMetricsDataList);
}
}

5
zc-business/src/main/java/com/zc/business/controller/VideoController.java

@ -9,7 +9,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.zc.business.constant.DeviceTypeConstants;
import com.zc.business.domain.DcDevice;
import com.zc.business.enums.CameraDirection;
import com.zc.business.enums.CameraDirectionEnum;
import com.zc.business.enums.LaneDirection;
import com.zc.business.service.IDcDeviceService;
import com.zc.business.service.IMiddleDatabaseService;
@ -26,7 +26,6 @@ import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
@ -121,7 +120,7 @@ public class VideoController extends BaseController {
String pileNum = cameraInfo.getString("pileNum");
// 方向
Integer camOrientation = cameraInfo.getInteger("camOrientation");
LaneDirection laneDirection = CameraDirection.fromCode(camOrientation).toLaneDirection();
LaneDirection laneDirection = CameraDirectionEnum.fromCode(camOrientation).toLaneDirection();
// 是否有云台控制 0 有(球机) 1 ⽆(枪机)
String ptzCtrl = cameraInfo.getString("ptzCtrl");

20
zc-business/src/main/java/com/zc/business/domain/DcDevice.java

@ -3,6 +3,7 @@ package com.zc.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.zc.business.utils.StakeMarkUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -81,23 +82,6 @@ public class DcDevice {
private String manufacturer;
public Integer stakeMarkToInt() {
if (stakeMark == null) {
return null;
}
// 不区分大小写的正则表达式匹配 'k' 和 '+'
String[] parts = this.stakeMark.split("(?i)k|\\+");
// 提取出公里数部分和米数部分
String kmStr = parts[1].trim();
// 将公里数和米数转换为整数
int km = Integer.parseInt(kmStr);
int m = 0;
if (parts.length == 3) {
String mStr = parts[2].trim();
m = Integer.parseInt(mStr);
}
// 计算总米数
return km * 1000 + m;
return StakeMarkUtils.stakeMarkToInt(stakeMark);
}
}

59
zc-business/src/main/java/com/zc/business/domain/DcTrafficMetricsData.java

@ -0,0 +1,59 @@
package com.zc.business.domain;
import lombok.Data;
import java.util.Date;
/**
* 交通特征指数定义
* @author xiepufeng
*/
@Data
public class DcTrafficMetricsData {
/**
* 所属辖区路段
*/
private Long roadSectionId;
/**
* 交通组成特征指数
*/
private Integer trafficFeature;
/**
* 通道拥挤度
*/
private Integer channelCongestionLevel;
/**
* 路网拥挤度
*/
private Integer roadNetworkCongestionLevel;
/**
* 饱和度
*/
private Integer saturationLevel;
/**
* 统计时间
*/
private Date statisticalDate;
/**
* 道路方向
*/
private Byte direction;
/**
* 时段类型
* 1- 2- 3- 4-
*/
private Byte periodType;
/**
* 交通量
*/
private Integer trafficVolume;
}

34
zc-business/src/main/java/com/zc/business/domain/DcTrafficVolumeForecast.java

@ -0,0 +1,34 @@
package com.zc.business.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
/**
* 路段设计交通量预测数据定义
* @author xiepufeng
*/
@Data
public class DcTrafficVolumeForecast {
/**
* 主键
*/
@TableId
private Long id;
/**
* 所属路段
*/
private Long roadSectionId;
/**
* 年份
*/
private Integer year;
/**
* 设计交通量
*/
private Integer designTrafficVolume;
}

8
zc-business/src/main/java/com/zc/business/enums/CameraDirection.java → zc-business/src/main/java/com/zc/business/enums/CameraDirectionEnum.java

@ -3,7 +3,7 @@ package com.zc.business.enums;
/**
* 摄像头方向
*/
public enum CameraDirection {
public enum CameraDirectionEnum {
/**
* 摄像头上行
*/
@ -20,7 +20,7 @@ public enum CameraDirection {
private final int code;
private final String description;
CameraDirection(int code, String description) {
CameraDirectionEnum(int code, String description) {
this.code = code;
this.description = description;
}
@ -34,8 +34,8 @@ public enum CameraDirection {
}
// 可选:根据code获取枚举值
public static CameraDirection fromCode(int code) {
for (CameraDirection direction : values()) {
public static CameraDirectionEnum fromCode(int code) {
for (CameraDirectionEnum direction : values()) {
if (direction.getCode() == code) {
return direction;
}

98
zc-business/src/main/java/com/zc/business/enums/ChannelCongestionLevelEnum.java

@ -0,0 +1,98 @@
package com.zc.business.enums;
import lombok.Getter;
/**
* 通道拥挤度等级枚举
* @author xiepufeng
*/
@Getter
public enum ChannelCongestionLevelEnum {
/**
* 表示通道畅通速度阈值为70 km/h
*/
FLOWING(70, 0, "畅通"),
/**
* 表示通道基本畅通速度阈值为50 km/h
*/
BASIC_FLOWING(50, 0, "基本畅通"),
/**
* 表示通道轻度拥堵速度阈值为40 km/h
*/
LIGHT_CONGESTION(40, 1, "轻度拥堵"),
/**
* 表示通道中度拥堵速度阈值为20 km/h
*/
MEDIUM_CONGESTION(20, 2, "中度拥堵"),
/**
* 使用负数作为默认值表示无限小始终小于其他速度阈值表示通道严重拥堵
*/
SEVERE_CONGESTION(-1, 4, "严重拥堵");
/**
* 速度阈值用于判断通道拥挤程度
*/
private final int speedThreshold;
/**
* 默认的拥堵里程数
*/
private final int defaultCongestionDistance;
/**
* 对拥挤度等级的描述
*/
private final String description;
/**
* 构造函数初始化通道拥挤度等级
*
* @param speedThreshold 速度阈值
* @param description 等级描述
*/
ChannelCongestionLevelEnum(int speedThreshold, int defaultCongestionMiles, String description) {
this.speedThreshold = speedThreshold;
this.defaultCongestionDistance = defaultCongestionMiles;
this.description = description;
}
/**
* 根据给定速度返回对应的通道拥挤度等级
*
* @param speed 速度单位km/h
* @return 对应的通道拥挤度等级
*/
public static ChannelCongestionLevelEnum fromSpeed(int speed) {
for (ChannelCongestionLevelEnum level : values()) {
if (speed > level.speedThreshold) {
return level;
}
}
return SEVERE_CONGESTION; // 若速度小于等于所有等级阈值,则视为严重拥堵
}
/**
* 判断速度是否是是中度拥堵或者严重拥堵
*/
public static boolean isMediumOrSevereCongestion(int speed) {
ChannelCongestionLevelEnum level = fromSpeed(speed);
return level == MEDIUM_CONGESTION || level == SEVERE_CONGESTION;
}
/**
* 判断给定速度是否属于指定的拥挤度等级
*
* @param speed 速度单位km/h
* @param level 拥挤度等级
* @return 如果速度属于该等级返回true否则返回false
*/
public static boolean isWithinLevel(int speed, ChannelCongestionLevelEnum level) {
ChannelCongestionLevelEnum currentLevel = fromSpeed(speed);
return currentLevel == level;
}
}

4
zc-business/src/main/java/com/zc/business/enums/DeviceDataCategory.java → zc-business/src/main/java/com/zc/business/enums/DeviceDataCategoryEnum.java

@ -6,13 +6,13 @@ import lombok.Getter;
* @author xiepufeng
*/
@Getter
public enum DeviceDataCategory {
public enum DeviceDataCategoryEnum {
REAL_TIME("实时数据"),
HISTORY("历史数据");
private final String description;
DeviceDataCategory(String description) {
DeviceDataCategoryEnum(String description) {
this.description = description;
}

4
zc-business/src/main/java/com/zc/business/enums/LaneDirection.java

@ -22,13 +22,13 @@ public enum LaneDirection {
this.description = description;
}
public static LaneDirection fromValue(int value) {
for (LaneDirection direction : values()) {
if (direction.getValue() == value) {
return direction;
}
}
throw new IllegalArgumentException("Invalid value for LaneDirection: " + value);
throw new IllegalArgumentException("无效的LaneDirection值: " + value);
}
}

48
zc-business/src/main/java/com/zc/business/enums/VehicleLoadSaturationFactorEnum.java

@ -0,0 +1,48 @@
package com.zc.business.enums;
import lombok.Getter;
/**
* 饱和度计算车辆折算系数
* @author xiepufeng
*/
@Getter
public enum VehicleLoadSaturationFactorEnum {
/**
* 小客车车辆折算系数1.0座位19座的客车和载质量2t的货车
*/
PASSENGER_CAR(1.0, "小客车,座位≤19座的客车和载质量≤2t的货车"),
/**
* 中型车车辆折算系数1.5座位>19座的客车和2t<载质量7t的货车
*/
MEDIUM_VEHICLE(1.5, "中型车,座位>19座的客车和2t<载质量≤7t的货车"),
/**
* 大型车车辆折算系数2.57t<载质量20t的货车
*/
LARGE_VEHICLE(2.5, "大型车,7t<载质量≤20t的货车"),
/**
* 汽车列车车辆折算系数4.020t<载质量的货车
*/
TRUCK_TRAINS(4.0, "汽车列车,20t<载质量的货车");
private final double conversionFactor;
private final String description;
VehicleLoadSaturationFactorEnum(double conversionFactor, String description) {
this.conversionFactor = conversionFactor;
this.description = description;
}
// 如果需要根据车辆折算系数查找枚举项
public static VehicleLoadSaturationFactorEnum findByConversionFactor(double conversionFactor) {
for (VehicleLoadSaturationFactorEnum type : values()) {
if (type.getConversionFactor() == conversionFactor) {
return type;
}
}
throw new IllegalArgumentException("未知的饱和度计算车辆折算系数:" + conversionFactor);
}
}

18
zc-business/src/main/java/com/zc/business/mapper/DcTrafficVolumeForecastMapper.java

@ -0,0 +1,18 @@
package com.zc.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.domain.DcTrafficVolumeForecast;
import org.apache.ibatis.annotations.Mapper;
import java.util.Date;
/**
* 路段设计日最大交通量预测数据Mapper接口
*
* @author xiepufeng
*/
@Mapper
public interface DcTrafficVolumeForecastMapper extends BaseMapper<DcTrafficVolumeForecast> {
}

44
zc-business/src/main/java/com/zc/business/request/DcTrafficMetricsDataRequest.java

@ -0,0 +1,44 @@
package com.zc.business.request;
import lombok.Data;
import java.util.Date;
/**
* 交通特征指数请求参数定义
* @author xiepufeng
*/
@Data
public class DcTrafficMetricsDataRequest {
/**
* 所属辖区路段
*/
private Long roadSectionId;
/**
* 道路方向
*/
private Byte direction;
/**
* 时段类型
* 1- 2- 3- 4-
*/
private Byte periodType;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 是否分路段统计
*/
private boolean segmented;
}

23
zc-business/src/main/java/com/zc/business/service/DcTrafficStatisticsService.java

@ -4,6 +4,9 @@ import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zc.business.domain.DcTrafficMetricsData;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.request.DcTrafficMetricsDataRequest;
import java.util.List;
public interface DcTrafficStatisticsService extends IService<DcTrafficSectionData> {
@ -14,12 +17,22 @@ public interface DcTrafficStatisticsService extends IService<DcTrafficSectionDat
*/
void processRealtimeOneStopMessage(JSONObject msg);
/**
* 根据提供的请求参数获取当前的流量指标数据
*
* @param request 包含获取流量指标所需的所有请求参数的对象
* @return DcTrafficMetricsData 返回一个包含当前流量指标数据的对象
* 该对象包含了关于数据中心网络流量的各种度量指标
*/
DcTrafficMetricsData currentTrafficMetrics(DcTrafficMetricsDataRequest request);
/**
* 获取当前交通特征指数
* 获取历史流量指标数据列表
*
* @param direction 交通方向例如1代表菏泽方向3济南方向
* @param roadSectionId 路段ID
* @return 当前交通特征指数
* @param request 包含获取流量指标所需的所有请求参数的对象
* @return 返回符合查询条件的历史流量指标数据列表
*/
DcTrafficMetricsData currentTrafficMetrics(Byte direction, Long roadSectionId);
List<DcTrafficMetricsData> historyTrafficMetrics(DcTrafficMetricsDataRequest request);
}

7
zc-business/src/main/java/com/zc/business/service/DcTrafficVolumeForecastService.java

@ -0,0 +1,7 @@
package com.zc.business.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zc.business.domain.DcTrafficVolumeForecast;
public interface DcTrafficVolumeForecastService extends IService<DcTrafficVolumeForecast> {
}

505
zc-business/src/main/java/com/zc/business/service/impl/DcTrafficStatisticsServiceImpl.java

@ -6,28 +6,30 @@ import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.zc.business.constant.RedisKeyConstants;
import com.zc.business.constant.StatisticalRecoveryOffsetTime;
import com.zc.business.controller.DcDeviceController;
import com.zc.business.domain.DcDevice;
import com.zc.business.domain.DcRoadSection;
import com.zc.business.domain.DcTrafficMetricsData;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.*;
import com.zc.business.request.DcTrafficMetricsDataRequest;
import com.zc.business.statistics.cache.*;
import com.zc.business.mapper.DcTrafficSectionDataMapper;
import com.zc.business.service.DcTrafficStatisticsService;
import com.zc.business.statistics.handler.RealtimeTrafficStatistics;
import com.zc.business.statistics.handler.TrafficAnalysis;
import com.zc.business.statistics.handler.TrafficStatistics;
import com.zc.business.utils.StakeMarkUtils;
import com.zc.common.core.httpclient.exception.HttpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* 交通断面数据服务实现类负责处理实时设备消息缓存数据定时任务以及数据保存等功能
@ -42,15 +44,18 @@ public class DcTrafficStatisticsServiceImpl
// 日志记录器
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private DcTrafficSectionDataMapper dcTrafficSectionDataMapper;
@Resource
private RedisCache redisCache;
@Resource
private DcDeviceController dcDeviceController;
@Resource
private TrafficStatistics trafficStatistics;
@Resource
private TrafficAnalysis trafficAnalysis;
/**
* 初始化方法用于在对象创建后恢复各种周期的交通数据缓存
* 该方法标注了@PostConstruct注解确保在依赖注入完成后调用
@ -72,52 +77,85 @@ public class DcTrafficStatisticsServiceImpl
public void processRealtimeOneStopMessage(JSONObject msg) {
// 1. 将设备消息转换为交通断面数据统计定义对象
List<DcTrafficSectionData> dcTrafficSectionDataList = convertToTrafficStatistics(msg, DeviceDataCategory.REAL_TIME);
List<DcTrafficSectionData> dcTrafficSectionDataList = trafficStatistics.convertToTrafficStatistics(msg, DeviceDataCategoryEnum.REAL_TIME);
if (dcTrafficSectionDataList != null && !dcTrafficSectionDataList.isEmpty()) {
// 2. 将转换后的数据添加到缓存中
dcTrafficSectionDataList.forEach(DailyTrafficStatisticsCache::addCacheData);
// 2. 添加到缓存中
dcTrafficSectionDataList.forEach(this::addCacheData);
}
}
/**
* 获取当前交通特征指数
* 根据请求获取当前交通流量指标数据
*
* @param direction 交通方向例如1代表菏泽方向3济南方向
* @param roadSectionId 路段ID
* @return 当前交通特征指数
* @param request 包含获取流量指标所需的所有请求参数的对象
* @return 返回当前交通流量指标数据
* @throws ServiceException 如果没有获取到交通数据则抛出异常
*/
@Override
public DcTrafficMetricsData currentTrafficMetrics(Byte direction, Long roadSectionId) {
public DcTrafficMetricsData currentTrafficMetrics(DcTrafficMetricsDataRequest request) {
// 从Redis缓存中获取指定方向的交通路段数据列表
List<DcTrafficSectionData> dcTrafficSectionDataCaches = getDcTrafficSectionDataRedisCache(request.getDirection());
DcTrafficMetricsData dcTrafficMetricsData = new DcTrafficMetricsData();
return dcTrafficMetricsData;
// 根据指定的方向和路段ID范围获取交通数据
List<DcTrafficSectionData> trafficSectionDataList = fetchTrafficDataByDirAndRange(request.getRoadSectionId(), dcTrafficSectionDataCaches);
// 对获取的交通路段数据进行分析,得到交通指标数据
List<DcTrafficMetricsData> dcTrafficMetricsDataList = trafficAnalysis.calculateTrafficMetrics(request, trafficSectionDataList);
if (dcTrafficMetricsDataList == null || dcTrafficMetricsDataList.isEmpty()) {
// 如果没有获取到数据,则抛出异常
throw new ServiceException("获取当前交通特征指数失败");
}
// 根据收集到的交通段数据计算并返回交通指标数据
return dcTrafficMetricsDataList.get(0);
}
/**
* 定义每小时第20分钟执行的任务用于清除过期缓存数据并将缓存中的数据整合后保存至数据库
* 计算获取指定条件下的历史交通流量指标数据
*
* @param request 包含查询条件的请求对象包括开始时间结束时间方向周期类型和所属路段ID等信息
* @return 返回一个包含交通流量指标数据的列表
*/
@Scheduled(cron = "0 20 * * * ?") // 每小时的20分整点执行该任务
public void performHourlyCleanupAndPersist() {
// 清除已过期的缓存数据
DailyTrafficStatisticsCache.clearExpiredData();
MonthlyTrafficStatisticsCache.clearExpiredData();
QuarterlyTrafficStatisticsCache.clearExpiredData();
YearlyTrafficStatisticsCache.clearExpiredData();
// 整合缓存数据并保存至数据库
// 将缓存中的数据按日统计后保存至数据库
// 添加月交通断面数据到缓存中
persistAggregatedData(DailyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.DAY, MonthlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按月统计后保存至数据库
// 添加季度交通断面数据到缓存中
persistAggregatedData(MonthlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.MONTH, QuarterlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按季度统计后保存至数据库
// 添加年交通断面数据到缓存中
persistAggregatedData(QuarterlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.QUARTER, YearlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按年统计后保存至数据库
persistAggregatedData(YearlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.YEAR, (a) -> {});
@Override
public List<DcTrafficMetricsData> historyTrafficMetrics(DcTrafficMetricsDataRequest request) {
if (request.getStartTime() == null || request.getEndTime() == null) {
throw new ServiceException("开始时间或结束时间不能为空");
}
if (request.getPeriodType() == null) {
throw new ServiceException("时段类型不能为空");
}
// 构建查询条件
LambdaQueryWrapper<DcTrafficSectionData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.between(DcTrafficSectionData::getStatisticalDate, request.getStartTime(), request.getEndTime());
queryWrapper.eq(DcTrafficSectionData::getPeriodType, request.getPeriodType());
queryWrapper.eq(DcTrafficSectionData::getDirection, request.getDirection());
// 根据请求获取所属路段ID,并进一步筛选路段范围内的数据
Long roadSectionId = request.getRoadSectionId();
if (roadSectionId != null) {
// 从缓存中获取路段信息,并根据路段的起止桩号筛选数据
DcRoadSection dcRoadSection = redisCache.getCacheMapValue(RedisKeyConstants.DC_ROAD_SECTION, roadSectionId);
if (dcRoadSection != null) {
queryWrapper.between(
DcTrafficSectionData::getStakeMark,
StakeMarkUtils.stakeMarkToInt(dcRoadSection.getStartStakeMark()),
StakeMarkUtils.stakeMarkToInt(dcRoadSection.getEndStakeMark()));
}
}
// 根据收集到的交通段数据计算并返回交通指标数据
return trafficAnalysis.calculateTrafficMetrics(request, list(queryWrapper));
}
/**
* 恢复每日缓存的函数
* 该方法尝试从物联平台获取所有设备信息并对这些信息进行处理
@ -137,7 +175,7 @@ public class DcTrafficStatisticsServiceImpl
// 将获取的设备信息转换为JSON数组,并遍历处理每个设备的数据
JSONArray deviceJsonArray = JSONArray.parseArray(oneStopDeviceMap.get("data").toString());
deviceJsonArray.forEach(this::processDeviceData);
deviceJsonArray.forEach(trafficStatistics::processDeviceData);
} catch (HttpException | IOException e) {
// 记录处理设备数据时发生的异常
@ -145,111 +183,6 @@ public class DcTrafficStatisticsServiceImpl
}
}
/**
* 处理设备数据的函数
* 该方法首先将传入的设备对象转换为JSON对象然后从JSON对象中提取设备ID
* 如果设备ID有效则查询设备的实时流量数据并对获取的设备属性数据进行处理
*
* @param deviceObject 设备对象需要是一个JSONObject包含设备信息
*/
private void processDeviceData(Object deviceObject) {
JSONObject deviceJsonObject = (JSONObject)deviceObject;
// 提取设备ID
String deviceId = deviceJsonObject.getString("id");
// 检查设备ID的有效性
if (deviceId == null || deviceId.isEmpty()) {
logger.error("获取一类交通量调查站设备id失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 查询并处理设备属性数据
try {
HashMap<String, Object> props = buildPropertiesRequiredParameter();
// 查询设备的实时属性数据
Map<String, Object> deviceProperties = dcDeviceController.queryDeviceProperties(deviceId, IotProductPropertiesEnum.ONE_STOP_PRODUCT_01.getNumber(), props);
// 对获取的设备属性数据进行进一步处理
processDeviceProperties(deviceProperties);
} catch (HttpException | IOException e) {
// 记录处理设备属性数据时发生的异常
logger.error("处理设备属性数据时发生异常,设备id:{}", deviceId, e);
}
}
/**
* 构建必需的属性请求参数集合
* 该方法不接受任何参数返回一个包含特定键值对的HashMap对象
* 键值对表达了查询条件其中键是查询条件的结构化字符串表示值是条件的具体值
*
* @return HashMap<String, Object> 包含查询条件的HashMap对象
*/
private HashMap<String, Object> buildPropertiesRequiredParameter() {
// 获取数据统计的最大日期
Date maxStatisticalDate = dcTrafficSectionDataMapper.getMaxStatisticalDate();
// 如果最大统计日期为null,则计算默认的最大统计日期
if (maxStatisticalDate == null) {
maxStatisticalDate = DateUtil.offsetDay(new Date(), StatisticalRecoveryOffsetTime.TRAFFIC_SECTION_DATA_OFFSET_DAY);
}
HashMap<String, Object> props = new HashMap<>();
// 设置查询条件的键为“timestamp$BTW”,表示时间戳在一定范围内
props.put("terms[0].column", "timestamp$BTW");
ArrayList<String> dateList = new ArrayList<>();
// 添加当前日期的开始和结束时间到列表,用于设定时间范围
dateList.add(DateUtil.beginOfDay(maxStatisticalDate).toString());
dateList.add(DateUtil.endOfDay(new Date()).toString());
// 将日期列表以逗号分隔并设置为查询条件的值
props.put("terms[0].value", String.join(",", dateList));
props.put("paging", false);
return props;
}
/**
* 处理设备属性信息
* 该方法接收一个设备属性的映射从中提取属性数据并将其转换为交通断面数据统计定义对象列表然后将这些数据添加到缓存中
*
* @param deviceProperties 包含设备属性数据的映射该映射应包含一个名为"data"的键其值是一个包含具体属性信息的JSON对象
*/
private void processDeviceProperties(Map<String, Object> deviceProperties) {
// 检查传入的设备属性映射是否为空,或者其中的"data"键对应的值是否为空
if (deviceProperties == null || deviceProperties.get("data") == null) {
logger.error("获取一类交通量调查站属性数据失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 从设备属性映射的"data"值中解析出JSON对象
JSONObject propertiesObject = JSONObject.parseObject(deviceProperties.get("data").toString());
// 检查解析出的JSON对象是否包含"data"键
if (propertiesObject.get("data") == null) {
logger.error("获取一类交通量调查站属性数据失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 从解析出的JSON对象中获取设备属性数据列表
JSONArray properties = JSONArray.parseArray(propertiesObject.get("data").toString());
// 遍历设备属性数据列表,对每个属性对象进行处理
properties.forEach(propertyObject -> {
JSONObject propertyJsonObject = (JSONObject)propertyObject;
// 将设备消息转换为交通断面数据统计定义对象
List<DcTrafficSectionData> dcTrafficSectionDataList = convertToTrafficStatistics(propertyJsonObject, DeviceDataCategory.HISTORY);
// 如果转换结果非空且不为空列表,则将转换后的数据添加到缓存中
if (dcTrafficSectionDataList != null && !dcTrafficSectionDataList.isEmpty()) {
// 将转换后的数据添加到缓存中
dcTrafficSectionDataList.forEach(DailyTrafficStatisticsCache::addCacheData);
}
});
}
/**
* 恢复每月交通数据缓存的方法
* 通过查询当前月份至今的每日交通数据并将其添加到每月交通统计缓存中
@ -292,254 +225,100 @@ public class DcTrafficStatisticsServiceImpl
dcTrafficSectionDataList.forEach(YearlyTrafficStatisticsCache::addCacheData);
}
/**
* 将设备消息转换为交通断面数据统计定义对象
* 将交通数据添加到缓存中
* 该方法将交通数据既添加到日交通数据缓存中也实时缓存到Redis中
*
* @param msg JSON格式的设备实时消息
* @param category 设备数据类型
* @return 转换后的交通断面数据统计定义对象
* @param dcTrafficSectionData 交通段数据对象包含交通数据的详细信息
*/
public List<DcTrafficSectionData> convertToTrafficStatistics(JSONObject msg, DeviceDataCategory category) {
// 获取属性
JSONObject property;
if (category == DeviceDataCategory.REAL_TIME) {
JSONObject properties = msg.getJSONObject("properties");
// 属性数据
if (properties == null) {
logger.error("接收实时属性数据,属性数据不存在,请检查物联网一类交通量调查站设备数据是否正常");
return null;
}
// 获取属性01的数据
property = properties.getJSONObject(IotProductPropertiesEnum.ONE_STOP_PRODUCT_01.getNumber());
} else {
property = msg.getJSONObject("value");
}
// 判断是否属性01的数据
if (property == null) {
logger.error("非属性01的数据,无法处理,请检查物联网一类交通量调查站设备数据是否正常");
return null;
}
// 物联设备id
String iotDeviceId = msg.getString("deviceId");
if (iotDeviceId == null || iotDeviceId.isEmpty()) {
logger.error("设备id为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 上报时间(时间戳)
Long timestamp = msg.getLong("timestamp");
if (timestamp == null || timestamp == 0L) {
logger.error("上报时间为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 从缓存中获取设备信息
DcDevice dcDevice = redisCache.getCacheMapValue(RedisKeyConstants.DC_DEVICES, iotDeviceId);
if (dcDevice == null) {
logger.error("设备信息不存在,请检查是否配置设备信息,物联平台设备id:{}无对应设备信息", iotDeviceId);
return null;
}
// 上报时间
Date reportTime = DateUtil.date(timestamp);
// 车道信息
JSONArray lanes = property.getJSONArray("lanes");
if (lanes == null || lanes.isEmpty()) {
logger.error("车道信息为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 分别处理上行和下行数据
return new ArrayList<>(processLaneData(lanes, dcDevice, reportTime).values());
private void addCacheData(DcTrafficSectionData dcTrafficSectionData) {
// 添加到日交通数据缓存中
DailyTrafficStatisticsCache.addCacheData(dcTrafficSectionData);
// 将数据缓存到redis中
redisCache.setCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(dcTrafficSectionData.getDirection()),
dcTrafficSectionData.getDeviceId(),
dcTrafficSectionData);
}
/**
* 处理车道数据计算上行和下行的车流量和累计平均速度
* 从Redis缓存中获取指定方向的交通路段数据列表
*
* @param lanes 车道数据的JSON数组包含各个车道的信息
* @param dcDevice 设备信息用于标识数据来源
* @param reportTime 报告时间标识数据的时间戳
* @return 返回一个Map包含上行和下行的交通段数据
* @param direction 交通方向如果为null则获取双向数据
* @return DcTrafficSectionData列表确保列表非空
*/
private Map<LaneDirection, DcTrafficSectionData> processLaneData(JSONArray lanes, DcDevice dcDevice, Date reportTime) {
Map<LaneDirection, DcTrafficSectionData> resultMap = new HashMap<>();
// 初始化上行和下行的交通段数据
DcTrafficSectionData upwardData = new DcTrafficSectionData();
DcTrafficSectionData downwardData = new DcTrafficSectionData();
initializeTrafficSectionData(upwardData, dcDevice, reportTime, LaneDirection.UPWARD);
initializeTrafficSectionData(downwardData, dcDevice, reportTime, LaneDirection.DOWNWARD);
// 初始化上行和下行的车流量
int upwardTrafficVolume = 0, downwardTrafficVolume = 0;
// 初始化上行和下行的累计大车流量
int upwardLargeTrafficVolume = 0, downwardLargeTrafficVolume = 0;
// 初始化上行和下行的累计平均速度
double upwardCumulativeAverageSpeed = 0.0, downwardCumulativeAverageSpeed = 0.0;
// 遍历车道信息,计算上行和下行的车流量和累计平均速度
for (Object lane : lanes) {
// 解析车道数据
JSONObject laneData = (JSONObject) lane;
// 获取车道号,并判断是否为下行车道
Integer laneNumber = laneData.getInteger("laneNumber");
boolean isDownward = laneNumber >= 31 || laneNumber == 3;
// 根据车道方向累加车流量和累计平均速度
int totalTrafficFlow = laneData.getInteger("totalTrafficFlow");
int cumulativeAverageSpeed = calculateCumulativeAverageSpeed(laneData);
int cumulativeLargeTrafficVolume = calculateCumulativeLargeTrafficVolume(laneData);
if (isDownward) {
downwardTrafficVolume += totalTrafficFlow;
downwardCumulativeAverageSpeed += cumulativeAverageSpeed;
downwardLargeTrafficVolume += cumulativeLargeTrafficVolume;
} else {
upwardTrafficVolume += totalTrafficFlow;
upwardCumulativeAverageSpeed += cumulativeAverageSpeed;
upwardLargeTrafficVolume += cumulativeLargeTrafficVolume;
}
}
// 设置上行和下行的车流量和累计平均速度
setTrafficSectionData(upwardData, upwardTrafficVolume, upwardCumulativeAverageSpeed, upwardLargeTrafficVolume);
setTrafficSectionData(downwardData, downwardTrafficVolume, downwardCumulativeAverageSpeed, downwardLargeTrafficVolume);
public List<DcTrafficSectionData> getDcTrafficSectionDataRedisCache(Byte direction) {
// 将上行和下行的交通段数据放入结果映射中
resultMap.put(LaneDirection.UPWARD, upwardData);
resultMap.put(LaneDirection.DOWNWARD, downwardData);
Map<String, DcTrafficSectionData> dcTrafficSectionDataMap = new HashMap<>();
return resultMap;
}
// 根据方向选择相应的交通数据
Map<String, DcTrafficSectionData> trafficDataMap = (direction != null)
? getTrafficDataByDirection(direction)
: getTrafficDataForBothDirections();
// 将获取的交通数据合并到结果映射中
if (trafficDataMap != null) {
dcTrafficSectionDataMap.putAll(trafficDataMap);
}
/**
* 初始化交通段数据
* @param data 交通段数据对象用于存储交通数据
* @param dcDevice 设备对象包含设备的详细信息
* @param reportTime 数据上报时间
* @param direction 车道方向
*/
private void initializeTrafficSectionData(DcTrafficSectionData data, DcDevice dcDevice, Date reportTime, LaneDirection direction) {
// 设置设备id
data.setDeviceId(dcDevice.getId());
// 设置车道方向
data.setDirection(direction.getValue());
// 设置上报时间
data.setReportTime(reportTime);
// 设置数据上报时间
data.setStatisticalDate(reportTime);
// 设置统计时段类型为日
data.setPeriodType(TrafficDataPeriodTypeEnum.DAY);
// 将设备的桩号转换为整数后设置
data.setStakeMark(dcDevice.stakeMarkToInt());
// 将结果映射的值转换为列表并返回,确保列表非空
return new ArrayList<>(dcTrafficSectionDataMap.values());
}
/**
* 设置交通路段数据
* 该方法用于根据给定的交通量和累积平均速度设置交通路段的数据
* 根据方向获取交通流量数据
*
* @param data 交通路段数据对象用于存储交通路段的相关信息
* @param trafficVolume 该路段的交通量单位通常为车辆数
* @param cumulativeAverageSpeed 该路段的累积平均速度单位通常为千米/小时
* @param largeTrafficVolume 大型车交通量
* @param direction 方向
* @return 单向交通流量数据
*/
private void setTrafficSectionData(DcTrafficSectionData data, int trafficVolume, double cumulativeAverageSpeed, int largeTrafficVolume) {
data.setTrafficVolume(trafficVolume); // 设置交通量
data.setLargeTrafficVolume(largeTrafficVolume); // 设置大车流量
data.setAverageSpeed(0);
if (trafficVolume != 0) { // 当交通量不为0时,计算并设置平均速度
data.setAverageSpeed((int) Math.round(cumulativeAverageSpeed / trafficVolume)); // 平均速度 = 累积平均速度 / 交通量
}
private Map<String, DcTrafficSectionData> getTrafficDataByDirection(Byte direction) {
return redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(direction));
}
/**
* 计算给定车道数据的累计平均速度
* 该函数根据laneData中提供的各种车辆类型的数量和平均速度计算出累计的平均速度
* 获取上行和下行所有交通流量数据
*
* @param laneData 包含车道数据的JSONObject对象其中包含了各种车辆类型的数量和平均速度等数据
* @return 返回计算出的累计平均速度
* @return 所有交通流量数据
*/
private int calculateCumulativeAverageSpeed(JSONObject laneData) {
// 根据laneData中的数据计算累计平均速度
// 累加平均速度(中小客车)
return laneData.getInteger("trafficNumberOfInAndSmall") * laneData.getInteger("inAndSmallAverageVehicleSpeed") +
// 累加平均速度(小型货车)
laneData.getInteger("trafficVolumeOfSmallTrucks") * laneData.getInteger("smallTrucksAverageVehicleSpeed") +
// 累加平均速度(大客车)
laneData.getInteger("busTrafficVolume") * laneData.getInteger("averageSpeedOfBus") +
// 累加平均速度(中型货车)
laneData.getInteger("mediumTruckTrafficVolume") * laneData.getInteger("averageSpeedOfMediumSizeTrucks") +
// 累加平均速度(大型货车)
laneData.getInteger("largeTruckTrafficVolume") * laneData.getInteger("averageSpeedOfLargeTrucks") +
// 累加平均速度(特大型货车)
laneData.getInteger("extraLargeTrucksTrafficVolume") * laneData.getInteger("averageSpeedOfExtraLargeTrucks") +
// 累加平均速度(集装箱车)
laneData.getInteger("containerTruckTrafficVolume") * laneData.getInteger("averageSpeedOfContainerTruck") +
// 累加平均速度(拖拉机)
laneData.getInteger("tractorTrafficVolume") * laneData.getInteger("averageSpeedOfTractor") +
// 累加平均速度(摩托车)
laneData.getInteger("motorcycleTrafficVolume") * laneData.getInteger("averageSpeedOfMotorcycle");
private Map<String, DcTrafficSectionData> getTrafficDataForBothDirections() {
Map<String, DcTrafficSectionData> allData = new HashMap<>();
allData.putAll(redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(LaneDirection.UPWARD.getValue())));
allData.putAll(redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(LaneDirection.DOWNWARD.getValue())));
return allData;
}
/**
* 计算累积大型交通流量
* 该方法用于根据给定的车道数据计算出特定类型的车辆包括公共汽车中型货车大型货车特大型货车和集装箱车的交通量总和
* 根据指定的方向和路段ID范围获取交通数据
*
* @param laneData 包含车道交通量数据的JSONObject对象该对象应包含以下键
* - "busTrafficVolume"公共汽车交通量
* - "mediumTruckTrafficVolume"中型货车交通量
* - "largeTruckTrafficVolume"大型货车交通量
* - "extraLargeTrucksTrafficVolume"特大型货车交通量
* - "containerTruckTrafficVolume"集装箱车交通量
* @return 返回各种类型大型车辆交通量的总和类型为int
*/
private int calculateCumulativeLargeTrafficVolume(JSONObject laneData) {
// 计算各种类型大型车辆的交通量总和
return laneData.getInteger("busTrafficVolume") +
laneData.getInteger("mediumTruckTrafficVolume") +
laneData.getInteger("largeTruckTrafficVolume") +
laneData.getInteger("extraLargeTrucksTrafficVolume") +
laneData.getInteger("containerTruckTrafficVolume");
}
/**
* 将缓存中的数据统计后保存至数据库
* @param roadSectionId 指定的路段ID如果为null则不根据路段筛选数据
* @return 返回符合指定方向和路段范围的交通段数据列表
*/
public void persistAggregatedData(
Map<String, ? extends AbstractTrafficStatisticsCache> cache,
TrafficDataPeriodTypeEnum periodType,
Consumer<DcTrafficSectionData> consumer
) {
for (AbstractTrafficStatisticsCache data : cache.values()) {
// 如果数据已经存储过,则跳过此次处理
if (data.isStored()) {
continue;
}
DcTrafficSectionData aggregatedData = RealtimeTrafficStatistics.trafficStatistics(data.getData(), periodType);
if (dcTrafficSectionDataMapper.insertOrUpdate(aggregatedData)) {
// 设置数据已存储状态
data.setStored(true);
// 调用回调函数
consumer.accept(aggregatedData);
public List<DcTrafficSectionData> fetchTrafficDataByDirAndRange(Long roadSectionId, List<DcTrafficSectionData> dcTrafficSectionDataCaches) {
// 初始化路段起始和终止桩号
Integer startStakeMark;
Integer endStakeMark;
// 根据提供的路段ID查询路段信息,并转换为起始和终止桩号
if (roadSectionId != null) {
DcRoadSection dcRoadSection = redisCache.getCacheMapValue(RedisKeyConstants.DC_ROAD_SECTION, roadSectionId);
// 验证路段ID是否存在
if (dcRoadSection == null) {
throw new ServiceException("路段ID不存在");
}
startStakeMark = StakeMarkUtils.stakeMarkToInt(dcRoadSection.getStartStakeMark());
endStakeMark = StakeMarkUtils.stakeMarkToInt(dcRoadSection.getEndStakeMark());
} else {
startStakeMark = null;
endStakeMark = null;
}
// 筛选并返回符合路段范围条件的交通数据列表
return dcTrafficSectionDataCaches.stream()
.filter(data -> startStakeMark == null || endStakeMark == null
|| (data.getStakeMark() >= startStakeMark && data.getStakeMark() <= endStakeMark))
.collect(Collectors.toList());
}
}

19
zc-business/src/main/java/com/zc/business/service/impl/DcTrafficVolumeForecastServiceImpl.java

@ -0,0 +1,19 @@
package com.zc.business.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zc.business.domain.DcTrafficVolumeForecast;
import com.zc.business.mapper.DcTrafficVolumeForecastMapper;
import com.zc.business.service.DcTrafficVolumeForecastService;
import org.springframework.stereotype.Service;
/**
* 交通断面数据服务实现类负责处理实时设备消息缓存数据定时任务以及数据保存等功能
*
* @author xiepufeng
*/
@Service
public class DcTrafficVolumeForecastServiceImpl
extends ServiceImpl<DcTrafficVolumeForecastMapper, DcTrafficVolumeForecast>
implements DcTrafficVolumeForecastService {
}

79
zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java

@ -1,79 +0,0 @@
package com.zc.business.statistics.handler;
import com.ruoyi.common.utils.DateUtils;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
public class RealtimeTrafficStatistics {
/**
* 对给定的交通数据集合进行统计分析返回一个综合交通数据对象
*
* @param dataCollection 交通数据集合不可为null或空包含多个交通路段的详细数据
* @param trafficDataPeriodType 交通数据的时段类型例如小时周等
* @return 综合交通数据对象包含车流量总和平均车速等统计结果如果输入数据为空则返回null
*/
public static DcTrafficSectionData trafficStatistics(Collection<DcTrafficSectionData> dataCollection, TrafficDataPeriodTypeEnum trafficDataPeriodType) {
// 判断输入数据是否为空
if (CollectionUtils.isEmpty(dataCollection)) {
return null;
}
// 创建一个汇总统计用的对象
DcTrafficSectionData aggregatedData = new DcTrafficSectionData();
// 初始化车流量总和
int trafficVolume = 0;
// 初始化最大车流量
int largeTrafficVolume = 0;
// 初始化计算平均车速所需的分子部分
double numerator = 0;
// 遍历原始数据列表,累加车流量并计算平均车速
for (DcTrafficSectionData data: dataCollection) {
// 累加车流量
trafficVolume += data.getTrafficVolume();
// 累加最大车流量
largeTrafficVolume += data.getLargeTrafficVolume();
// 计算分子部分
numerator += data.getAverageSpeed() * data.getTrafficVolume();
}
// 使用第一个数据项的信息填充汇总统计对象的基本属性(设备ID、桩号、方向)
DcTrafficSectionData firstDcTrafficSectionData = dataCollection.iterator().next();
// 设备id
aggregatedData.setDeviceId(firstDcTrafficSectionData.getDeviceId());
// 桩号
aggregatedData.setStakeMark(firstDcTrafficSectionData.getStakeMark());
// 道路方向
aggregatedData.setDirection(firstDcTrafficSectionData.getDirection());
// 上报时间
aggregatedData.setReportTime(firstDcTrafficSectionData.getReportTime());
// 计算平均车速并设置到汇总统计对象中
if (trafficVolume != 0) {
aggregatedData.setAverageSpeed((int) Math.round(numerator / trafficVolume));
} else {
// 若车流量为0,则默认设置平均车速为0
aggregatedData.setAverageSpeed(0);
}
// 时段类型
aggregatedData.setPeriodType(trafficDataPeriodType);
// 设置统计时间
aggregatedData.setStatisticalDate(firstDcTrafficSectionData.getStatisticalDate(), trafficDataPeriodType);
// 车流量
aggregatedData.setTrafficVolume(trafficVolume);
// 大型车车流量
aggregatedData.setLargeTrafficVolume(largeTrafficVolume);
// 更新或插入操作
aggregatedData.setUpdateTime(DateUtils.getNowDate());
// 生成主键
aggregatedData.generateUniqueId();
return aggregatedData;
}
}

610
zc-business/src/main/java/com/zc/business/statistics/handler/TrafficAnalysis.java

@ -0,0 +1,610 @@
package com.zc.business.statistics.handler;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.redis.RedisCache;
import com.zc.business.constant.RedisKeyConstants;
import com.zc.business.constant.StakeMarkConstant;
import com.zc.business.domain.DcRoadSection;
import com.zc.business.domain.DcTrafficMetricsData;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.domain.DcTrafficVolumeForecast;
import com.zc.business.enums.*;
import com.zc.business.request.DcTrafficMetricsDataRequest;
import com.zc.business.service.DcTrafficVolumeForecastService;
import com.zc.business.utils.AlgorithmUtils;
import com.zc.business.utils.StakeMarkUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* 交通数据分析类
* 用于对交通数据进行各种分析处理
*
* @author xiepufeng
*/
@Component
public class TrafficAnalysis {
@Resource
private RedisCache redisCache;
@Resource
private DcTrafficVolumeForecastService dcTrafficVolumeForecastService;
// 使用ConcurrentHashMap来缓存路段ID和设计交通量,以提高并发性能和减少数据库访问
private static final Map<Long, HashMap<Integer, Integer>> designTrafficVolumeCache = new HashMap<>();
// 缓存路段列表
private static List<DcRoadSection> orderedDcRoadSectionCache;
/**
* 计算交通指标数据
* 根据提供的请求信息和路段数据列表计算相应的交通指标可以针对全程或指定路段进行统计统计结果受请求中的分段标志路段ID方向和周期类型影响
*
* @param request 包含交通指标计算请求信息的对象如是否分路段路段ID方向和周期类型
* @param trafficSectionDataList 包含多个路段数据的列表用于计算交通指标
* @return 返回一个交通指标数据列表每个列表项对应一个路段的交通指标数据
*/
public List<DcTrafficMetricsData> calculateTrafficMetrics(
DcTrafficMetricsDataRequest request,
List<DcTrafficSectionData> trafficSectionDataList) {
// 处理空列表的情况,如果列表为空,则直接返回一个空列表
if (trafficSectionDataList.isEmpty()) {
return Collections.emptyList();
}
// 根据请求信息决定如何计算交通指标
boolean segmented = request.isSegmented(); // 是否按分路段计算
Long roadSectionId = request.getRoadSectionId(); // 指定的路段ID
Byte direction = request.getDirection(); // 行驶方向
Byte periodType = request.getPeriodType(); // 周期类型
// 根据是否指定了路段ID,以及是否需要分路段统计来选择计算方法
if (roadSectionId == null) {
// 如果没有指定路段ID,则根据是否分路段来调用不同的计算方法
return segmented ? calculateFullSectionTrafficMetrics(trafficSectionDataList, direction, periodType) // 分路段统计
: calculateFullTrafficMetrics(trafficSectionDataList, direction, periodType); // 全程统计
} else {
// 如果指定了路段ID,则只统计该路段
return calculateSectionTrafficMetrics(trafficSectionDataList, roadSectionId, direction, periodType);
}
}
/**
* 计算整个路的路段的交通指标数据
*
* @param sectionDataList 路段交通数据列表包含各个监测点的交通数据
* @param direction 交通方向表示数据是朝哪个方向收集的
* @param periodType 时期类型标识数据是属于哪个时间段的
* @return 返回一个包含整个路段交通指标数据的列表
*/
private List<DcTrafficMetricsData> calculateFullSectionTrafficMetrics(
List<DcTrafficSectionData> sectionDataList,
Byte direction,
Byte periodType
) {
// 如果输入的路段交通数据列表为空或为空列表,直接返回空列表。
if (sectionDataList == null || sectionDataList.isEmpty()) {
return Collections.emptyList();
}
// 将输入的路段交通数据列表按照路段ID进行分组,得到一个Map,key为路段ID,value为该路段的交通数据列表。
Map<Long, List<DcTrafficSectionData>> sectionDataMap = groupTrafficSectionDataByRoadSectionId(sectionDataList);
// 统计每个路段的交通指标数据。
List<DcTrafficMetricsData> collectTrafficMetricsDataList = new ArrayList<>();
// 遍历整理后的路段交通数据,计算每个路段的交通指标。
sectionDataMap.forEach((roadSectionId, trafficSectionData) -> collectTrafficMetricsDataList.addAll(calculateSectionTrafficMetrics(trafficSectionData, roadSectionId, direction, periodType)));
// 返回计算得到的整个路段的交通指标数据列表。
return collectTrafficMetricsDataList;
}
/**
* 计算全程的交通指标数据
*
* @param trafficSectionDataList 交通路段数据列表包含各个路段的交通统计信息
* @param direction 行驶方向用于筛选数据
* @param periodType 统计周期类型用于区分不同类型的交通数据
* @return 返回一个包含完整交通指标数据的列表每个指标对应一个日期和行驶方向
*/
private List<DcTrafficMetricsData> calculateFullTrafficMetrics(
List<DcTrafficSectionData> trafficSectionDataList,
Byte direction,
Byte periodType
) {
// 检查输入数据是否为空,若为空则直接返回空列表
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) {
return Collections.emptyList();
}
// 根据统计日期和行驶方向对交通数据进行分组
Map<Date, Map<Byte, List<DcTrafficSectionData>>> groupedByDateAndDirection = trafficSectionDataList.stream()
.collect(Collectors.groupingBy(
DcTrafficSectionData::getStatisticalDate,
Collectors.groupingBy(DcTrafficSectionData::getDirection)
));
List<DcTrafficMetricsData> result = new ArrayList<>();
// 遍历分组后的数据,计算每组数据的交通指标
groupedByDateAndDirection.forEach((date, dataMap) -> {
// 合并同一日期不同方向的所有数据
List<DcTrafficSectionData> sectionDataList = new ArrayList<>();
dataMap.forEach((key, value) -> sectionDataList.addAll(value));
// 根据合并后的数据计算交通指标
DcTrafficMetricsData metricsData = new DcTrafficMetricsData();
// 设置周期类型
metricsData.setPeriodType(periodType);
// 设置统计日期
metricsData.setStatisticalDate(date);
// 计算交通组成特征指数
metricsData.setTrafficFeature(calculateTrafficFeature(sectionDataList));
// 计算饱和度
metricsData.setSaturationLevel(calculateFullSaturationDegree(sectionDataList, direction, periodType));
// 计算通道拥挤度
metricsData.setChannelCongestionLevel(calculateSectionChannelCongestionLevel(sectionDataList));
// 计算路网拥堵指数
metricsData.setRoadNetworkCongestionLevel(calculateRoadNetworkCongestionLevel(sectionDataList));
result.add(metricsData);
});
return result;
}
/**
* 计算路段交通指标的方法
*
* @param trafficSectionDataList 交通路段数据列表包含各个时间段的交通数据
* @param roadSectionId 路段ID用于指定需要计算交通指标的路段
* @param direction 方向标识用于指定计算交通指标的方向
* @param periodType 时段类型用于指定计算交通指标的时间段分类季度
* @return 返回一个交通指标数据列表每个指标包含特定日期和方向的交通组成特征值饱和度通道拥挤度和断面交通量
*/
private List<DcTrafficMetricsData> calculateSectionTrafficMetrics(
List<DcTrafficSectionData> trafficSectionDataList,
Long roadSectionId,
Byte direction,
Byte periodType
) {
// 检查输入数据列表是否为空,若为空则直接返回空列表
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) {
return Collections.emptyList();
}
// 根据统计日期和行驶方向对交通数据进行分组
Map<Date, Map<Byte, List<DcTrafficSectionData>>> groupedByDateAndDirection = trafficSectionDataList.stream()
.collect(Collectors.groupingBy(
DcTrafficSectionData::getStatisticalDate,
Collectors.groupingBy(DcTrafficSectionData::getDirection)
));
List<DcTrafficMetricsData> result = new ArrayList<>();
// 遍历分组后的数据,计算每组的交通指标
groupedByDateAndDirection.forEach((date, dataMap) -> {
// 合并同一日期不同方向的所有数据
List<DcTrafficSectionData> sectionDataList = new ArrayList<>();
dataMap.forEach((key, value) -> sectionDataList.addAll(value));
// 根据合并后的数据计算交通指标
DcTrafficMetricsData metricsData = new DcTrafficMetricsData();
// 设置路段ID
metricsData.setRoadSectionId(roadSectionId);
// 时段类型
metricsData.setPeriodType(periodType);
// 设置方向
metricsData.setDirection(direction);
// 设置统计日期
metricsData.setStatisticalDate(date);
// 计算交通组成特征指数
metricsData.setTrafficFeature(calculateTrafficFeature(sectionDataList));
// 计算饱和度
metricsData.setSaturationLevel(calculateSectionSaturationDegree(sectionDataList, roadSectionId, direction, periodType, DateUtil.year(date)));
// 计算通道拥挤度
metricsData.setChannelCongestionLevel(calculateSectionChannelCongestionLevel(sectionDataList));
// 计算断面交通量
metricsData.setTrafficVolume(calculateSectionTrafficVolume(dataMap));
result.add(metricsData);
});
return result;
}
/**
* 计算交通组成特征值
*
* @param trafficSectionData 交通数据集合
* @return 大车流量在总车流量中占比的百分比如果数据集合为空或总流量为0则返回null
*/
private Integer calculateTrafficFeature(List<DcTrafficSectionData> trafficSectionData) {
// 检查数据集合是否为空,是则返回 0
if (trafficSectionData == null || trafficSectionData.isEmpty()) {
return null;
}
// 初始化总流量和大车流量总和
int totalTrafficVolume = 0;
int totalLargeTrafficVolume = 0;
// 使用Stream API计算大流量和总流量的累加值,同时处理了可能的NullPointerException
for (DcTrafficSectionData data : trafficSectionData) {
totalLargeTrafficVolume += data.getLargeTrafficVolume();
totalTrafficVolume += data.getTrafficVolume();
}
// 总流量为0时,返回0
if (totalTrafficVolume == 0) {
return null;
}
// 计算大流量占比的百分比并返回
double percentage = (double) totalLargeTrafficVolume / totalTrafficVolume * 100;
return (int) Math.round(percentage);
}
/**
* 计算整个路网的饱和度程度
*
* @param trafficSectionDataList 交通路段数据列表
* @param direction 行驶方向用字节表示
* @param periodType 时期类型用于确定是哪个时间段的交通数据季度
* @return 计算得到的整个路段平均饱和度如果输入数据为空则返回null
*/
private Integer calculateFullSaturationDegree(List<DcTrafficSectionData> trafficSectionDataList, Byte direction, Byte periodType) {
// 检查输入的数据映射是否为空,若为空则直接返回null
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) {
return null;
}
// 将输入的路段交通数据列表按照路段ID进行分组,得到一个Map,key为路段ID,value为该路段的交通数据列表。
Map<Long, List<DcTrafficSectionData>> trafficSectionMap = groupTrafficSectionDataByRoadSectionId(trafficSectionDataList);
List<Integer> sectionSaturationDegreeList = new ArrayList<>();
// 遍历每个路段的数据列表,计算并收集每个路段的饱和度
trafficSectionMap.forEach((roadSectionId, sectionDataList) -> {
if (!sectionDataList.isEmpty()) {
Integer year = DateUtil.year(sectionDataList.get(0).getStatisticalDate());
Integer saturationDegree = calculateSectionSaturationDegree(sectionDataList, roadSectionId, direction, periodType, year);
sectionSaturationDegreeList.add(saturationDegree);
}
});
// 计算所有路段饱和度的平均值
double averageSaturationDegree = sectionSaturationDegreeList.stream()
.mapToDouble(Integer::doubleValue)
.average()
.orElse(0.0);
// 将平均饱和度值四舍五入为整数后返回
return (int) Math.round(averageSaturationDegree);
}
/**
* 计算路段饱和度
*
* @param trafficSectionData 交通路段数据列表
* @param roadSectionId 路段ID用于标识具体的路段
* @param direction 行驶方向用于确定交通量是否需要折半计算
* @param periodType 时期类型用于确定是哪个时间段的交通数据季度
* @param year 年份用于确定是哪年的交通数据
* @return 返回计算得到的路段饱和度饱和度为交通量与设计交通量的比值乘以100
*/
private Integer calculateSectionSaturationDegree(List<DcTrafficSectionData> trafficSectionData, Long roadSectionId, Byte direction, Byte periodType, Integer year) {
// 检查数据集合是否为空,是则返回 0
if (trafficSectionData == null || trafficSectionData.isEmpty()) {
return null;
}
// 获取设计交通量
Integer designTrafficVolume = getDesignTrafficVolume(roadSectionId, periodType, year);
// 如果指定了行驶方向,设计交通量需要折半计算
if (direction != null) {
designTrafficVolume = (int) Math.round(designTrafficVolume * 0.5);
}
// 获取当前交通量的平均值
double averageWeightedVolume = trafficSectionData.stream()
.mapToDouble(data ->
data.getLargeTrafficVolume() * VehicleLoadSaturationFactorEnum.LARGE_VEHICLE.getConversionFactor()
+ (data.getTrafficVolume() - data.getLargeTrafficVolume()))
.average()
.orElse(0.0);
// 计算饱和度
double saturationDegree = averageWeightedVolume / designTrafficVolume * 100;
return (int) Math.round(saturationDegree);
}
/**
* 计算路段通道拥挤度
*
* @param trafficSectionDataList 路段交通数据列表包含多个路段的交通情况数据
* @return 返回计算得到的路段通道拥挤度如果输入数据为空则返回0
*/
private Integer calculateSectionChannelCongestionLevel(List<DcTrafficSectionData> trafficSectionDataList) {
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) {
return 0; // 列表为空时返回0
}
double totalWeightedSpeed = 0; // 总的加权速度
int totalVolume = 0; // 总的车流量,作为权重
// 遍历路段数据,计算加权速度和总车流量
for (DcTrafficSectionData data : trafficSectionDataList) {
if (data.getTrafficVolume() != null && data.getAverageSpeed() != null) {
totalWeightedSpeed += data.getTrafficVolume() * data.getAverageSpeed(); // 累加加权速度
totalVolume += data.getTrafficVolume(); // 累加权重
}
}
if (totalVolume == 0) {
return 0; // 防止除以0的情况
}
// 计算并返回加权平均速度
return (int) Math.round(totalWeightedSpeed / totalVolume);
}
/**
* 计算路网拥堵指数
* @param trafficSectionDataList 交通路段数据列表包含各个路段的拥堵情况和里程等信息
* @return 返回路网整体的拥堵指数如果输入列表为空则返回null
*/
public Integer calculateRoadNetworkCongestionLevel(List<DcTrafficSectionData> trafficSectionDataList) {
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) {
return null; // 列表为空时返回null
}
// 根据行驶方向对交通数据进行分组,以便分别计算每个方向的拥堵情况
Map<Byte, List<DcTrafficSectionData>> groupedByDirection = trafficSectionDataList.stream()
.collect(Collectors.groupingBy(DcTrafficSectionData::getDirection));
// 总里程,基于道路方向的数量乘以单方向的总里程
int totalDistance = StakeMarkConstant.calculateRoadLength() * groupedByDirection.size();
// 总拥堵里程,使用AtomicInteger以支持在并发环境中进行累加操作
AtomicInteger totalCongestionDistance = new AtomicInteger();
// 遍历每个方向的数据,计算总拥堵里程
groupedByDirection.forEach((directionData, trafficSectionList) -> {
List<DcTrafficSectionData> sortedList;
// 根据行驶方向,对交通路段数据进行排序,上行方向逆序,下行方向正序
if (directionData.equals(LaneDirection.UPWARD.getValue())) {
sortedList = trafficSectionList.stream()
.sorted(Comparator.comparing(DcTrafficSectionData::getStakeMark).reversed())
.collect(Collectors.toList());
} else {
sortedList = trafficSectionList.stream()
.sorted(Comparator.comparing(DcTrafficSectionData::getStakeMark))
.collect(Collectors.toList());
}
// 用于计算拥堵里程的辅助变量
int previousStakeMark = 0;
int previousAverageSpeed = 0;
int defaultCongestionDistance = 0;
// 遍历排序后的路段数据,计算每个路段的拥堵里程,并累加到总拥堵里程中
for (DcTrafficSectionData dcTrafficSectionData : sortedList) {
int averageSpeed = dcTrafficSectionData.getAverageSpeed();
int stakeMark = dcTrafficSectionData.getStakeMark();
// 对于不拥堵的路段,累加之前计算的默认拥堵距离
if (!ChannelCongestionLevelEnum.isMediumOrSevereCongestion(averageSpeed)) {
totalCongestionDistance.addAndGet(defaultCongestionDistance);
previousStakeMark = stakeMark;
defaultCongestionDistance = 0;
continue;
}
// 根据平均速度计算默认拥堵距离
defaultCongestionDistance = ChannelCongestionLevelEnum.fromSpeed(averageSpeed).getDefaultCongestionDistance();
// 如果之前已经有路段被计算过,则根据两个路段之间的距离和默认拥堵距离,计算实际应累加的拥堵距离
if (previousAverageSpeed != 0) {
int congestionDistance = Math.abs(stakeMark - previousStakeMark);
totalCongestionDistance.addAndGet(Math.min(congestionDistance, defaultCongestionDistance));
}
// 更新辅助变量以备后续计算
previousStakeMark = stakeMark;
previousAverageSpeed = averageSpeed;
}
});
// 计算并返回路网整体的拥堵指数
return Math.round((float) totalCongestionDistance.get() / totalDistance * 100);
}
/**
* 计算路段交通量
*
* @param trafficSectionData 路段交通数据的映射key为方向value为该方向上的路段交通数据列表
* @return 返回计算出的总交通量如果输入数据为空则返回null
*/
private Integer calculateSectionTrafficVolume(Map<Byte, List<DcTrafficSectionData>> trafficSectionData) {
// 检查输入数据是否为空,为空则直接返回0
if (trafficSectionData == null || trafficSectionData.isEmpty()) {
return null;
}
// 使用AtomicReference来存放最终的交通量,确保线程安全
AtomicReference<Integer> trafficVolume = new AtomicReference<>(0);
// 遍历每个方向的路段数据,累加其交通量
trafficSectionData.forEach((direction, trafficSectionList) -> {
// 对每个方向上的路段数据,提取交通量并找出最大值,然后累加到总交通量上
trafficVolume.updateAndGet(v -> v + trafficSectionList.stream()
.map(DcTrafficSectionData::getTrafficVolume) // 提取每个路段数据的交通量
.max(Integer::compareTo).orElse(0)); // 找出最大交通量,若无则为0
});
// 返回最终计算出的总交通量
return trafficVolume.get();
}
/**
* 根据道路路段ID和周期类型年份获取设计交通量
*
* @param roadSectionId 道路路段的ID如果为null则默认为0
* @param periodType 交通数据的周期类型如果为null则默认计算五分钟内的交通量
* 周期类型包括年季度月以及其他默认情况
* @param year 年份用于查询指定年份的设计交通量
* @return 根据给定的路段ID和周期类型年份计算出的设计交通量
*/
private Integer getDesignTrafficVolume(Long roadSectionId, Byte periodType, Integer year) {
// 处理null的roadSectionId,默认为0
if (roadSectionId == null) {
roadSectionId = 0L;
}
Integer designTrafficVolume;
// 使用缓存来存储每年的设计交通量,减少数据库查询
HashMap<Integer, Integer> yearDesignTrafficVolumeCache = designTrafficVolumeCache.computeIfAbsent(roadSectionId, k -> new HashMap<>());
// 判断缓存中是否已存在当前年份的设计交通量,如果存在直接使用,否则进行查询
if (yearDesignTrafficVolumeCache.containsKey(year)) {
designTrafficVolume = yearDesignTrafficVolumeCache.get(year);
} else {
// 构建查询条件,并根据条件查询数据库获取设计交通量
LambdaQueryWrapper<DcTrafficVolumeForecast> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DcTrafficVolumeForecast::getRoadSectionId, roadSectionId);
queryWrapper.eq(DcTrafficVolumeForecast::getYear, year);
DcTrafficVolumeForecast dcTrafficVolumeForecast = dcTrafficVolumeForecastService.getOne(queryWrapper);
designTrafficVolume = dcTrafficVolumeForecast.getDesignTrafficVolume();
yearDesignTrafficVolumeCache.put(year, designTrafficVolume);
}
// 根据周期类型计算不同周期的交通量,默认为五分钟交通量
if (periodType == null) {
return Math.round(designTrafficVolume / ((float) 24 * 60 / 5));
}
TrafficDataPeriodTypeEnum typeEnum = TrafficDataPeriodTypeEnum.valueOfCode(periodType);
switch (typeEnum) {
case YEAR:
return designTrafficVolume * 365; // 年交通量
case QUARTER:
return designTrafficVolume * 91; // 季度交通量
case MONTH:
return designTrafficVolume * 31; // 月交通量
default:
return designTrafficVolume; // 默认为日交通量
}
}
/**
* 获取有序的DcRoadSection缓存列表
* 该方法首先检查有序的DcRoadSection缓存是否已经存在如果存在直接返回缓存如果缓存不存在则从Redis缓存中获取所有路段信息
* 根据路段的起止桩号进行排序然后将排序后的结果存储到有序的DcRoadSection缓存中并返回该缓存列表
*
* @return 返回一个按路段起止桩号顺序排序的DcRoadSection列表
*/
private List<DcRoadSection> getOrderedDcRoadSectionCache() {
if (orderedDcRoadSectionCache != null) {
return orderedDcRoadSectionCache; // 直接返回已缓存的有序路段信息
}
// 从Redis缓存中获取所有路段信息,并根据路段起止桩号进行排序
Map<String, DcRoadSection> dcRoadSectionMap = redisCache.getCacheMapValue(RedisKeyConstants.DC_ROAD_SECTION);
List<DcRoadSection> orderedDcRoadSection = dcRoadSectionMap.values().stream()
.sorted(Comparator.comparing((dcRoadSection) -> StakeMarkUtils.stakeMarkToInt(dcRoadSection.getEndStakeMark()) + StakeMarkUtils.stakeMarkToInt(dcRoadSection.getStartStakeMark())))
.collect(Collectors.toList());
orderedDcRoadSectionCache = orderedDcRoadSection; // 将排序后的路段信息缓存起来
return orderedDcRoadSection;
}
/**
* 使用二分查找在已排序的路段时间对象列表中找到对应的路段ID
*
* @param sortedRoadSectionObjects 已排序的路段时间对象列表列表中的对象应实现了Comparable接口
* @param dcTrafficSectionData 需要查找的交通数据段对象包含桩号信息
* @return 找到的路段ID如果没有找到则返回特定的标识-1
*/
private int findSectionIdByBinarySearch(List<Object> sortedRoadSectionObjects, DcTrafficSectionData dcTrafficSectionData) {
// 使用AlgorithmUtils的二分查找方法进行查找,比较函数通过lambda表达式定义
return AlgorithmUtils.binarySearch(sortedRoadSectionObjects, dcTrafficSectionData, (roadSection, trafficSectionData) -> {
DcRoadSection dcRoadSection = (DcRoadSection) roadSection;
Integer startStakeMark = StakeMarkUtils.stakeMarkToInt(dcRoadSection.getStartStakeMark());
Integer endStakeMark = StakeMarkUtils.stakeMarkToInt(dcRoadSection.getEndStakeMark());
DcTrafficSectionData trafficSectionData1 = (DcTrafficSectionData) trafficSectionData;
Integer stakeMark = trafficSectionData1.getStakeMark();
// 比较交通数据的桩号是否位于路段的起始桩号和结束桩号之间,以判断数据是否属于该路段
if (stakeMark >= startStakeMark && stakeMark <= endStakeMark) {
return 0; // 属于该路段,返回0表示找到
} else if (stakeMark < startStakeMark) {
return 1; // 桩号小于起始桩号,说明要查找的路段在当前路段的左侧
} else {
return -1; // 桩号大于结束桩号,说明要查找的路段在当前路段的右侧
}
});
}
/**
* 将交通数据按照路段ID分组
*
* @param trafficSectionDataList 交通路段数据列表每个数据包含路段相关信息
* @return 分组后的交通路段数据以路段ID为键对应路段的所有交通数据为值
*/
private Map<Long, List<DcTrafficSectionData>> groupTrafficSectionDataByRoadSectionId(List<DcTrafficSectionData> trafficSectionDataList) {
// 获取已排序的路段信息缓存。
List<DcRoadSection> sortedRoadSections = getOrderedDcRoadSectionCache();
// 创建一个对象列表,用于二分查找。
List<Object> sortedRoadSectionObjects = new ArrayList<>(sortedRoadSections.size());
sortedRoadSectionObjects.addAll(sortedRoadSections);
// 使用HashMap来组织路段交通数据,以便于后续处理。
Map<Long, List<DcTrafficSectionData>> sectionDataMap = new HashMap<>();
// 遍历输入的路段交通数据列表,将数据按照路段进行组织。
for (DcTrafficSectionData trafficSectionData : trafficSectionDataList) {
// 使用二分查找来找到当前交通数据所属的路段。
int index = findSectionIdByBinarySearch(sortedRoadSectionObjects, trafficSectionData);
// 如果找到对应的路段,则将交通数据添加到该路段的数据列表中。
if (index > -1) {
DcRoadSection dcRoadSection = sortedRoadSections.get(index);
List<DcTrafficSectionData> dcTrafficSectionList = sectionDataMap.putIfAbsent(dcRoadSection.getId(), new ArrayList<>());
dcTrafficSectionList.add(trafficSectionData);
}
}
return sectionDataMap;
}
}

495
zc-business/src/main/java/com/zc/business/statistics/handler/TrafficStatistics.java

@ -0,0 +1,495 @@
package com.zc.business.statistics.handler;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.DateUtils;
import com.zc.business.constant.RedisKeyConstants;
import com.zc.business.constant.StatisticalRecoveryOffsetTime;
import com.zc.business.controller.DcDeviceController;
import com.zc.business.domain.DcDevice;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.*;
import com.zc.business.mapper.DcTrafficSectionDataMapper;
import com.zc.business.statistics.cache.*;
import com.zc.common.core.httpclient.exception.HttpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
/**
* 交通数据统计处理类
*
* 该类提供了一系列方法用于对交通数据进行统计分析包括车流量总和平均车速等
* @author xiepufeng
*/
@Component
public class TrafficStatistics {
// 日志记录器
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private DcTrafficSectionDataMapper dcTrafficSectionDataMapper;
@Resource
private RedisCache redisCache;
@Resource
private DcDeviceController dcDeviceController;
/**
* 定义每小时第20分钟执行的任务用于清除过期缓存数据并将缓存中的数据整合后保存至数据库
*/
@Scheduled(cron = "0 20 * * * ?") // 每小时的20分整点执行该任务
public void performHourlyCleanupAndPersist() {
// 清除已过期的缓存数据
DailyTrafficStatisticsCache.clearExpiredData();
MonthlyTrafficStatisticsCache.clearExpiredData();
QuarterlyTrafficStatisticsCache.clearExpiredData();
YearlyTrafficStatisticsCache.clearExpiredData();
// 整合缓存数据并保存至数据库
// 将缓存中的数据按日统计后保存至数据库
// 添加月交通断面数据到缓存中
persistAggregatedData(DailyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.DAY, MonthlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按月统计后保存至数据库
// 添加季度交通断面数据到缓存中
persistAggregatedData(MonthlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.MONTH, QuarterlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按季度统计后保存至数据库
// 添加年交通断面数据到缓存中
persistAggregatedData(QuarterlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.QUARTER, YearlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按年统计后保存至数据库
persistAggregatedData(YearlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.YEAR, (a) -> {});
}
/**
* 处理设备数据的函数
* 该方法首先将传入的设备对象转换为JSON对象然后从JSON对象中提取设备ID
* 如果设备ID有效则查询设备的实时流量数据并对获取的设备属性数据进行处理
*
* @param deviceObject 设备对象需要是一个JSONObject包含设备信息
*/
public void processDeviceData(Object deviceObject) {
JSONObject deviceJsonObject = (JSONObject)deviceObject;
// 提取设备ID
String deviceId = deviceJsonObject.getString("id");
// 检查设备ID的有效性
if (deviceId == null || deviceId.isEmpty()) {
logger.error("获取一类交通量调查站设备id失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 查询并处理设备属性数据
try {
HashMap<String, Object> props = buildPropertiesRequiredParameter();
// 查询设备的实时属性数据
Map<String, Object> deviceProperties = dcDeviceController.queryDeviceProperties(deviceId, IotProductPropertiesEnum.ONE_STOP_PRODUCT_01.getNumber(), props);
// 对获取的设备属性数据进行进一步处理
processDeviceProperties(deviceProperties);
} catch (HttpException | IOException e) {
// 记录处理设备属性数据时发生的异常
logger.error("处理设备属性数据时发生异常,设备id:{}", deviceId, e);
}
}
/**
* 构建必需的属性请求参数集合
* 该方法不接受任何参数返回一个包含特定键值对的HashMap对象
* 键值对表达了查询条件其中键是查询条件的结构化字符串表示值是条件的具体值
*
* @return HashMap<String, Object> 包含查询条件的HashMap对象
*/
private HashMap<String, Object> buildPropertiesRequiredParameter() {
// 获取数据统计的最大日期
Date maxStatisticalDate = dcTrafficSectionDataMapper.getMaxStatisticalDate();
// 如果最大统计日期为null,则计算默认的最大统计日期
if (maxStatisticalDate == null) {
maxStatisticalDate = DateUtil.offsetDay(new Date(), StatisticalRecoveryOffsetTime.TRAFFIC_SECTION_DATA_OFFSET_DAY);
}
HashMap<String, Object> props = new HashMap<>();
// 设置查询条件的键为“timestamp$BTW”,表示时间戳在一定范围内
props.put("terms[0].column", "timestamp$BTW");
ArrayList<String> dateList = new ArrayList<>();
// 添加当前日期的开始和结束时间到列表,用于设定时间范围
dateList.add(DateUtil.beginOfDay(maxStatisticalDate).toString());
dateList.add(DateUtil.endOfDay(new Date()).toString());
// 将日期列表以逗号分隔并设置为查询条件的值
props.put("terms[0].value", String.join(",", dateList));
props.put("paging", false);
return props;
}
/**
* 处理设备属性信息
* 该方法接收一个设备属性的映射从中提取属性数据并将其转换为交通断面数据统计定义对象列表然后将这些数据添加到缓存中
*
* @param deviceProperties 包含设备属性数据的映射该映射应包含一个名为"data"的键其值是一个包含具体属性信息的JSON对象
*/
private void processDeviceProperties(Map<String, Object> deviceProperties) {
// 检查传入的设备属性映射是否为空,或者其中的"data"键对应的值是否为空
if (deviceProperties == null || deviceProperties.get("data") == null) {
logger.error("获取一类交通量调查站属性数据失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 从设备属性映射的"data"值中解析出JSON对象
JSONObject propertiesObject = JSONObject.parseObject(deviceProperties.get("data").toString());
// 检查解析出的JSON对象是否包含"data"键
if (propertiesObject.get("data") == null) {
logger.error("获取一类交通量调查站属性数据失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value());
return;
}
// 从解析出的JSON对象中获取设备属性数据列表
JSONArray properties = JSONArray.parseArray(propertiesObject.get("data").toString());
// 遍历设备属性数据列表,对每个属性对象进行处理
properties.forEach(propertyObject -> {
JSONObject propertyJsonObject = (JSONObject)propertyObject;
// 将设备消息转换为交通断面数据统计定义对象
List<DcTrafficSectionData> dcTrafficSectionDataList = convertToTrafficStatistics(propertyJsonObject, DeviceDataCategoryEnum.HISTORY);
// 如果转换结果非空且不为空列表,则将转换后的数据添加到缓存中
if (dcTrafficSectionDataList != null && !dcTrafficSectionDataList.isEmpty()) {
// 将转换后的数据添加到缓存中
dcTrafficSectionDataList.forEach(DailyTrafficStatisticsCache::addCacheData);
}
});
}
/**
* 将设备消息转换为交通断面数据统计定义对象
*
* @param msg JSON格式的设备实时消息
* @param category 设备数据类型
* @return 转换后的交通断面数据统计定义对象
*/
public List<DcTrafficSectionData> convertToTrafficStatistics(JSONObject msg, DeviceDataCategoryEnum category) {
// 获取属性
JSONObject property;
if (category == DeviceDataCategoryEnum.REAL_TIME) {
JSONObject properties = msg.getJSONObject("properties");
// 属性数据
if (properties == null) {
logger.error("接收实时属性数据,属性数据不存在,请检查物联网一类交通量调查站设备数据是否正常");
return null;
}
// 获取属性01的数据
property = properties.getJSONObject(IotProductPropertiesEnum.ONE_STOP_PRODUCT_01.getNumber());
} else {
property = msg.getJSONObject("value");
}
// 判断是否属性01的数据
if (property == null) {
logger.error("非属性01的数据,无法处理,请检查物联网一类交通量调查站设备数据是否正常");
return null;
}
// 物联设备id
String iotDeviceId = msg.getString("deviceId");
if (iotDeviceId == null || iotDeviceId.isEmpty()) {
logger.error("设备id为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 上报时间(时间戳)
Long timestamp = msg.getLong("timestamp");
if (timestamp == null || timestamp == 0L) {
logger.error("上报时间为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 从缓存中获取设备信息
DcDevice dcDevice = redisCache.getCacheMapValue(RedisKeyConstants.DC_DEVICES, iotDeviceId);
if (dcDevice == null) {
logger.error("设备信息不存在,请检查是否配置设备信息,物联平台设备id:{}无对应设备信息", iotDeviceId);
return null;
}
// 上报时间
Date reportTime = DateUtil.date(timestamp);
// 车道信息
JSONArray lanes = property.getJSONArray("lanes");
if (lanes == null || lanes.isEmpty()) {
logger.error("车道信息为空,无法处理,请检查物联网平台一类交通量调查站设备数据是否正常");
return null;
}
// 分别处理上行和下行数据
return new ArrayList<>(processLaneData(lanes, dcDevice, reportTime).values());
}
/**
* 处理车道数据计算上行和下行的车流量和累计平均速度
*
* @param lanes 车道数据的JSON数组包含各个车道的信息
* @param dcDevice 设备信息用于标识数据来源
* @param reportTime 报告时间标识数据的时间戳
* @return 返回一个Map包含上行和下行的交通段数据
*/
private Map<LaneDirection, DcTrafficSectionData> processLaneData(JSONArray lanes, DcDevice dcDevice, Date reportTime) {
Map<LaneDirection, DcTrafficSectionData> resultMap = new HashMap<>();
// 初始化上行和下行的交通段数据
DcTrafficSectionData upwardData = new DcTrafficSectionData();
DcTrafficSectionData downwardData = new DcTrafficSectionData();
initializeTrafficSectionData(upwardData, dcDevice, reportTime, LaneDirection.UPWARD);
initializeTrafficSectionData(downwardData, dcDevice, reportTime, LaneDirection.DOWNWARD);
// 初始化上行和下行的车流量
int upwardTrafficVolume = 0, downwardTrafficVolume = 0;
// 初始化上行和下行的累计大车流量
int upwardLargeTrafficVolume = 0, downwardLargeTrafficVolume = 0;
// 初始化上行和下行的累计平均速度
double upwardCumulativeAverageSpeed = 0.0, downwardCumulativeAverageSpeed = 0.0;
// 遍历车道信息,计算上行和下行的车流量和累计平均速度
for (Object lane : lanes) {
// 解析车道数据
JSONObject laneData = (JSONObject) lane;
// 获取车道号,并判断是否为下行车道
Integer laneNumber = laneData.getInteger("laneNumber");
boolean isDownward = laneNumber >= 31 || laneNumber == 3;
// 根据车道方向累加车流量和累计平均速度
int totalTrafficFlow = laneData.getInteger("totalTrafficFlow");
int cumulativeAverageSpeed = calculateCumulativeAverageSpeed(laneData);
int cumulativeLargeTrafficVolume = calculateCumulativeLargeTrafficVolume(laneData);
if (isDownward) {
downwardTrafficVolume += totalTrafficFlow;
downwardCumulativeAverageSpeed += cumulativeAverageSpeed;
downwardLargeTrafficVolume += cumulativeLargeTrafficVolume;
} else {
upwardTrafficVolume += totalTrafficFlow;
upwardCumulativeAverageSpeed += cumulativeAverageSpeed;
upwardLargeTrafficVolume += cumulativeLargeTrafficVolume;
}
}
// 设置上行和下行的车流量和累计平均速度
setTrafficSectionData(upwardData, upwardTrafficVolume, upwardCumulativeAverageSpeed, upwardLargeTrafficVolume);
setTrafficSectionData(downwardData, downwardTrafficVolume, downwardCumulativeAverageSpeed, downwardLargeTrafficVolume);
// 将上行和下行的交通段数据放入结果映射中
resultMap.put(LaneDirection.UPWARD, upwardData);
resultMap.put(LaneDirection.DOWNWARD, downwardData);
return resultMap;
}
/**
* 初始化交通段数据
* @param data 交通段数据对象用于存储交通数据
* @param dcDevice 设备对象包含设备的详细信息
* @param reportTime 数据上报时间
* @param direction 车道方向
*/
private void initializeTrafficSectionData(DcTrafficSectionData data, DcDevice dcDevice, Date reportTime, LaneDirection direction) {
// 设置设备id
data.setDeviceId(dcDevice.getId());
// 设置车道方向
data.setDirection(direction.getValue());
// 设置上报时间
data.setReportTime(reportTime);
// 设置数据上报时间
data.setStatisticalDate(reportTime);
// 设置统计时段类型为日
data.setPeriodType(TrafficDataPeriodTypeEnum.DAY);
// 将设备的桩号转换为整数后设置
data.setStakeMark(dcDevice.stakeMarkToInt());
}
/**
* 设置交通路段数据
* 该方法用于根据给定的交通量和累积平均速度设置交通路段的数据
*
* @param data 交通路段数据对象用于存储交通路段的相关信息
* @param trafficVolume 该路段的交通量单位通常为车辆数
* @param cumulativeAverageSpeed 该路段的累积平均速度单位通常为千米/小时
* @param largeTrafficVolume 大型车交通量
*/
private void setTrafficSectionData(DcTrafficSectionData data, int trafficVolume, double cumulativeAverageSpeed, int largeTrafficVolume) {
data.setTrafficVolume(trafficVolume); // 设置交通量
data.setLargeTrafficVolume(largeTrafficVolume); // 设置大车流量
data.setAverageSpeed(0);
if (trafficVolume != 0) { // 当交通量不为0时,计算并设置平均速度
data.setAverageSpeed((int) Math.round(cumulativeAverageSpeed / trafficVolume)); // 平均速度 = 累积平均速度 / 交通量
}
}
/**
* 计算给定车道数据的累计平均速度
* 该函数根据laneData中提供的各种车辆类型的数量和平均速度计算出累计的平均速度
*
* @param laneData 包含车道数据的JSONObject对象其中包含了各种车辆类型的数量和平均速度等数据
* @return 返回计算出的累计平均速度
*/
private int calculateCumulativeAverageSpeed(JSONObject laneData) {
// 根据laneData中的数据计算累计平均速度
// 累加平均速度(中小客车)
return laneData.getInteger("trafficNumberOfInAndSmall") * laneData.getInteger("inAndSmallAverageVehicleSpeed") +
// 累加平均速度(小型货车)
laneData.getInteger("trafficVolumeOfSmallTrucks") * laneData.getInteger("smallTrucksAverageVehicleSpeed") +
// 累加平均速度(大客车)
laneData.getInteger("busTrafficVolume") * laneData.getInteger("averageSpeedOfBus") +
// 累加平均速度(中型货车)
laneData.getInteger("mediumTruckTrafficVolume") * laneData.getInteger("averageSpeedOfMediumSizeTrucks") +
// 累加平均速度(大型货车)
laneData.getInteger("largeTruckTrafficVolume") * laneData.getInteger("averageSpeedOfLargeTrucks") +
// 累加平均速度(特大型货车)
laneData.getInteger("extraLargeTrucksTrafficVolume") * laneData.getInteger("averageSpeedOfExtraLargeTrucks") +
// 累加平均速度(集装箱车)
laneData.getInteger("containerTruckTrafficVolume") * laneData.getInteger("averageSpeedOfContainerTruck") +
// 累加平均速度(拖拉机)
laneData.getInteger("tractorTrafficVolume") * laneData.getInteger("averageSpeedOfTractor") +
// 累加平均速度(摩托车)
laneData.getInteger("motorcycleTrafficVolume") * laneData.getInteger("averageSpeedOfMotorcycle");
}
/**
* 计算累积大型交通流量
* 该方法用于根据给定的车道数据计算出特定类型的车辆包括公共汽车中型货车大型货车特大型货车和集装箱车的交通量总和
*
* @param laneData 包含车道交通量数据的JSONObject对象该对象应包含以下键
* - "busTrafficVolume"公共汽车交通量
* - "mediumTruckTrafficVolume"中型货车交通量
* - "largeTruckTrafficVolume"大型货车交通量
* - "extraLargeTrucksTrafficVolume"特大型货车交通量
* - "containerTruckTrafficVolume"集装箱车交通量
* @return 返回各种类型大型车辆交通量的总和类型为int
*/
private int calculateCumulativeLargeTrafficVolume(JSONObject laneData) {
// 计算各种类型大型车辆的交通量总和
return laneData.getInteger("busTrafficVolume") +
laneData.getInteger("mediumTruckTrafficVolume") +
laneData.getInteger("largeTruckTrafficVolume") +
laneData.getInteger("extraLargeTrucksTrafficVolume") +
laneData.getInteger("containerTruckTrafficVolume");
}
/**
* 将缓存中的数据统计后保存至数据库
*/
public void persistAggregatedData(
Map<String, ? extends AbstractTrafficStatisticsCache> cache,
TrafficDataPeriodTypeEnum periodType,
Consumer<DcTrafficSectionData> consumer
) {
for (AbstractTrafficStatisticsCache data : cache.values()) {
// 如果数据已经存储过,则跳过此次处理
if (data.isStored()) {
continue;
}
DcTrafficSectionData aggregatedData = trafficStatistics(data.getData(), periodType);
if (dcTrafficSectionDataMapper.insertOrUpdate(aggregatedData)) {
// 设置数据已存储状态
data.setStored(true);
// 调用回调函数
consumer.accept(aggregatedData);
}
}
}
/**
* 对给定的交通数据集合进行统计分析返回一个综合交通数据对象
*
* @param dataCollection 交通数据集合不可为null或空包含多个交通路段的详细数据
* @param trafficDataPeriodType 交通数据的时段类型例如小时周等
* @return 综合交通数据对象包含车流量总和平均车速等统计结果如果输入数据为空则返回null
*/
public DcTrafficSectionData trafficStatistics(Collection<DcTrafficSectionData> dataCollection, TrafficDataPeriodTypeEnum trafficDataPeriodType) {
// 判断输入数据是否为空
if (CollectionUtils.isEmpty(dataCollection)) {
return null;
}
// 创建一个汇总统计用的对象
DcTrafficSectionData aggregatedData = new DcTrafficSectionData();
// 初始化车流量总和
int trafficVolume = 0;
// 初始化最大车流量
int largeTrafficVolume = 0;
// 初始化计算平均车速所需的分子部分
double numerator = 0;
// 遍历原始数据列表,累加车流量并计算平均车速
for (DcTrafficSectionData data: dataCollection) {
// 累加车流量
trafficVolume += data.getTrafficVolume();
// 累加最大车流量
largeTrafficVolume += data.getLargeTrafficVolume();
// 计算分子部分
numerator += data.getAverageSpeed() * data.getTrafficVolume();
}
// 使用第一个数据项的信息填充汇总统计对象的基本属性(设备ID、桩号、方向)
DcTrafficSectionData firstDcTrafficSectionData = dataCollection.iterator().next();
// 设备id
aggregatedData.setDeviceId(firstDcTrafficSectionData.getDeviceId());
// 桩号
aggregatedData.setStakeMark(firstDcTrafficSectionData.getStakeMark());
// 道路方向
aggregatedData.setDirection(firstDcTrafficSectionData.getDirection());
// 上报时间
aggregatedData.setReportTime(firstDcTrafficSectionData.getReportTime());
// 计算平均车速并设置到汇总统计对象中
if (trafficVolume != 0) {
aggregatedData.setAverageSpeed((int) Math.round(numerator / trafficVolume));
} else {
// 若车流量为0,则默认设置平均车速为0
aggregatedData.setAverageSpeed(0);
}
// 时段类型
aggregatedData.setPeriodType(trafficDataPeriodType);
// 设置统计时间
aggregatedData.setStatisticalDate(firstDcTrafficSectionData.getStatisticalDate(), trafficDataPeriodType);
// 车流量
aggregatedData.setTrafficVolume(trafficVolume);
// 大型车车流量
aggregatedData.setLargeTrafficVolume(largeTrafficVolume);
// 更新或插入操作
aggregatedData.setUpdateTime(DateUtils.getNowDate());
// 生成主键
aggregatedData.generateUniqueId();
return aggregatedData;
}
}

54
zc-business/src/main/java/com/zc/business/utils/AlgorithmUtils.java

@ -0,0 +1,54 @@
package com.zc.business.utils;
import java.util.Comparator;
import java.util.List;
/**
* @author xiepufeng
* AlgorithmUtils 类提供了一系列的算法工具方法
* 主要用于各种算法的实现和操作
*/
public class AlgorithmUtils {
/**
* 在已排序的列表中使用二分查找算法来查找指定目标元素的索引
* @param list 一个有序的列表列表元素必须实现Comparable接口
* @param target 要在列表中查找的目标元素
* @return 如果目标元素在列表中找到则返回其索引如果未找到则返回-1
* @param <T> 列表和目标元素的类型该类型必须实现Comparable接口以支持比较操作
*/
public static <T> int binarySearch(List<T> list, T target, Comparator<T> comparator) {
// 检查输入列表是否为null
if (list == null) {
throw new IllegalArgumentException("输入列表不能为空.");
}
// 检查列表是否为空,如果是,直接返回-1
if (list.isEmpty()) {
return -1;
}
// 检查Comparator是否存在,如果没有则默认使用Comparable接口
Comparator<T> effectiveComparator = comparator != null ? comparator : (x, y) -> ((Comparable<T>) x).compareTo(y);
int low = 0; // 定义搜索范围的最低索引
int high = list.size() - 1; // 定义搜索范围的最高索引
while (low <= high) { // 当搜索范围未缩小到零时继续循环
int mid = low + (high - low) / 2; // 计算当前搜索范围的中间索引
int cmp = effectiveComparator.compare(list.get(mid), target); // 比较中间元素和目标元素
if (cmp < 0) { // 如果中间元素小于目标元素,则目标元素可能在右侧
low = mid + 1;
} else if (cmp > 0) { // 如果中间元素大于目标元素,则目标元素可能在左侧
high = mid - 1;
} else {
return mid; // 如果中间元素等于目标元素,则找到目标,返回其索引
}
}
return -1; // 如果未找到目标元素,则返回-1
}
}

37
zc-business/src/main/java/com/zc/business/utils/StakeMarkUtils.java

@ -0,0 +1,37 @@
package com.zc.business.utils;
/**
* StakeMarkUtils 桩号工具类
* @author xiepufeng
*/
public class StakeMarkUtils {
/**
* 将桩号格式的字符串转换为总米数
* 桩号格式字符串的样式可以是 "Xkm+Ym"其中 X 表示公里数Y 表示米数
* 公里数和米数之间使用 '+' 连接且公里数和米数的单位字符 'k' '+' 可以是任意大小写
*
* @param stakeMark 桩号格式的字符串例如 "1km+200m"
* @return 如果输入字符串为 null则返回 null否则返回计算得到的总米数
*/
public static Integer stakeMarkToInt(String stakeMark) {
if (stakeMark == null) {
return null;
}
// 使用正则表达式分割字符串,以不区分大小写的 'k' 和 '+' 为分隔符
String[] parts = stakeMark.split("(?i)k|\\+");
// 提取公里数和米数字符串
String kmStr = parts[1].trim();
int km = Integer.parseInt(kmStr); // 将公里数字符串转换为整数
int m = 0;
if (parts.length == 3) {
// 如果存在米数,则提取并转换为整数
String mStr = parts[2].trim();
m = Integer.parseInt(mStr);
}
// 计算并返回总米数
return km * 1000 + m;
}
}
Loading…
Cancel
Save