// Copyright © 2015 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.core.service.reporting; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.mysema.commons.lang.CloseableIterator; import fi.hsl.parkandride.back.RegionRepository; import fi.hsl.parkandride.core.back.UtilizationRepository; import fi.hsl.parkandride.core.domain.*; import fi.hsl.parkandride.core.service.*; import org.joda.time.LocalDate; import java.util.*; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; import static com.google.common.collect.Lists.newArrayList; import static fi.hsl.parkandride.core.domain.FacilityStatus.*; import static fi.hsl.parkandride.core.domain.Region.UNKNOWN_REGION; import static fi.hsl.parkandride.core.service.reporting.Excel.TableColumn.col; import static fi.hsl.parkandride.util.MapUtils.*; import static java.util.Arrays.asList; import static java.util.Collections.*; import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.Collectors.*; import static java.util.stream.StreamSupport.stream; public class MaxUtilizationReportService extends AbstractReportService { private static final String REPORT_NAME = "MaxUtilization"; private static final Set<FacilityStatus> EXCLUDED_STATES = ImmutableSet.of(INACTIVE, TEMPORARILY_CLOSED); private static final Collector<Map.Entry<MaxUtilizationReportKeyWithDate, FacilityRowInfo>, ?, MaxUtilizationReportInfo> toMaxUtilizationReportInfo = Collector.of( MaxUtilizationReportInfo::new, (info, entry) -> info.addRow(entry.getKey(), entry.getValue()), (info1, info2) -> { info2.rows.forEach(info1::addRow); return info1; } ); public MaxUtilizationReportService(FacilityService facilityService, OperatorService operatorService, ContactService contactService, HubService hubService, UtilizationRepository utilizationRepository, RegionRepository regionRepository, TranslationService translationService, FacilityHistoryService facilityHistoryService) { super(REPORT_NAME, facilityService, operatorService, contactService, hubService, utilizationRepository, translationService, regionRepository, facilityHistoryService); } private static boolean hasBuiltCapacity(Utilization u, Map<Long, Facility> facilities) { return facilities.get(u.facilityId).builtCapacity.containsKey(u.capacityType); } @Override protected Excel generateReport(ReportContext ctx, ReportParameters parameters) { // Filtered utilizations to spaces available // (Facility, LocalDate, CapacityType, Usage) -> [totalCapacity,unavailableCapacity,availableSpaces] MaxUtilizationReportInfo reportInfo = getReportInfo(ctx, parameters); // group facility keys by hubs Map<HubReportKey, List<MaxUtilizationReportKeyWithDate>> hubStats = groupKeysByHubs(ctx, reportInfo.rows.keySet()); // calculate averages and sums List<MaxUtilizationReportRow> rows = createReportRows(ctx, hubStats, reportInfo); Excel excel = createExcelReport(ctx, rows); return excel; } private MaxUtilizationReportInfo getReportInfo(ReportContext ctx, ReportParameters params) { Map<MaxUtilizationReportKeyWithDate, Integer> facilityStats = getFacilityStats(ctx, params, toUtilizationSearch(params, ctx)); Set<Long> facilityIds = facilityStats.keySet().stream().map(key -> key.targetId).distinct().collect(toSet()); // Historical information Map<Long, Map<LocalDate, FacilityStatus>> facilityStatusHistory = getFacilityStatusHistory(facilityIds, params.startDate, params.endDate); Map<Long, Map<LocalDate, FacilityCapacity>> facilityCapacityHistory = getFacilityCapacityHistory(facilityIds, params.startDate, params.endDate); Map<MaxUtilizationReportKey, Integer> facilityUnavailableCapacity = getFacilityUnavailableCapacity(ctx, facilityCapacityHistory); return facilityStats.entrySet().stream().map(mappingValue((key, spacesAvailable) -> { final Facility facility = ctx.facilities.get(key.targetId); final FacilityRowInfo info = new FacilityRowInfo(); info.facility = key.facility; info.spacesAvailable = spacesAvailable; info.totalCapacity = facilityCapacityHistory .getOrDefault(key.targetId, emptyMap()) .getOrDefault(key.date, new FacilityCapacity(facility)) .builtCapacity.getOrDefault(key.capacityType, 0); info.unavailableCapacity = facilityUnavailableCapacity.get(key.toReportKey()); info.status = facilityStatusHistory.get(key.targetId).get(key.date); info.date = key.date; return info; })).collect(toMaxUtilizationReportInfo); } private Map<Long, Map<LocalDate, FacilityStatus>> getFacilityStatusHistory(Set<Long> facilityIds, LocalDate startDate, LocalDate endDate) { return Maps.toMap(facilityIds, id -> facilityHistoryService.getStatusHistoryByDay(id, startDate, endDate)); } private Map<Long, Map<LocalDate, FacilityCapacity>> getFacilityCapacityHistory(Set<Long> facilityIds, LocalDate startDate, LocalDate endDate) { return Maps.toMap(facilityIds, id -> facilityHistoryService.getCapacityHistory(id, startDate, endDate)); } private Map<MaxUtilizationReportKey, Integer> getFacilityUnavailableCapacity(ReportContext ctx, Map<Long, Map<LocalDate, FacilityCapacity>> capacityHistory) { Set<MaxUtilizationReportKeyWithDate> keysForUnavailableDates = capacityHistory.entrySet().stream() .flatMap(mappingEntry((id, val) -> val.entrySet().stream() .flatMap(mappingEntry((LocalDate date, FacilityCapacity capacity) -> capacity.unavailableCapacities.stream() .map((UnavailableCapacity uc) -> { final MaxUtilizationReportKeyWithDate key = new MaxUtilizationReportKeyWithDate(); key.targetId = id; key.facility = ctx.facilities.get(id); key.date = date; key.capacityType = uc.capacityType; key.usage = uc.usage; return key; }))))) .collect(toSet()); return Maps.toMap(keysForUnavailableDates, key -> { final FacilityCapacity currentCapacities = new FacilityCapacity(ctx.facilities.get(key.targetId)); return capacityHistory .getOrDefault(key.targetId, emptyMap()) .getOrDefault(key.date, currentCapacities) .unavailableCapacities .stream() .filter(uc -> uc.capacityType == key.capacityType) .filter(uc -> uc.usage == key.usage) .map(uc -> uc.capacity) .findFirst().orElse(0); }).entrySet().stream().map(mappingKey((key, val) -> key.toReportKey())).collect(entriesToMap(Math::max)); } private Excel createExcelReport(ReportContext ctx, List<MaxUtilizationReportRow> rows) { Excel excel = new Excel(); Function<MaxUtilizationReportRow, Object> valFn = (MaxUtilizationReportRow r) -> r.average; List<Excel.TableColumn<MaxUtilizationReportRow>> columns = asList( excelUtil.tcol("reports.utilization.col.hub", (MaxUtilizationReportRow r) -> r.hubName), excelUtil.tcol("reports.utilization.col.region", (MaxUtilizationReportRow r) -> r.regionName), excelUtil.tcol("reports.utilization.col.operator", (MaxUtilizationReportRow r) -> r.operatorNames), excelUtil.tcol("reports.utilization.col.usage", (MaxUtilizationReportRow r) -> translationService.translate(r.key.usage)), excelUtil.tcol("reports.utilization.col.capacityType", (MaxUtilizationReportRow r) -> translationService.translate(r.key.capacityType)), excelUtil.tcol("reports.utilization.col.totalCapacity", (MaxUtilizationReportRow r) -> r.totalCapacity), excelUtil.tcol("reports.utilization.col.unavailableCapacity", (MaxUtilizationReportRow r) -> r.unavailableCapacity), excelUtil.tcol("reports.utilization.col.dayType", (MaxUtilizationReportRow r) -> translationService.translate(r.key.dayType)), excelUtil.tcol("reports.utilization.col.averageMaxUsage", valFn, excel.percent), col("", (MaxUtilizationReportRow r) -> r.hasHadExceptionalStates ? excelUtil.getMessage("reports.utilization.exceptionalSituation") : null) ); excel.addSheet(excelUtil.getMessage("reports.utilization.sheets.summary"), rows, columns); excel.addSheet(excelUtil.getMessage("reports.utilization.sheets.legend"), excelUtil.getMessage("reports.utilization.legend").split("\n")); return excel; } private List<MaxUtilizationReportRow> createReportRows(ReportContext ctx, Map<HubReportKey, List<MaxUtilizationReportKeyWithDate>> hubStats, MaxUtilizationReportInfo reportInfo) { List<MaxUtilizationReportRow> rows = new ArrayList<>(); // At this point, the hub key is grouped by dayType, facility keys still contain the actual dat // However, the maximum utilization value has already been calculated for each facility and date. hubStats.forEach((hubKey, facilityKeys) -> { if (hubKey == null) { // Facilities not belonging to any hub rows.addAll(createRowsForFacilitiesWithoutHub(ctx, reportInfo, facilityKeys)); } else { rows.add(createRowForHub(ctx, reportInfo, hubKey, facilityKeys)); } }); sort(rows); return rows; } private List<MaxUtilizationReportRow> createRowsForFacilitiesWithoutHub(ReportContext ctx, MaxUtilizationReportInfo reportInfo, List<MaxUtilizationReportKeyWithDate> facilityKeys) { // Group by facility final Map<MaxUtilizationReportKey, List<MaxUtilizationReportKeyWithDate>> groupedByFacility = facilityKeys.stream().collect(groupingBy(k -> k.toReportKey())); return groupedByFacility.entrySet().stream().map(mappingEntry((facilityKey, keys) -> { final RowMetrics rowMetrics = calculateRowMetrics(ctx, reportInfo, keys, facilityKey.capacityType); return new MaxUtilizationReportRow(facilityKey.facility.name, ctx.regionByFacilityId.getOrDefault(facilityKey.facility.id, UNKNOWN_REGION).name, facilityKey, ctx.operators.get(facilityKey.facility.operatorId).name.fi, rowMetrics); })).collect(toList()); } private MaxUtilizationReportRow createRowForHub(ReportContext ctx, MaxUtilizationReportInfo reportInfo, HubReportKey hubKey, List<MaxUtilizationReportKeyWithDate> facilityKeys) { final RowMetrics rowMetrics = calculateRowMetrics(ctx, reportInfo, facilityKeys, hubKey.capacityType); return new MaxUtilizationReportRow(hubKey.hub.name, ctx.regionByHubId.getOrDefault(hubKey.hub.id, UNKNOWN_REGION).name, facilityKeys.get(0).toReportKey(), operatorNames(ctx, hubKey), rowMetrics); } private static class RowMetrics { int totalCapacity; int unavailableCapacity; double utilizationRate; boolean hasHadExceptionalStates; public RowMetrics(int totalCapacity, int unavailableCapacity, double utilizationRate, boolean hasHadExceptionalStates) { this.totalCapacity = totalCapacity; this.unavailableCapacity = unavailableCapacity; this.utilizationRate = utilizationRate; this.hasHadExceptionalStates = hasHadExceptionalStates; } } private RowMetrics calculateRowMetrics(ReportContext ctx, MaxUtilizationReportInfo reportInfo, List<MaxUtilizationReportKeyWithDate> facilityKeys, CapacityType capacityType) { final Integer totalCapacity = facilityKeys.stream() .map(k -> k.targetId) .distinct() .map(id -> ctx.facilities.get(id)) .collect(summingInt(f -> f.builtCapacity.get(capacityType))); final List<FacilityRowInfo> facilityInfos = facilityKeys.stream().map(k -> reportInfo.rows.get(k)).collect(toList()); final int unavailableCapacity = facilityInfos.stream() .map(row -> row.unavailableCapacity) .filter(Objects::nonNull) .max(Ordering.natural()) .orElse(0); final Map<LocalDate, Integer> capacityPerDate = facilityInfos.stream() .collect(groupingBy(info -> info.date, summingInt(info -> info.totalCapacity))); final Map<LocalDate, Double> freeSpacesPerDate = facilityInfos.stream() .filter(info -> !EXCLUDED_STATES.contains(info.status)) .collect(groupingBy(info -> info.date, summingDouble(info -> info.spacesAvailable))); final Double averageOfPercentages = freeSpacesPerDate.entrySet().stream() .map(mappingValue((date, freeSpaces) -> { final Integer capacity = capacityPerDate.get(date); return capacity == 0 ? 0.0d : 1.0d - (freeSpaces / ((double) capacity)); })) .collect(averagingDouble(e -> e.getValue())); final boolean hasHadExceptionalStates = facilityInfos.stream().map(i -> i.status).anyMatch(s -> EXCEPTIONAL_SITUATION.equals(s)); return new RowMetrics(totalCapacity, unavailableCapacity, averageOfPercentages, hasHadExceptionalStates); } private String operatorNames(ReportContext ctx, HubReportKey hubKey) { return ctx.operatorsByHubId.get(hubKey.targetId).stream().map(op -> op.name.fi).sorted().collect(Collectors.joining(", ")); } private Map<HubReportKey, List<MaxUtilizationReportKeyWithDate>> groupKeysByHubs(ReportContext ctx, Set<MaxUtilizationReportKeyWithDate> reportKeys) { Map<HubReportKey, List<MaxUtilizationReportKeyWithDate>> hubStats = new LinkedHashMap<>(); reportKeys.forEach(key -> { final List<Hub> hubs = ctx.hubsByFacilityId.getOrDefault(key.targetId, emptyList()); if (hubs.isEmpty()) { // Belongs to no hubs hubStats.computeIfAbsent(null, k -> new ArrayList<>()).add(key); } else { // Add it to each hub hubs.forEach(hub -> { List<MaxUtilizationReportKeyWithDate> l = newArrayList(key); HubReportKey hubKey = new HubReportKey(); // almost same key -> just targetId switches from facilityId to // hubId hubKey.targetId = hub.id; hubKey.capacityType = key.capacityType; hubKey.usage = key.usage; hubKey.dayType = DayType.valueOf(key.date.toDateTimeAtStartOfDay()); hubKey.hub = hub; hubStats.merge(hubKey, l, (o, n) -> { o.addAll(n); return o; }); }); } }); return hubStats; } private Map<MaxUtilizationReportKeyWithDate, Integer> getFacilityStats(ReportContext ctx, ReportParameters parameters, UtilizationSearch search) { try (CloseableIterator<Utilization> utilizations = utilizationRepository.findUtilizations(search)) { return stream(spliteratorUnknownSize(addFilters(utilizations, ctx, parameters), Spliterator.ORDERED), false) .filter(u -> hasBuiltCapacity(u, ctx.facilities)) .collect(toMap( u -> new MaxUtilizationReportKeyWithDate(u, ctx.facilities.get(u.facilityId)), u -> u.spacesAvailable, Math::min, LinkedHashMap::new )); } } private static class MaxUtilizationReportInfo { Map<MaxUtilizationReportKeyWithDate, FacilityRowInfo> rows = new LinkedHashMap<>(); Map<MaxUtilizationReportKey, List<FacilityRowInfo>> groupedByDayType = new LinkedHashMap<>(); public MaxUtilizationReportInfo addRow(MaxUtilizationReportKeyWithDate key, FacilityRowInfo info) { rows.merge(key, info, (r1, r2) -> { throw new IllegalArgumentException(String.format("Duplicate keys encountered: <%s> <%s>", r1, r2)); }); groupedByDayType.computeIfAbsent(key.toReportKey(), k -> newArrayList()).add(info); return this; } } private static class FacilityRowInfo { Facility facility; Integer spacesAvailable; Integer totalCapacity; Integer unavailableCapacity; FacilityStatus status; LocalDate date; } static class MaxUtilizationReportRow implements Comparable<MaxUtilizationReportRow> { final MultilingualString hubName; final MultilingualString regionName; final MaxUtilizationReportKey key; final String operatorNames; final double average; final int totalCapacity; final int unavailableCapacity; final boolean hasHadExceptionalStates; MaxUtilizationReportRow(MultilingualString hubName, MultilingualString regionName, MaxUtilizationReportKey key, String operatorNames, double average, int totalCapacity, int unavailableCapacity, boolean hasHadExceptionalStates) { this.hubName = hubName; this.regionName = regionName; this.key = key; this.operatorNames = operatorNames; this.average = average; this.totalCapacity = totalCapacity; this.unavailableCapacity = unavailableCapacity; this.hasHadExceptionalStates = hasHadExceptionalStates; } MaxUtilizationReportRow(MultilingualString hubName, MultilingualString regionName, MaxUtilizationReportKey key, String operatorNames, RowMetrics rowMetrics) { this(hubName, regionName, key, operatorNames, rowMetrics.utilizationRate, rowMetrics.totalCapacity, rowMetrics.unavailableCapacity, rowMetrics.hasHadExceptionalStates); } @Override public int compareTo(MaxUtilizationReportRow o) { int c = hubName.fi.compareTo(o.hubName.fi); if (c != 0) { return c; } c = key.usage.compareTo(o.key.usage); if (c != 0) { return c; } c = key.capacityType.compareTo(o.key.capacityType); if (c != 0) { return c; } return key.dayType.compareTo(o.key.dayType); } } /** * Key for grouping facility keys by hubs */ static class HubReportKey extends MaxUtilizationReportKey { Hub hub; } static class MaxUtilizationReportKey extends BasicUtilizationReportKey { DayType dayType; Facility facility; public MaxUtilizationReportKey() { } public MaxUtilizationReportKey(Utilization u) { super(u); this.dayType = DayType.valueOf(u.timestamp); } public MaxUtilizationReportKey(Utilization u, Facility facility) { this(u); this.facility = facility; } @Override public int hashCode() { return super.hashCode() ^ dayType.hashCode(); } @Override public boolean equals(Object obj) { return super.equals(obj) && dayType.equals(((MaxUtilizationReportKey) obj).dayType); } } static class MaxUtilizationReportKeyWithDate extends BasicUtilizationReportKey { LocalDate date; Facility facility; public MaxUtilizationReportKeyWithDate() { } public MaxUtilizationReportKeyWithDate(Utilization u) { super(u); this.date = u.timestamp.toLocalDate(); } public MaxUtilizationReportKeyWithDate(Utilization u, Facility facility) { this(u); this.facility = facility; } public MaxUtilizationReportKey toReportKey() { final MaxUtilizationReportKey key = new MaxUtilizationReportKey(); key.targetId = targetId; key.usage = usage; key.capacityType = capacityType; key.facility = facility; key.dayType = DayType.valueOf(date.toDateTimeAtStartOfDay()); return key; } @Override public int hashCode() { return super.hashCode() ^ date.hashCode(); } @Override public boolean equals(Object obj) { return super.equals(obj) && date.equals(((MaxUtilizationReportKeyWithDate) obj).date); } } }