36 changed files with 3482 additions and 304 deletions
@ -0,0 +1,27 @@ |
|||||
|
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; |
||||
|
|
||||
|
// 定义毫米波雷达最大测量间隔
|
||||
|
public static final int MAX_INTERVAL_MILLIMETER_WAVE_RADAR = 12030; |
||||
|
|
||||
|
/** |
||||
|
* 计算道路长度 |
||||
|
* @return 道路长度(单位:米) |
||||
|
*/ |
||||
|
public static int calculateRoadLength() { |
||||
|
return MAX_STAKE_MARK - MIN_STAKE_MARK; |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,14 +0,0 @@ |
|||||
package com.zc.business.constant; |
|
||||
|
|
||||
/** |
|
||||
* 统计恢复偏移时间类 |
|
||||
* 用于定义与交通数据恢复相关的偏移时间常量。 |
|
||||
* |
|
||||
* @author xiepufeng |
|
||||
*/ |
|
||||
public class StatisticalRecoveryOffsetTime { |
|
||||
|
|
||||
// 定义交通数据段偏移的天数常量,表示偏移-10天
|
|
||||
public static final int TRAFFIC_SECTION_DATA_OFFSET_DAY = -10; |
|
||||
} |
|
||||
|
|
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,69 @@ |
|||||
|
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; |
||||
|
|
||||
|
/** |
||||
|
* 拥堵路段数量 |
||||
|
*/ |
||||
|
private Integer congestedSectionQuantity; |
||||
|
|
||||
|
/** |
||||
|
* 拥堵里程 |
||||
|
*/ |
||||
|
private Integer congestedDistance; |
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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> { |
||||
|
|
||||
|
} |
@ -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; |
||||
|
} |
@ -1,15 +0,0 @@ |
|||||
package com.zc.business.service; |
|
||||
|
|
||||
import com.alibaba.fastjson.JSONObject; |
|
||||
import com.baomidou.mybatisplus.extension.service.IService; |
|
||||
import com.zc.business.domain.DcTrafficSectionData; |
|
||||
|
|
||||
public interface DcTrafficSectionDataService extends IService<DcTrafficSectionData> { |
|
||||
|
|
||||
/** |
|
||||
* 处理实时接收到的一类交流站设备消息,并将其转换为交通断面统计数据对象并缓存。 |
|
||||
* |
|
||||
* @param msg 设备发送的JSON格式实时消息 |
|
||||
*/ |
|
||||
void processRealtimeOneStopMessage(JSONObject msg); |
|
||||
} |
|
@ -0,0 +1,38 @@ |
|||||
|
package com.zc.business.service; |
||||
|
|
||||
|
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> { |
||||
|
|
||||
|
/** |
||||
|
* 处理实时接收到的一类交流站设备消息,并将其转换为交通断面统计数据对象并缓存。 |
||||
|
* |
||||
|
* @param msg 设备发送的JSON格式实时消息 |
||||
|
*/ |
||||
|
void processRealtimeOneStopMessage(JSONObject msg); |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 根据提供的请求参数获取当前的流量指标数据。 |
||||
|
* |
||||
|
* @param request 包含获取流量指标所需的所有请求参数的对象。 |
||||
|
* @return DcTrafficMetricsData 返回一个包含当前流量指标数据的对象。 |
||||
|
* 该对象包含了关于数据中心网络流量的各种度量指标。 |
||||
|
*/ |
||||
|
DcTrafficMetricsData currentTrafficMetrics(DcTrafficMetricsDataRequest request); |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 获取历史流量指标数据列表。 |
||||
|
* |
||||
|
* @param request 包含获取流量指标所需的所有请求参数的对象。 |
||||
|
* @return 返回符合查询条件的历史流量指标数据列表。 |
||||
|
*/ |
||||
|
List<DcTrafficMetricsData> historyTrafficMetrics(DcTrafficMetricsDataRequest request); |
||||
|
} |
@ -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> { |
||||
|
} |
@ -0,0 +1,324 @@ |
|||||
|
package com.zc.business.service.impl; |
||||
|
|
||||
|
import cn.hutool.core.date.DateUtil; |
||||
|
import com.alibaba.fastjson.JSONArray; |
||||
|
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.controller.DcDeviceController; |
||||
|
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.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.stereotype.Service; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.Resource; |
||||
|
import java.io.IOException; |
||||
|
import java.util.*; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* 交通断面数据服务实现类,负责处理实时设备消息、缓存数据、定时任务以及数据保存等功能。 |
||||
|
* |
||||
|
* @author xiepufeng |
||||
|
*/ |
||||
|
@Service |
||||
|
public class DcTrafficStatisticsServiceImpl |
||||
|
extends ServiceImpl<DcTrafficSectionDataMapper, DcTrafficSectionData> |
||||
|
implements DcTrafficStatisticsService { |
||||
|
|
||||
|
// 日志记录器
|
||||
|
protected final Logger logger = LoggerFactory.getLogger(this.getClass()); |
||||
|
|
||||
|
@Resource |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
@Resource |
||||
|
private DcDeviceController dcDeviceController; |
||||
|
|
||||
|
@Resource |
||||
|
private TrafficStatistics trafficStatistics; |
||||
|
|
||||
|
@Resource |
||||
|
private TrafficAnalysis trafficAnalysis; |
||||
|
|
||||
|
/** |
||||
|
* 初始化方法,用于在对象创建后恢复各种周期的交通数据缓存。 |
||||
|
* 该方法标注了@PostConstruct注解,确保在依赖注入完成后调用。 |
||||
|
*/ |
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
recoveryDailyCache(); // 从es中恢复当天交通数据缓存
|
||||
|
recoveryMonthlyCache(); // 恢复每月交通数据缓存
|
||||
|
recoveryQuarterlyCache(); // 恢复每季度交通数据缓存
|
||||
|
recoveryYearlyCache(); // 恢复每年交通数据缓存
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理实时接收到的一类交流站设备消息,并将其转换为交通断面统计数据对象并缓存。 |
||||
|
* |
||||
|
* @param msg 设备发送的JSON格式实时消息 |
||||
|
*/ |
||||
|
@Override |
||||
|
public void processRealtimeOneStopMessage(JSONObject msg) { |
||||
|
|
||||
|
// 1. 将设备消息转换为交通断面数据统计定义对象
|
||||
|
List<DcTrafficSectionData> dcTrafficSectionDataList = trafficStatistics.convertToTrafficStatistics(msg, DeviceDataCategoryEnum.REAL_TIME); |
||||
|
|
||||
|
if (dcTrafficSectionDataList != null && !dcTrafficSectionDataList.isEmpty()) { |
||||
|
// 2. 添加到缓存中
|
||||
|
dcTrafficSectionDataList.forEach(this::addCacheData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据请求获取当前交通流量指标数据。 |
||||
|
* |
||||
|
* @param request 包含获取流量指标所需的所有请求参数的对象。 |
||||
|
* @return 返回当前交通流量指标数据。 |
||||
|
* @throws ServiceException 如果没有获取到交通数据,则抛出异常。 |
||||
|
*/ |
||||
|
@Override |
||||
|
public DcTrafficMetricsData currentTrafficMetrics(DcTrafficMetricsDataRequest request) { |
||||
|
|
||||
|
// 从Redis缓存中获取指定方向的交通路段数据列表
|
||||
|
List<DcTrafficSectionData> dcTrafficSectionDataCaches = getDcTrafficSectionDataRedisCache(request.getDirection()); |
||||
|
|
||||
|
// 根据指定的方向和路段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); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 计算获取指定条件下的历史交通流量指标数据。 |
||||
|
* |
||||
|
* @param request 包含查询条件的请求对象,包括开始时间、结束时间、方向、周期类型和所属路段ID等信息。 |
||||
|
* @return 返回一个包含交通流量指标数据的列表。 |
||||
|
*/ |
||||
|
@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)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 恢复每日缓存的函数。 |
||||
|
* 该方法尝试从物联平台获取所有设备信息,并对这些信息进行处理。 |
||||
|
* 如果获取信息失败或处理过程中发生异常,则记录错误信息。 |
||||
|
*/ |
||||
|
private void recoveryDailyCache() { |
||||
|
|
||||
|
try { |
||||
|
// 尝试从指定产品ID获取设备信息
|
||||
|
Map<String, Object> oneStopDeviceMap = dcDeviceController.getDeviceByProductId(IotProductEnum.ONE_STOP_PRODUCT.value()); |
||||
|
|
||||
|
// 检查获取的设备信息是否为空
|
||||
|
if (oneStopDeviceMap == null || oneStopDeviceMap.get("data") == null) { |
||||
|
logger.error("获取一类交通量调查站设备数据失败,产品id:{}", IotProductEnum.ONE_STOP_PRODUCT.value()); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 将获取的设备信息转换为JSON数组,并遍历处理每个设备的数据
|
||||
|
JSONArray deviceJsonArray = JSONArray.parseArray(oneStopDeviceMap.get("data").toString()); |
||||
|
deviceJsonArray.forEach(trafficStatistics::processDeviceData); |
||||
|
|
||||
|
} catch (HttpException | IOException e) { |
||||
|
// 记录处理设备数据时发生的异常
|
||||
|
logger.error("处理设备数据时发生异常", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复每月交通数据缓存的方法。 |
||||
|
* 通过查询当前月份至今的每日交通数据,并将其添加到每月交通统计缓存中。 |
||||
|
*/ |
||||
|
private void recoveryMonthlyCache() { |
||||
|
// 构建查询条件,查询当前月份至今的每日交通数据
|
||||
|
LambdaQueryWrapper<DcTrafficSectionData> queryWrapper = new LambdaQueryWrapper<>(); |
||||
|
queryWrapper.eq(DcTrafficSectionData::getPeriodType, TrafficDataPeriodTypeEnum.DAY); |
||||
|
queryWrapper.between(DcTrafficSectionData::getStatisticalDate, DateUtil.beginOfMonth(new Date()), new Date()); |
||||
|
List<DcTrafficSectionData> dcTrafficSectionDataList = this.list(queryWrapper); |
||||
|
// 遍历查询结果,将每日数据添加到每月交通统计缓存
|
||||
|
dcTrafficSectionDataList.forEach(MonthlyTrafficStatisticsCache::addCacheData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复每季度交通数据缓存的方法。 |
||||
|
* 通过查询当前季度至今的每月交通数据,并将其添加到每季度交通统计缓存中。 |
||||
|
*/ |
||||
|
private void recoveryQuarterlyCache() { |
||||
|
// 构建查询条件,查询当前季度至今的每月交通数据
|
||||
|
LambdaQueryWrapper<DcTrafficSectionData> queryWrapper = new LambdaQueryWrapper<>(); |
||||
|
queryWrapper.eq(DcTrafficSectionData::getPeriodType, TrafficDataPeriodTypeEnum.MONTH); |
||||
|
queryWrapper.between(DcTrafficSectionData::getStatisticalDate, DateUtil.beginOfQuarter(new Date()), new Date()); |
||||
|
List<DcTrafficSectionData> dcTrafficSectionDataList = this.list(queryWrapper); |
||||
|
// 遍历查询结果,将每月数据添加到每季度交通统计缓存
|
||||
|
dcTrafficSectionDataList.forEach(QuarterlyTrafficStatisticsCache::addCacheData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复每年交通数据缓存的方法。 |
||||
|
* 通过查询当前年份至今的每季度交通数据,并将其添加到每年交通统计缓存中。 |
||||
|
*/ |
||||
|
private void recoveryYearlyCache() { |
||||
|
// 构建查询条件,查询当前年份至今的每季度交通数据
|
||||
|
LambdaQueryWrapper<DcTrafficSectionData> queryWrapper = new LambdaQueryWrapper<>(); |
||||
|
queryWrapper.eq(DcTrafficSectionData::getPeriodType, TrafficDataPeriodTypeEnum.QUARTER); |
||||
|
queryWrapper.between(DcTrafficSectionData::getStatisticalDate, DateUtil.beginOfYear(new Date()), new Date()); |
||||
|
List<DcTrafficSectionData> dcTrafficSectionDataList = this.list(queryWrapper); |
||||
|
// 遍历查询结果,将每季度数据添加到每年交通统计缓存
|
||||
|
dcTrafficSectionDataList.forEach(YearlyTrafficStatisticsCache::addCacheData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将交通数据添加到缓存中。 |
||||
|
* 该方法将交通数据既添加到日交通数据缓存中,也实时缓存到Redis中。 |
||||
|
* |
||||
|
* @param dcTrafficSectionData 交通段数据对象,包含交通数据的详细信息。 |
||||
|
*/ |
||||
|
private void addCacheData(DcTrafficSectionData dcTrafficSectionData) { |
||||
|
// 添加到日交通数据缓存中
|
||||
|
DailyTrafficStatisticsCache.addCacheData(dcTrafficSectionData); |
||||
|
|
||||
|
// 将数据缓存到redis中
|
||||
|
redisCache.setCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(dcTrafficSectionData.getDirection()), |
||||
|
dcTrafficSectionData.getDeviceId(), |
||||
|
dcTrafficSectionData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 从Redis缓存中获取指定方向的交通路段数据列表。 |
||||
|
* |
||||
|
* @param direction 交通方向,如果为null,则获取双向数据 |
||||
|
* @return DcTrafficSectionData列表,确保列表非空 |
||||
|
*/ |
||||
|
public List<DcTrafficSectionData> getDcTrafficSectionDataRedisCache(Byte direction) { |
||||
|
|
||||
|
Map<String, DcTrafficSectionData> dcTrafficSectionDataMap = new HashMap<>(); |
||||
|
|
||||
|
// 根据方向选择相应的交通数据
|
||||
|
Map<String, DcTrafficSectionData> trafficDataMap = (direction != null) |
||||
|
? getTrafficDataByDirection(direction) |
||||
|
: getTrafficDataForBothDirections(); |
||||
|
|
||||
|
// 将获取的交通数据合并到结果映射中
|
||||
|
if (trafficDataMap != null) { |
||||
|
dcTrafficSectionDataMap.putAll(trafficDataMap); |
||||
|
} |
||||
|
|
||||
|
// 将结果映射的值转换为列表并返回,确保列表非空
|
||||
|
return new ArrayList<>(dcTrafficSectionDataMap.values()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据方向获取交通流量数据 |
||||
|
* |
||||
|
* @param direction 方向 |
||||
|
* @return 单向交通流量数据 |
||||
|
*/ |
||||
|
private Map<String, DcTrafficSectionData> getTrafficDataByDirection(Byte direction) { |
||||
|
return redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(direction)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取上行和下行所有交通流量数据 |
||||
|
* |
||||
|
* @return 所有交通流量数据 |
||||
|
*/ |
||||
|
private Map<String, DcTrafficSectionData> getTrafficDataForBothDirections() { |
||||
|
Map<String, DcTrafficSectionData> allData = new HashMap<>(); |
||||
|
allData.putAll(redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(LaneDirectionEnum.UPWARD.getValue()))); |
||||
|
allData.putAll(redisCache.getCacheMapValue(RedisKeyConstants.getDcDevicesTrafficStatisticsKey(LaneDirectionEnum.DOWNWARD.getValue()))); |
||||
|
return allData; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据指定的方向和路段ID范围获取交通数据。 |
||||
|
* |
||||
|
* @param roadSectionId 指定的路段ID,如果为null,则不根据路段筛选数据。 |
||||
|
* @return 返回符合指定方向和路段范围的交通段数据列表。 |
||||
|
*/ |
||||
|
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()); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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 { |
||||
|
|
||||
|
} |
@ -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; |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,635 @@ |
|||||
|
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)); |
||||
|
// 计算路网整体拥堵的方法
|
||||
|
DcTrafficMetricsData trafficMetricsData = calculateRoadNetworkCongestion(sectionDataList); |
||||
|
// 拥堵路段数量
|
||||
|
metricsData.setCongestedSectionQuantity(trafficMetricsData.getCongestedSectionQuantity()); |
||||
|
// 返回拥堵里程
|
||||
|
metricsData.setCongestedDistance(trafficMetricsData.getCongestedDistance()); |
||||
|
// 计算路网拥堵指数
|
||||
|
metricsData.setRoadNetworkCongestionLevel(trafficMetricsData.getRoadNetworkCongestionLevel()); |
||||
|
|
||||
|
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 DcTrafficMetricsData 包含路网整体拥堵指数、拥堵路段数量和拥堵里程的数据对象。 |
||||
|
*/ |
||||
|
public DcTrafficMetricsData calculateRoadNetworkCongestion(List<DcTrafficSectionData> trafficSectionDataList) { |
||||
|
|
||||
|
DcTrafficMetricsData metricsData = new DcTrafficMetricsData(); |
||||
|
|
||||
|
// 如果输入的交通路段数据为空或为空列表,直接返回一个不含数据的metricsData对象
|
||||
|
if (trafficSectionDataList == null || trafficSectionDataList.isEmpty()) { |
||||
|
return metricsData; |
||||
|
} |
||||
|
|
||||
|
// 根据行驶方向对交通数据进行分组,以便分别计算每个方向的拥堵情况
|
||||
|
Map<Byte, List<DcTrafficSectionData>> groupedByDirection = trafficSectionDataList.stream() |
||||
|
.collect(Collectors.groupingBy(DcTrafficSectionData::getDirection)); |
||||
|
|
||||
|
// 总里程,基于道路方向的数量乘以单方向的总里程
|
||||
|
int totalDistance = StakeMarkConstant.calculateRoadLength() * groupedByDirection.size(); |
||||
|
// 总拥堵里程,使用AtomicInteger以支持在并发环境中进行累加操作
|
||||
|
AtomicInteger totalCongestionDistance = new AtomicInteger(); |
||||
|
// 拥堵路段数量
|
||||
|
AtomicInteger congestedSectionQuantity = new AtomicInteger(); |
||||
|
|
||||
|
// 遍历每个方向的数据,计算总拥堵里程
|
||||
|
groupedByDirection.forEach((directionData, trafficSectionList) -> { |
||||
|
|
||||
|
List<DcTrafficSectionData> sortedList; |
||||
|
|
||||
|
// 根据行驶方向,对交通路段数据进行排序,上行方向逆序,下行方向正序
|
||||
|
if (directionData.equals(LaneDirectionEnum.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); |
||||
|
if (congestionDistance > StakeMarkConstant.MAX_INTERVAL_MILLIMETER_WAVE_RADAR) { |
||||
|
totalCongestionDistance.addAndGet(defaultCongestionDistance); |
||||
|
} else { |
||||
|
totalCongestionDistance.addAndGet(congestionDistance); |
||||
|
congestedSectionQuantity.addAndGet(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 更新辅助变量以备后续计算
|
||||
|
previousStakeMark = stakeMark; |
||||
|
previousAverageSpeed = averageSpeed; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 计算并返回路网整体的拥堵指数
|
||||
|
metricsData.setRoadNetworkCongestionLevel(Math.round((float) totalCongestionDistance.get() / totalDistance * 100)); |
||||
|
// 返回拥堵路段数量
|
||||
|
metricsData.setCongestedSectionQuantity(congestedSectionQuantity.get()); |
||||
|
// 返回拥堵里程
|
||||
|
metricsData.setCongestedDistance(totalCongestionDistance.get()); |
||||
|
|
||||
|
return metricsData; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 计算路段交通量 |
||||
|
* |
||||
|
* @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; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
@ -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
|
||||
|
} |
||||
|
} |
@ -0,0 +1,369 @@ |
|||||
|
package com.zc.business.utils; |
||||
|
|
||||
|
import com.aliyuncs.utils.IOUtils; |
||||
|
import com.google.common.base.Strings; |
||||
|
import org.apache.poi.openxml4j.opc.OPCPackage; |
||||
|
import org.apache.poi.ss.util.CellRangeAddress; |
||||
|
import org.apache.poi.ss.util.CellReference; |
||||
|
import org.apache.poi.xddf.usermodel.chart.XDDFChartData; |
||||
|
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource; |
||||
|
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory; |
||||
|
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource; |
||||
|
import org.apache.poi.xssf.usermodel.XSSFSheet; |
||||
|
import org.apache.poi.xwpf.usermodel.*; |
||||
|
import org.apache.xmlbeans.XmlOptions; |
||||
|
import org.openxmlformats.schemas.drawingml.x2006.chart.CTPlotArea; |
||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
|
||||
|
import java.io.*; |
||||
|
import java.math.BigInteger; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : LCheng |
||||
|
* @date : 2020-12-10 10:03 |
||||
|
* description : poi工具 |
||||
|
*/ |
||||
|
public class PoiUtil { |
||||
|
|
||||
|
public static int headingCount1 = 1; |
||||
|
public static int headingCount2 = 1; |
||||
|
|
||||
|
/** |
||||
|
* 根据word模板导出 针对图表(柱状图,折线图,饼图等)的处理 |
||||
|
* |
||||
|
* @param docChart 图表对象 |
||||
|
* @param title 图表标题 |
||||
|
* @param seriesNames 系列名称数组 |
||||
|
* @return {@link XWPFChart} |
||||
|
* @author LCheng |
||||
|
* @date 2020/12/10 11:08 |
||||
|
*/ |
||||
|
public static XWPFChart wordExportChar(XWPFChart docChart, String title, String[] seriesNames, XSSFSheet sheet) { |
||||
|
//获取图表数据对象
|
||||
|
XDDFChartData chartData = docChart.getChartSeries().get(0); |
||||
|
|
||||
|
//word图表均对应一个内置的excel,用于保存图表对应的数据
|
||||
|
//excel中 第一列第二行开始的数据为分类信息
|
||||
|
//CellRangeAddress(1, categories.size(), 0, 0) 四个参数依次为 起始行 截止行 起始列 截止列。
|
||||
|
//根据分类信息的范围创建分类信息的数据源
|
||||
|
XDDFDataSource catDataSource = XDDFDataSourcesFactory.fromStringCellRange(sheet, new CellRangeAddress(1,sheet.getLastRowNum(),0,0)); |
||||
|
//更新数据
|
||||
|
for (int i = 0; i < seriesNames.length; i++) { |
||||
|
//excel中各系列对应的数据的范围
|
||||
|
//根据数据的范围创建值的数据源
|
||||
|
XDDFNumericalDataSource<Double> valDataSource = XDDFDataSourcesFactory.fromNumericCellRange(sheet, new CellRangeAddress(1,sheet.getLastRowNum(),i+1,i+1)); |
||||
|
//获取图表系列的数据对象
|
||||
|
XDDFChartData.Series series = chartData.getSeries().get(i); |
||||
|
//替换系列数据对象中的分类和值
|
||||
|
series.replaceData(catDataSource, valDataSource); |
||||
|
//修改系列数据对象中的标题
|
||||
|
CellReference cellReference = docChart.setSheetTitle(seriesNames[i], 1); |
||||
|
series.setTitle(seriesNames[i], cellReference); |
||||
|
} |
||||
|
//更新图表数据对象
|
||||
|
docChart.plot(chartData); |
||||
|
//图表整体的标题 传空值则不替换标题
|
||||
|
if (!Strings.isNullOrEmpty(title)) { |
||||
|
docChart.setTitleText(title); |
||||
|
docChart.setTitleOverlay(false); |
||||
|
} |
||||
|
return docChart; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 合并docx文件 |
||||
|
* @param srcDocxs 需要合并的目标docx文件 |
||||
|
* @param destDocx 合并后的docx输出文件 |
||||
|
*/ |
||||
|
public static void mergeDoc(XWPFDocument srcDocxs, XWPFDocument destDocx) { |
||||
|
|
||||
|
|
||||
|
try { |
||||
|
|
||||
|
//获取目标文件的CTDocument1对象
|
||||
|
CTDocument1 ctDocument1 = srcDocxs.getDocument(); |
||||
|
//获取第一个目标文件的CTBody对象
|
||||
|
CTBody src1Body = ctDocument1.getBody(); |
||||
|
|
||||
|
|
||||
|
//获取目标文件中的图表
|
||||
|
List<XWPFChart> relations = srcDocxs.getCharts(); |
||||
|
//判断是否有图表,没有图表的话,追加到之前的目标文件后面
|
||||
|
if (relations.size() <= 0) { |
||||
|
CTBody src2Body = srcDocxs.getDocument().getBody(); |
||||
|
//获取目标文件中的图片
|
||||
|
List<XWPFPictureData> allPictures = srcDocxs.getAllPictures(); |
||||
|
// 记录图片合并前及合并后的ID
|
||||
|
Map<String,String> map = new HashMap(); |
||||
|
//遍历图片
|
||||
|
for (XWPFPictureData picture : allPictures) { |
||||
|
String before = srcDocxs.getRelationId(picture); |
||||
|
//将原文档中的图片加入到目标文档中
|
||||
|
String after = destDocx.addPictureData(picture.getData(), Document.PICTURE_TYPE_PNG); |
||||
|
map.put(before, after); |
||||
|
} |
||||
|
//将当前文件的内容追加到之前的目标文件中
|
||||
|
appendBody(src1Body, src2Body,map); |
||||
|
} |
||||
|
//遍历图表,
|
||||
|
for (XWPFChart chart1 : relations) { |
||||
|
//是否是word中自带图表
|
||||
|
if (chart1 instanceof XWPFChart) { // 如果是图表元素
|
||||
|
XWPFChart chart = destDocx.createChart(5774310, 3076575); |
||||
|
CTPlotArea plotArea = chart1.getCTChart().getPlotArea(); |
||||
|
chart.getCTChart().setPlotArea(plotArea); |
||||
|
chart.getCTChart().setLegend(chart1.getCTChart().getLegend()); |
||||
|
} |
||||
|
} |
||||
|
//关闭流
|
||||
|
srcDocxs.close(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 合并chart |
||||
|
* @param chart 需要合并的目标chart |
||||
|
* @param destDocx 合并后的docx输出文件 |
||||
|
*/ |
||||
|
public static void mergeChart(XWPFChart chart, XWPFDocument destDocx) { |
||||
|
try { |
||||
|
|
||||
|
XWPFChart docxChart = destDocx.createChart(5774310, 3076575); |
||||
|
CTPlotArea plotArea = chart.getCTChart().getPlotArea(); |
||||
|
docxChart.getCTChart().setPlotArea(plotArea); |
||||
|
docxChart.getCTChart().setLegend(chart.getCTChart().getLegend()); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 插入换行符 |
||||
|
* @param destDocx 合并后的docx输出文件 |
||||
|
*/ |
||||
|
public static void createLineBreak(XWPFDocument destDocx) { |
||||
|
try { |
||||
|
|
||||
|
XWPFParagraph paragraph = destDocx.createParagraph(); |
||||
|
XWPFRun run = paragraph.createRun(); |
||||
|
run.addBreak(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 合并文档内容 |
||||
|
* |
||||
|
* @param src 目标文档 |
||||
|
* @param append 要合并的文档 |
||||
|
* @throws Exception |
||||
|
*/ |
||||
|
private static void appendBody(CTBody src, CTBody append,Map<String,String> map) throws Exception { |
||||
|
XmlOptions optionsOuter = new XmlOptions(); |
||||
|
optionsOuter.setSaveOuter(); |
||||
|
//获取目标文件的字符内容
|
||||
|
String srcString = src.xmlText(); |
||||
|
//获取目标文件字符的开头
|
||||
|
String prefix = srcString.substring(0, srcString.indexOf(">") + 1); |
||||
|
//获取目标文件字符的内容
|
||||
|
String mainPart = srcString.substring(srcString.indexOf(">") + 1, |
||||
|
srcString.lastIndexOf("<")); |
||||
|
//获取目标文件字符的结尾
|
||||
|
String sufix = srcString.substring(srcString.lastIndexOf("<")); |
||||
|
//获取需要追加的文件
|
||||
|
String appendString = append.xmlText(optionsOuter); |
||||
|
//获取需要追加的文件内容(除去头和尾)
|
||||
|
String addPart = appendString.substring(appendString.indexOf(">") + 1, |
||||
|
appendString.lastIndexOf("<")); |
||||
|
if (map != null && !map.isEmpty()) { |
||||
|
//对xml字符串中图片ID进行替换
|
||||
|
for (Map.Entry<String, String> set : map.entrySet()) { |
||||
|
addPart = addPart.replace(set.getKey(), set.getValue()); |
||||
|
} |
||||
|
} |
||||
|
//将获取到的文件内容合并成为新的CTBody
|
||||
|
CTBody makeBody = CTBody.Factory.parse(prefix + mainPart + addPart |
||||
|
+ sufix); |
||||
|
//将新的CTBody重新设置到目标文件中
|
||||
|
src.set(makeBody); |
||||
|
} |
||||
|
|
||||
|
public static XWPFParagraph createHeading(XWPFDocument doc, String title) { |
||||
|
//段落
|
||||
|
XWPFParagraph paragraph = doc.createParagraph(); |
||||
|
XWPFRun run = paragraph.createRun(); |
||||
|
run.setText(title); |
||||
|
// run.setColor("696969");
|
||||
|
run.setFontSize(18); |
||||
|
run.setBold(true);//标题加粗
|
||||
|
return paragraph; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建标题1 |
||||
|
* |
||||
|
* @param doc |
||||
|
* @param title |
||||
|
*/ |
||||
|
public static void createHeading1(XWPFDocument doc, String title) { |
||||
|
//段落
|
||||
|
XWPFParagraph paragraph = doc.createParagraph(); |
||||
|
XWPFRun run = paragraph.createRun(); |
||||
|
run.setText(title); |
||||
|
// run.setColor("696969");
|
||||
|
run.setFontSize(16); |
||||
|
run.setBold(true);//标题加粗
|
||||
|
paragraph.setStyle("Heading1"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建标题2 |
||||
|
* |
||||
|
* @param doc |
||||
|
* @param title |
||||
|
*/ |
||||
|
public static void createHeading2(XWPFDocument doc, String title) { |
||||
|
XWPFParagraph paragraph = doc.createParagraph(); |
||||
|
XWPFRun run = paragraph.createRun(); |
||||
|
run.setText(title); |
||||
|
run.setFontSize(14); |
||||
|
run.setBold(true);//标题加粗
|
||||
|
paragraph.setStyle("Heading2"); |
||||
|
} |
||||
|
|
||||
|
public static void createTable(XWPFDocument doc) { |
||||
|
XWPFTable table = doc.createTable(3, 3); |
||||
|
//列宽自动分割
|
||||
|
CTTblWidth infoTableWidth = table.getCTTbl().addNewTblPr().addNewTblW(); |
||||
|
infoTableWidth.setType(STTblWidth.DXA); |
||||
|
infoTableWidth.setW(BigInteger.valueOf(9072)); |
||||
|
|
||||
|
setTableFonts(table.getRow(0).getCell(0), "编号"); |
||||
|
setTableFonts(table.getRow(0).getCell(1), "问题"); |
||||
|
setTableFonts(table.getRow(0).getCell(2), "应答"); |
||||
|
setTableFonts(table.getRow(1).getCell(0), "1"); |
||||
|
setTableFonts(table.getRow(1).getCell(1), "陈述日期"); |
||||
|
setTableFonts(table.getRow(1).getCell(2), "2017年02月17日"); |
||||
|
setTableFonts(table.getRow(2).getCell(0), "2"); |
||||
|
setTableFonts(table.getRow(2).getCell(1), "PICS序列号"); |
||||
|
setTableFonts(table.getRow(2).getCell(2), "121313132131"); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// word跨列合并单元格
|
||||
|
public static void mergeCellsHorizontal(XWPFTable table, int row, int fromCell, int toCell) { |
||||
|
for (int cellIndex = fromCell; cellIndex <= toCell; cellIndex++) { |
||||
|
XWPFTableCell cell = table.getRow(row).getCell(cellIndex); |
||||
|
if (cellIndex == fromCell) { |
||||
|
// The first merged cell is set with RESTART merge value
|
||||
|
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.RESTART); |
||||
|
} else { |
||||
|
// Cells which join (merge) the first one, are set with CONTINUE
|
||||
|
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.CONTINUE); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// word跨行并单元格
|
||||
|
public static void mergeCellsVertically(XWPFTable table, int col, int fromRow, int toRow) { |
||||
|
for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++) { |
||||
|
XWPFTableCell cell = table.getRow(rowIndex).getCell(col); |
||||
|
if (rowIndex == fromRow) { |
||||
|
// The first merged cell is set with RESTART merge value
|
||||
|
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.RESTART); |
||||
|
} else { |
||||
|
// Cells which join (merge) the first one, are set with CONTINUE
|
||||
|
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.CONTINUE); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置表格中字体 |
||||
|
* |
||||
|
* @param cell |
||||
|
* @param cellText |
||||
|
*/ |
||||
|
public static void setTableFonts(XWPFTableCell cell, String cellText) { |
||||
|
CTP ctp = CTP.Factory.newInstance(); |
||||
|
XWPFParagraph p = new XWPFParagraph(ctp, cell); |
||||
|
p.setAlignment(ParagraphAlignment.CENTER); |
||||
|
XWPFRun run = p.createRun(); |
||||
|
run.setFontSize(8); |
||||
|
run.setText(cellText); |
||||
|
CTRPr rpr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr(); |
||||
|
CTFonts fonts = rpr.isSetRFonts() ? rpr.getRFonts() : rpr.addNewRFonts(); |
||||
|
fonts.setAscii("仿宋"); |
||||
|
fonts.setEastAsia("仿宋"); |
||||
|
fonts.setHAnsi("仿宋"); |
||||
|
cell.setParagraph(p); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加描述信息 |
||||
|
* |
||||
|
* @param doc |
||||
|
* @param description |
||||
|
*/ |
||||
|
public static void addDescription(XWPFDocument doc, String description) { |
||||
|
if (StringUtils.isEmpty(description)) { |
||||
|
return; |
||||
|
} |
||||
|
XWPFParagraph title = doc.createParagraph(); |
||||
|
XWPFRun run = title.createRun(); |
||||
|
run.setText(description); |
||||
|
run.setBold(true); |
||||
|
title.setAlignment(ParagraphAlignment.CENTER); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建目录 |
||||
|
* 创建并插入带超链接的目录 |
||||
|
* @param document |
||||
|
*/ |
||||
|
public static void insertTOC2(XWPFDocument document) { |
||||
|
// 定义 TOC 字段属性
|
||||
|
CTSimpleField tocField = CTSimpleField.Factory.newInstance(); |
||||
|
tocField.setInstr("TOC \\h \\z \\t \"Heading1,Heading2\""); // 包含 Heading1 和 Heading2 样式的目录
|
||||
|
|
||||
|
// 创建包含 TOC 字段的段落
|
||||
|
XWPFParagraph tocPara = document.createParagraph(); |
||||
|
tocPara.getCTP().addNewFldSimple().set(tocField); |
||||
|
|
||||
|
// 更新文档字段以计算目录
|
||||
|
document.enforceUpdateFields(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建目录 |
||||
|
* 创建并插入带超链接的目录 |
||||
|
* @param document |
||||
|
*/ |
||||
|
public static void insertTOC(XWPFDocument document) { |
||||
|
// 创建目录所在的段落
|
||||
|
XWPFParagraph tocPara = document.createParagraph(); |
||||
|
|
||||
|
|
||||
|
// 添加 TOC 域代码
|
||||
|
String tocFieldCode = "TOC \\o \"1-3\" \\h \\z \\u"; |
||||
|
CTSimpleField tocField = tocPara.getCTP().addNewFldSimple(); |
||||
|
tocField.setInstr(tocFieldCode); |
||||
|
tocField.setDirty(STOnOff.TRUE); |
||||
|
} |
||||
|
} |
||||
|
|
@ -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; |
||||
|
} |
||||
|
} |
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue