| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import com.trafficaudit.observation.dto.ObservationDailyOperation; |
| | | import com.trafficaudit.observation.mapper.ObservationStationFlowMapper; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.Function; |
| | | |
| | | @Service |
| | | public class HolidayTrendService { |
| | | |
| | | private static final BigDecimal HUNDRED = new BigDecimal("100"); |
| | | |
| | | @Value("${holiday.trend-template:docs/专项统计/重要节假日/2026年十一假期全省综合运输客流量走势图.docx}") |
| | | private String trendTemplatePath; |
| | | |
| | | @Resource |
| | | private HolidayStatService statService; |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayTrendChartService chartService; |
| | | @Resource |
| | | private HolidayTrendWordService wordService; |
| | | @Resource |
| | | private ObservationStationFlowMapper observationStationFlowMapper; |
| | | |
| | | public byte[] generate(int year, String holidayType) { |
| | | String holidayLabel = HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType); |
| | | if (holidayLabel == null) { |
| | | throw new IllegalArgumentException("不支持的节假日类型"); |
| | | } |
| | | List<Integer> years = Arrays.asList(year, year - 1, year - 2); |
| | | Map<Integer, YearData> yearData = new LinkedHashMap<>(); |
| | | for (Integer item : years) { |
| | | yearData.put(item, buildYearData(item, holidayType)); |
| | | } |
| | | YearData selected = yearData.get(year); |
| | | if (selected == null || !selected.hasAnyData()) { |
| | | throw new IllegalArgumentException("所选年份没有足够的走势图数据"); |
| | | } |
| | | |
| | | String prefix = "“" + holidayLabel + "”假期"; |
| | | List<HolidayTrendWordService.DocumentBlock> blocks = new ArrayList<>(); |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("一、总体情况")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "全省跨区域人员流动量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.totalDaily), false, 1100, 715), |
| | | lineImageSize(5268595L, 3424555L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期累计跨区域人员流动量走势", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.totalCumulative), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期各交通运输方式客运量占比情况", |
| | | chartService.pieChart("人次", selected.modalShare(), 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("二、各种运输方式客流量走势")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期自驾出行量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.selfDrivingPersonFlow), false, 1100, 522), |
| | | lineImageSize(5232400L, 2484120L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期铁路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.rail), false, 1100, 445), |
| | | lineImageSize(5264785L, 2129155L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期水路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.water), false, 1100, 433), |
| | | lineImageSize(5274310L, 2075815L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期民航客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.civilAviation), false, 1100, 521), |
| | | lineImageSize(5271135L, 2496185L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期道路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.road), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("三、车流量走势")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期高速公路车流量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.expressway), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期普通国省道车流量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.nationalProvincial), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期全省车流总量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.totalVehicleFlow), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期公路出行结构", |
| | | chartService.stackedBarChart("万人次", |
| | | Arrays.asList((year - 2) + "年", (year - 1) + "年", year + "年"), |
| | | roadStructureSeries(years, yearData), false, 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("附录一 每日客流构成变化")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期每日客流构成变化", |
| | | chartService.stackedBarChart("占比", |
| | | HolidayTrendChartService.DAY_LABELS, |
| | | dailyShareSeries(selected), true, 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("附录二 普通国省道观测站路网运行")); |
| | | ObservationData observation = observationData(year, holidayType); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "普通国省道观测站平均机动车速度", |
| | | chartService.lineChart("公里/小时", |
| | | Collections.singletonList(series("平均速度", observation.speed)), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "普通国省道观测站平均拥挤度", |
| | | chartService.lineChart("拥挤度", |
| | | Collections.singletonList(series("平均拥挤度", observation.congestion)), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | | throw new IllegalArgumentException("走势图模板文件不存在: " + template.getAbsolutePath()); |
| | | } |
| | | try (InputStream input = new FileInputStream(template)) { |
| | | return wordService.generate(readAll(input), year, blocks); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("走势图生成失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private YearData buildYearData(int year, String holidayType) { |
| | | List<DailyMetrics> metrics = new ArrayList<>(); |
| | | for (int day = 1; day <= HolidayTrendChartService.DAY_LABELS.size(); day++) { |
| | | metrics.add(statService.dailyMetrics(year, holidayType, day)); |
| | | } |
| | | YearData data = new YearData(); |
| | | data.year = year; |
| | | data.rail = values(metrics, DailyMetrics::getRail); |
| | | data.civilAviation = values(metrics, DailyMetrics::getCivilAviation); |
| | | data.road = values(metrics, DailyMetrics::getRoad); |
| | | data.water = values(metrics, DailyMetrics::getWater); |
| | | data.expressway = values(metrics, DailyMetrics::getExpressway); |
| | | data.nationalProvincial = values(metrics, DailyMetrics::getNationalProvincial); |
| | | data.selfDrivingPersonFlow = values(metrics, DailyMetrics::getNonBusinessPersonFlow); |
| | | data.selfDrivingVehicleFlow = combine(metrics, |
| | | DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial); |
| | | data.totalVehicleFlow = combine(metrics, |
| | | DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial); |
| | | data.totalDaily = values(metrics, DailyMetrics::getTotalPersonFlow); |
| | | data.totalCumulative = cumulative(data.totalDaily); |
| | | data.railCumulative = cumulative(data.rail); |
| | | data.civilAviationCumulative = cumulative(data.civilAviation); |
| | | data.roadCumulative = cumulative(data.road); |
| | | data.waterCumulative = cumulative(data.water); |
| | | data.selfDrivingPersonFlowCumulative = cumulative(data.selfDrivingPersonFlow); |
| | | return data; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> series( |
| | | List<Integer> years, Map<Integer, YearData> data, |
| | | Function<YearData, List<BigDecimal>> selector) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | for (Integer year : years) { |
| | | YearData item = data.get(year); |
| | | result.add(series(year + "年", selector.apply(item))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> yoySeries( |
| | | String label, List<Integer> years, Map<Integer, YearData> data, |
| | | Function<YearData, List<BigDecimal>> selector) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | YearData selected = data.get(years.get(0)); |
| | | List<BigDecimal> selectedValues = selector.apply(selected); |
| | | for (int i = 1; i < years.size(); i++) { |
| | | YearData comparison = data.get(years.get(i)); |
| | | result.add(series(label + "同比" + comparison.year, |
| | | percentageChanges(selectedValues, selector.apply(comparison)))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> dailyShareSeries(YearData data) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | result.add(series("自驾", share(data.selfDrivingPersonFlow, data))); |
| | | result.add(series("道路", share(data.road, data))); |
| | | result.add(series("铁路", share(data.rail, data))); |
| | | result.add(series("民航", share(data.civilAviation, data))); |
| | | result.add(series("水路", share(data.water, data))); |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> share(List<BigDecimal> values, YearData data) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | BigDecimal total = dayTotal(data, i); |
| | | BigDecimal value = valueAt(values, i); |
| | | result.add(total == null || total.signum() == 0 |
| | | ? null : HolidayNumberUtils.divide(value == null ? BigDecimal.ZERO : value, total)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal dayTotal(YearData data, int index) { |
| | | BigDecimal total = BigDecimal.ZERO; |
| | | boolean hasValue = false; |
| | | for (List<BigDecimal> values : Arrays.asList( |
| | | data.selfDrivingPersonFlow, data.road, data.rail, |
| | | data.civilAviation, data.water)) { |
| | | BigDecimal value = valueAt(values, index); |
| | | if (value != null) { |
| | | total = total.add(value); |
| | | hasValue = true; |
| | | } |
| | | } |
| | | return hasValue ? HolidayNumberUtils.normalize(total) : null; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> roadStructureSeries( |
| | | List<Integer> years, Map<Integer, YearData> data) { |
| | | List<BigDecimal> road = new ArrayList<>(); |
| | | List<BigDecimal> selfDriving = new ArrayList<>(); |
| | | for (int i = years.size() - 1; i >= 0; i--) { |
| | | YearData item = data.get(years.get(i)); |
| | | road.add(sum(item.road)); |
| | | selfDriving.add(sum(item.selfDrivingPersonFlow)); |
| | | } |
| | | return Arrays.asList( |
| | | series("自驾出行量", selfDriving), |
| | | series("道路客运量", road)); |
| | | } |
| | | |
| | | private ObservationData observationData(int year, String holidayType) { |
| | | List<HolidayCalendar> days = configService.findHolidayDays(year, holidayType); |
| | | if (days.isEmpty()) { |
| | | return new ObservationData(nulls(), nulls()); |
| | | } |
| | | LocalDate start = days.get(0).getHolidayDate(); |
| | | LocalDate end = start.plusDays(HolidayTrendChartService.DAY_LABELS.size() - 1L); |
| | | List<ObservationDailyOperation> rows = observationStationFlowMapper.selectDailyOperations(start, end); |
| | | Map<LocalDate, ObservationDailyOperation> byDate = new LinkedHashMap<>(); |
| | | for (ObservationDailyOperation row : rows) { |
| | | byDate.put(row.getObsDate(), row); |
| | | } |
| | | List<BigDecimal> speed = new ArrayList<>(); |
| | | List<BigDecimal> congestion = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | ObservationDailyOperation row = byDate.get(start.plusDays(i)); |
| | | speed.add(row == null ? null : row.getAverageSpeed()); |
| | | congestion.add(row == null ? null : row.getAverageCongestion()); |
| | | } |
| | | return new ObservationData(speed, congestion); |
| | | } |
| | | |
| | | private HolidayTrendChartService.ChartSeries series(String name, List<BigDecimal> values) { |
| | | HolidayTrendChartService.ChartSeries result = new HolidayTrendChartService.ChartSeries(); |
| | | result.setName(name); |
| | | result.setValues(values == null ? nulls() : values); |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> values(List<DailyMetrics> metrics, Function<DailyMetrics, BigDecimal> selector) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (DailyMetrics item : metrics) { |
| | | result.add(item == null ? null : selector.apply(item)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> combine(List<DailyMetrics> metrics, |
| | | Function<DailyMetrics, BigDecimal> left, |
| | | Function<DailyMetrics, BigDecimal> right) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (DailyMetrics item : metrics) { |
| | | result.add(item == null ? null : HolidayNumberUtils.sumNullable(left.apply(item), right.apply(item))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> cumulative(List<BigDecimal> values) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | result.add(value == null ? null : total); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> percentageChanges(List<BigDecimal> current, List<BigDecimal> previous) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | BigDecimal currentValue = valueAt(current, i); |
| | | BigDecimal previousValue = valueAt(previous, i); |
| | | BigDecimal ratio = HolidayNumberUtils.divide(currentValue, previousValue); |
| | | result.add(ratio == null ? null : HolidayNumberUtils.multiply( |
| | | ratio.subtract(BigDecimal.ONE), HUNDRED)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal sum(List<BigDecimal> values) { |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private BigDecimal valueAt(List<BigDecimal> values, int index) { |
| | | return values == null || index >= values.size() ? null : values.get(index); |
| | | } |
| | | |
| | | private List<BigDecimal> nulls() { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) result.add(null); |
| | | return result; |
| | | } |
| | | |
| | | private HolidayTrendWordService.ImageSize lineImageSize(long width, long height) { |
| | | return new HolidayTrendWordService.ImageSize( |
| | | width, HolidayTrendChartService.scaleLineChartHeight(height)); |
| | | } |
| | | |
| | | private File resolveTemplateFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(trendTemplatePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), trendTemplatePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + trendTemplatePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) return candidate.getCanonicalFile(); |
| | | } catch (Exception ignored) { |
| | | // Keep checking. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | private byte[] readAll(InputStream input) throws Exception { |
| | | java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = input.read(buffer)) > 0) { |
| | | output.write(buffer, 0, length); |
| | | } |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private static class YearData { |
| | | private int year; |
| | | private List<BigDecimal> rail; |
| | | private List<BigDecimal> civilAviation; |
| | | private List<BigDecimal> road; |
| | | private List<BigDecimal> water; |
| | | private List<BigDecimal> expressway; |
| | | private List<BigDecimal> nationalProvincial; |
| | | private List<BigDecimal> selfDrivingPersonFlow; |
| | | private List<BigDecimal> selfDrivingVehicleFlow; |
| | | private List<BigDecimal> totalVehicleFlow; |
| | | private List<BigDecimal> totalDaily; |
| | | private List<BigDecimal> totalCumulative; |
| | | private List<BigDecimal> railCumulative; |
| | | private List<BigDecimal> civilAviationCumulative; |
| | | private List<BigDecimal> roadCumulative; |
| | | private List<BigDecimal> waterCumulative; |
| | | private List<BigDecimal> selfDrivingPersonFlowCumulative; |
| | | |
| | | private boolean hasAnyData() { |
| | | return hasValue(totalDaily) || hasValue(rail) || hasValue(road) |
| | | || hasValue(expressway) || hasValue(nationalProvincial); |
| | | } |
| | | |
| | | private boolean hasValue(List<BigDecimal> values) { |
| | | if (values == null) return false; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartCategory> modalShare() { |
| | | List<HolidayTrendChartService.ChartCategory> result = new ArrayList<>(); |
| | | result.add(category("自驾", sum(selfDrivingPersonFlow))); |
| | | result.add(category("道路", sum(road))); |
| | | result.add(category("铁路", sum(rail))); |
| | | result.add(category("民航", sum(civilAviation))); |
| | | result.add(category("水路", sum(water))); |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal sum(List<BigDecimal> values) { |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private HolidayTrendChartService.ChartCategory category(String name, BigDecimal value) { |
| | | HolidayTrendChartService.ChartCategory category = new HolidayTrendChartService.ChartCategory(); |
| | | category.setName(name); |
| | | category.setValue(value == null ? BigDecimal.ZERO : value); |
| | | return category; |
| | | } |
| | | } |
| | | |
| | | private static class ObservationData { |
| | | private final List<BigDecimal> speed; |
| | | private final List<BigDecimal> congestion; |
| | | |
| | | private ObservationData(List<BigDecimal> speed, List<BigDecimal> congestion) { |
| | | this.speed = speed; |
| | | this.congestion = congestion; |
| | | } |
| | | } |
| | | } |