diff --git a/zc-business/src/main/java/com/zc/business/constant/RedisKeyConstants.java b/zc-business/src/main/java/com/zc/business/constant/RedisKeyConstants.java index 706e36a2..003a8ab5 100644 --- a/zc-business/src/main/java/com/zc/business/constant/RedisKeyConstants.java +++ b/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; + } } diff --git a/zc-business/src/main/java/com/zc/business/constant/StakeMarkConstant.java b/zc-business/src/main/java/com/zc/business/constant/StakeMarkConstant.java new file mode 100644 index 00000000..614a2978 --- /dev/null +++ b/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; + } + +} diff --git a/zc-business/src/main/java/com/zc/business/controller/DcTrafficStatisticsController.java b/zc-business/src/main/java/com/zc/business/controller/DcTrafficStatisticsController.java new file mode 100644 index 00000000..828e8712 --- /dev/null +++ b/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 dcTrafficMetricsDataList = dcTrafficStatisticsService.historyTrafficMetrics(request); + // 将查询结果封装成成功响应并返回 + return AjaxResult.success(dcTrafficMetricsDataList); + } + +} diff --git a/zc-business/src/main/java/com/zc/business/controller/VideoController.java b/zc-business/src/main/java/com/zc/business/controller/VideoController.java index 86d3a73f..374b6f99 100644 --- a/zc-business/src/main/java/com/zc/business/controller/VideoController.java +++ b/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"); diff --git a/zc-business/src/main/java/com/zc/business/domain/DcDevice.java b/zc-business/src/main/java/com/zc/business/domain/DcDevice.java index 9e0e0350..e19dbf00 100644 --- a/zc-business/src/main/java/com/zc/business/domain/DcDevice.java +++ b/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); } } diff --git a/zc-business/src/main/java/com/zc/business/domain/DcTrafficMetricsData.java b/zc-business/src/main/java/com/zc/business/domain/DcTrafficMetricsData.java new file mode 100644 index 00000000..199561fd --- /dev/null +++ b/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; +} diff --git a/zc-business/src/main/java/com/zc/business/domain/DcTrafficVolumeForecast.java b/zc-business/src/main/java/com/zc/business/domain/DcTrafficVolumeForecast.java new file mode 100644 index 00000000..0e7d3d0e --- /dev/null +++ b/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; + +} diff --git a/zc-business/src/main/java/com/zc/business/enums/CameraDirection.java b/zc-business/src/main/java/com/zc/business/enums/CameraDirectionEnum.java similarity index 86% rename from zc-business/src/main/java/com/zc/business/enums/CameraDirection.java rename to zc-business/src/main/java/com/zc/business/enums/CameraDirectionEnum.java index ee6d1d0c..86f50ab2 100644 --- a/zc-business/src/main/java/com/zc/business/enums/CameraDirection.java +++ b/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; } diff --git a/zc-business/src/main/java/com/zc/business/enums/ChannelCongestionLevelEnum.java b/zc-business/src/main/java/com/zc/business/enums/ChannelCongestionLevelEnum.java new file mode 100644 index 00000000..f151e262 --- /dev/null +++ b/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; + } +} diff --git a/zc-business/src/main/java/com/zc/business/enums/DeviceDataCategory.java b/zc-business/src/main/java/com/zc/business/enums/DeviceDataCategoryEnum.java similarity index 73% rename from zc-business/src/main/java/com/zc/business/enums/DeviceDataCategory.java rename to zc-business/src/main/java/com/zc/business/enums/DeviceDataCategoryEnum.java index 82bd185a..12fd766d 100644 --- a/zc-business/src/main/java/com/zc/business/enums/DeviceDataCategory.java +++ b/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; } diff --git a/zc-business/src/main/java/com/zc/business/enums/LaneDirection.java b/zc-business/src/main/java/com/zc/business/enums/LaneDirection.java index ee51bfe8..bf28b601 100644 --- a/zc-business/src/main/java/com/zc/business/enums/LaneDirection.java +++ b/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); } } diff --git a/zc-business/src/main/java/com/zc/business/enums/VehicleLoadSaturationFactorEnum.java b/zc-business/src/main/java/com/zc/business/enums/VehicleLoadSaturationFactorEnum.java new file mode 100644 index 00000000..4f907e37 --- /dev/null +++ b/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.5,7t<载质量≤20t的货车 + */ + LARGE_VEHICLE(2.5, "大型车,7t<载质量≤20t的货车"), + + /** + * 汽车列车,车辆折算系数4.0,20t<载质量的货车 + */ + 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); + } +} diff --git a/zc-business/src/main/java/com/zc/business/mapper/DcTrafficVolumeForecastMapper.java b/zc-business/src/main/java/com/zc/business/mapper/DcTrafficVolumeForecastMapper.java new file mode 100644 index 00000000..ee8e8aa7 --- /dev/null +++ b/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 { + +} diff --git a/zc-business/src/main/java/com/zc/business/request/DcTrafficMetricsDataRequest.java b/zc-business/src/main/java/com/zc/business/request/DcTrafficMetricsDataRequest.java new file mode 100644 index 00000000..bd882e7a --- /dev/null +++ b/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; +} diff --git a/zc-business/src/main/java/com/zc/business/service/DcTrafficStatisticsService.java b/zc-business/src/main/java/com/zc/business/service/DcTrafficStatisticsService.java index b7a14a89..bceec71b 100644 --- a/zc-business/src/main/java/com/zc/business/service/DcTrafficStatisticsService.java +++ b/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 { @@ -14,12 +17,22 @@ public interface DcTrafficStatisticsService extends IService historyTrafficMetrics(DcTrafficMetricsDataRequest request); } diff --git a/zc-business/src/main/java/com/zc/business/service/DcTrafficVolumeForecastService.java b/zc-business/src/main/java/com/zc/business/service/DcTrafficVolumeForecastService.java new file mode 100644 index 00000000..7e7586ee --- /dev/null +++ b/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 { +} diff --git a/zc-business/src/main/java/com/zc/business/service/impl/DcTrafficStatisticsServiceImpl.java b/zc-business/src/main/java/com/zc/business/service/impl/DcTrafficStatisticsServiceImpl.java index 14d7a48f..f785878c 100644 --- a/zc-business/src/main/java/com/zc/business/service/impl/DcTrafficStatisticsServiceImpl.java +++ b/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 dcTrafficSectionDataList = convertToTrafficStatistics(msg, DeviceDataCategory.REAL_TIME); + List 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 dcTrafficSectionDataCaches = getDcTrafficSectionDataRedisCache(request.getDirection()); - DcTrafficMetricsData dcTrafficMetricsData = new DcTrafficMetricsData(); - return dcTrafficMetricsData; + // 根据指定的方向和路段ID范围获取交通数据 + List trafficSectionDataList = fetchTrafficDataByDirAndRange(request.getRoadSectionId(), dcTrafficSectionDataCaches); + + // 对获取的交通路段数据进行分析,得到交通指标数据 + List 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 historyTrafficMetrics(DcTrafficMetricsDataRequest request) { + + if (request.getStartTime() == null || request.getEndTime() == null) { + throw new ServiceException("开始时间或结束时间不能为空"); + } + + if (request.getPeriodType() == null) { + throw new ServiceException("时段类型不能为空"); + } + + // 构建查询条件 + LambdaQueryWrapper 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 props = buildPropertiesRequiredParameter(); - - // 查询设备的实时属性数据 - Map 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 包含查询条件的HashMap对象。 - */ - private HashMap buildPropertiesRequiredParameter() { - - // 获取数据统计的最大日期 - Date maxStatisticalDate = dcTrafficSectionDataMapper.getMaxStatisticalDate(); - - // 如果最大统计日期为null,则计算默认的最大统计日期 - if (maxStatisticalDate == null) { - maxStatisticalDate = DateUtil.offsetDay(new Date(), StatisticalRecoveryOffsetTime.TRAFFIC_SECTION_DATA_OFFSET_DAY); - } - - HashMap props = new HashMap<>(); - // 设置查询条件的键为“timestamp$BTW”,表示时间戳在一定范围内 - props.put("terms[0].column", "timestamp$BTW"); - ArrayList 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 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 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 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 processLaneData(JSONArray lanes, DcDevice dcDevice, Date reportTime) { - - Map 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 getDcTrafficSectionDataRedisCache(Byte direction) { - // 将上行和下行的交通段数据放入结果映射中 - resultMap.put(LaneDirection.UPWARD, upwardData); - resultMap.put(LaneDirection.DOWNWARD, downwardData); + Map dcTrafficSectionDataMap = new HashMap<>(); - return resultMap; - } + // 根据方向选择相应的交通数据 + Map 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 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 getTrafficDataForBothDirections() { + Map 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 cache, - TrafficDataPeriodTypeEnum periodType, - Consumer 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 fetchTrafficDataByDirAndRange(Long roadSectionId, List 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()); } } diff --git a/zc-business/src/main/java/com/zc/business/service/impl/DcTrafficVolumeForecastServiceImpl.java b/zc-business/src/main/java/com/zc/business/service/impl/DcTrafficVolumeForecastServiceImpl.java new file mode 100644 index 00000000..eee59c30 --- /dev/null +++ b/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 + implements DcTrafficVolumeForecastService { + +} diff --git a/zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java b/zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java deleted file mode 100644 index 802e709e..00000000 --- a/zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java +++ /dev/null @@ -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 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; - } -} diff --git a/zc-business/src/main/java/com/zc/business/statistics/handler/TrafficAnalysis.java b/zc-business/src/main/java/com/zc/business/statistics/handler/TrafficAnalysis.java new file mode 100644 index 00000000..2a9cb54f --- /dev/null +++ b/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> designTrafficVolumeCache = new HashMap<>(); + + // 缓存路段列表 + private static List orderedDcRoadSectionCache; + + /** + * 计算交通指标数据。 + * 根据提供的请求信息和路段数据列表,计算相应的交通指标。可以针对全程或指定路段进行统计,统计结果受请求中的分段标志、路段ID、方向和周期类型影响。 + * + * @param request 包含交通指标计算请求信息的对象,如是否分路段、路段ID、方向和周期类型。 + * @param trafficSectionDataList 包含多个路段数据的列表,用于计算交通指标。 + * @return 返回一个交通指标数据列表,每个列表项对应一个路段的交通指标数据。 + */ + public List calculateTrafficMetrics( + DcTrafficMetricsDataRequest request, + List 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 calculateFullSectionTrafficMetrics( + List sectionDataList, + Byte direction, + Byte periodType + ) { + // 如果输入的路段交通数据列表为空或为空列表,直接返回空列表。 + if (sectionDataList == null || sectionDataList.isEmpty()) { + return Collections.emptyList(); + } + + // 将输入的路段交通数据列表按照路段ID进行分组,得到一个Map,key为路段ID,value为该路段的交通数据列表。 + Map> sectionDataMap = groupTrafficSectionDataByRoadSectionId(sectionDataList); + + // 统计每个路段的交通指标数据。 + List 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 calculateFullTrafficMetrics( + List trafficSectionDataList, + Byte direction, + Byte periodType + ) { + // 检查输入数据是否为空,若为空则直接返回空列表 + if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) { + return Collections.emptyList(); + } + + // 根据统计日期和行驶方向对交通数据进行分组 + Map>> groupedByDateAndDirection = trafficSectionDataList.stream() + .collect(Collectors.groupingBy( + DcTrafficSectionData::getStatisticalDate, + Collectors.groupingBy(DcTrafficSectionData::getDirection) + )); + + List result = new ArrayList<>(); + + // 遍历分组后的数据,计算每组数据的交通指标 + groupedByDateAndDirection.forEach((date, dataMap) -> { + // 合并同一日期不同方向的所有数据 + List 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 calculateSectionTrafficMetrics( + List trafficSectionDataList, + Long roadSectionId, + Byte direction, + Byte periodType + ) { + + // 检查输入数据列表是否为空,若为空则直接返回空列表 + if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) { + return Collections.emptyList(); + } + + // 根据统计日期和行驶方向对交通数据进行分组 + Map>> groupedByDateAndDirection = trafficSectionDataList.stream() + .collect(Collectors.groupingBy( + DcTrafficSectionData::getStatisticalDate, + Collectors.groupingBy(DcTrafficSectionData::getDirection) + )); + + List result = new ArrayList<>(); + + // 遍历分组后的数据,计算每组的交通指标 + groupedByDateAndDirection.forEach((date, dataMap) -> { + // 合并同一日期不同方向的所有数据 + List 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 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 trafficSectionDataList, Byte direction, Byte periodType) { + + // 检查输入的数据映射是否为空,若为空则直接返回null + if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) { + return null; + } + + // 将输入的路段交通数据列表按照路段ID进行分组,得到一个Map,key为路段ID,value为该路段的交通数据列表。 + Map> trafficSectionMap = groupTrafficSectionDataByRoadSectionId(trafficSectionDataList); + + + List 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 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 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 trafficSectionDataList) { + if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) { + return null; // 列表为空时返回null + } + + // 根据行驶方向对交通数据进行分组,以便分别计算每个方向的拥堵情况 + Map> groupedByDirection = trafficSectionDataList.stream() + .collect(Collectors.groupingBy(DcTrafficSectionData::getDirection)); + + // 总里程,基于道路方向的数量乘以单方向的总里程 + int totalDistance = StakeMarkConstant.calculateRoadLength() * groupedByDirection.size(); + // 总拥堵里程,使用AtomicInteger以支持在并发环境中进行累加操作 + AtomicInteger totalCongestionDistance = new AtomicInteger(); + + // 遍历每个方向的数据,计算总拥堵里程 + groupedByDirection.forEach((directionData, trafficSectionList) -> { + + List 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> trafficSectionData) { + // 检查输入数据是否为空,为空则直接返回0 + if (trafficSectionData == null || trafficSectionData.isEmpty()) { + return null; + } + + // 使用AtomicReference来存放最终的交通量,确保线程安全 + AtomicReference 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 yearDesignTrafficVolumeCache = designTrafficVolumeCache.computeIfAbsent(roadSectionId, k -> new HashMap<>()); + + // 判断缓存中是否已存在当前年份的设计交通量,如果存在直接使用,否则进行查询 + if (yearDesignTrafficVolumeCache.containsKey(year)) { + designTrafficVolume = yearDesignTrafficVolumeCache.get(year); + } else { + // 构建查询条件,并根据条件查询数据库获取设计交通量 + LambdaQueryWrapper 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 getOrderedDcRoadSectionCache() { + + if (orderedDcRoadSectionCache != null) { + return orderedDcRoadSectionCache; // 直接返回已缓存的有序路段信息 + } + + // 从Redis缓存中获取所有路段信息,并根据路段起止桩号进行排序 + Map dcRoadSectionMap = redisCache.getCacheMapValue(RedisKeyConstants.DC_ROAD_SECTION); + + List 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 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> groupTrafficSectionDataByRoadSectionId(List trafficSectionDataList) { + // 获取已排序的路段信息缓存。 + List sortedRoadSections = getOrderedDcRoadSectionCache(); + + // 创建一个对象列表,用于二分查找。 + List sortedRoadSectionObjects = new ArrayList<>(sortedRoadSections.size()); + sortedRoadSectionObjects.addAll(sortedRoadSections); + + // 使用HashMap来组织路段交通数据,以便于后续处理。 + Map> sectionDataMap = new HashMap<>(); + + // 遍历输入的路段交通数据列表,将数据按照路段进行组织。 + for (DcTrafficSectionData trafficSectionData : trafficSectionDataList) { + // 使用二分查找来找到当前交通数据所属的路段。 + int index = findSectionIdByBinarySearch(sortedRoadSectionObjects, trafficSectionData); + // 如果找到对应的路段,则将交通数据添加到该路段的数据列表中。 + if (index > -1) { + DcRoadSection dcRoadSection = sortedRoadSections.get(index); + List dcTrafficSectionList = sectionDataMap.putIfAbsent(dcRoadSection.getId(), new ArrayList<>()); + dcTrafficSectionList.add(trafficSectionData); + } + } + + return sectionDataMap; + } + + +} diff --git a/zc-business/src/main/java/com/zc/business/statistics/handler/TrafficStatistics.java b/zc-business/src/main/java/com/zc/business/statistics/handler/TrafficStatistics.java new file mode 100644 index 00000000..3862b9f0 --- /dev/null +++ b/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 props = buildPropertiesRequiredParameter(); + + // 查询设备的实时属性数据 + Map 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 包含查询条件的HashMap对象。 + */ + private HashMap buildPropertiesRequiredParameter() { + + // 获取数据统计的最大日期 + Date maxStatisticalDate = dcTrafficSectionDataMapper.getMaxStatisticalDate(); + + // 如果最大统计日期为null,则计算默认的最大统计日期 + if (maxStatisticalDate == null) { + maxStatisticalDate = DateUtil.offsetDay(new Date(), StatisticalRecoveryOffsetTime.TRAFFIC_SECTION_DATA_OFFSET_DAY); + } + + HashMap props = new HashMap<>(); + // 设置查询条件的键为“timestamp$BTW”,表示时间戳在一定范围内 + props.put("terms[0].column", "timestamp$BTW"); + ArrayList 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 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 dcTrafficSectionDataList = convertToTrafficStatistics(propertyJsonObject, DeviceDataCategoryEnum.HISTORY); + + // 如果转换结果非空且不为空列表,则将转换后的数据添加到缓存中 + if (dcTrafficSectionDataList != null && !dcTrafficSectionDataList.isEmpty()) { + // 将转换后的数据添加到缓存中 + dcTrafficSectionDataList.forEach(DailyTrafficStatisticsCache::addCacheData); + } + + }); + } + + /** + * 将设备消息转换为交通断面数据统计定义对象 + * + * @param msg JSON格式的设备实时消息 + * @param category 设备数据类型 + * @return 转换后的交通断面数据统计定义对象 + */ + + public List 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 processLaneData(JSONArray lanes, DcDevice dcDevice, Date reportTime) { + + Map 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 cache, + TrafficDataPeriodTypeEnum periodType, + Consumer 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 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; + } +} diff --git a/zc-business/src/main/java/com/zc/business/utils/AlgorithmUtils.java b/zc-business/src/main/java/com/zc/business/utils/AlgorithmUtils.java new file mode 100644 index 00000000..bb47b8cc --- /dev/null +++ b/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 列表和目标元素的类型,该类型必须实现Comparable接口以支持比较操作。 + */ + public static int binarySearch(List list, T target, Comparator comparator) { + + // 检查输入列表是否为null + if (list == null) { + throw new IllegalArgumentException("输入列表不能为空."); + } + + // 检查列表是否为空,如果是,直接返回-1 + if (list.isEmpty()) { + return -1; + } + + // 检查Comparator是否存在,如果没有则默认使用Comparable接口 + Comparator effectiveComparator = comparator != null ? comparator : (x, y) -> ((Comparable) 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 + } +} diff --git a/zc-business/src/main/java/com/zc/business/utils/StakeMarkUtils.java b/zc-business/src/main/java/com/zc/business/utils/StakeMarkUtils.java new file mode 100644 index 00000000..7946b272 --- /dev/null +++ b/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; + } +} +