// 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.ImmutableMap; 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.apache.poi.ss.usermodel.CellStyle; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import static fi.hsl.parkandride.core.domain.DayType.*; import static fi.hsl.parkandride.core.domain.FacilityStatus.*; import static fi.hsl.parkandride.util.ArgumentValidator.validate; import static java.time.LocalTime.ofSecondOfDay; import static java.util.Arrays.asList; import static java.util.Arrays.fill; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Comparator.comparing; import static java.util.function.Function.identity; import static java.util.stream.Collectors.*; public class FacilityUsageReportService extends AbstractReportService { private static final String REPORT_NAME = "FacilityUsage"; public FacilityUsageReportService(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); } @Override protected Excel generateReport(ReportContext ctx, ReportParameters parameters) { int intervalSeconds = validate(parameters.interval).gt(0) * 60; UtilizationSearch search = toUtilizationSearch(parameters, ctx); validate(search.start).lte(search.end); Map<UtilizationReportKey, UtilizationReportRow> reportRows = new LinkedHashMap<>(); try (CloseableIterator<Utilization> utilizations = utilizationRepository.findUtilizations(search)) { addFilters(utilizations, ctx, parameters) .forEachRemaining(setValueToLatestFreeSpacesInWindow(ctx, intervalSeconds, reportRows)); } addDayToDayStatusInformation(reportRows, parameters.startDate, parameters.endDate); Excel excel = new Excel(); List<UtilizationReportRow> rows = reportRows.values() .stream() .sorted(comparing((UtilizationReportRow row) -> row.key.date) .thenComparing(row -> row.key.facility.name.fi) .thenComparing(row -> row.key.capacityType) .thenComparing(row -> row.key.usage) ) .collect(toList()); Map<FacilityStatus, CellStyle> colors = ImmutableMap.of( IN_OPERATION, excel.green, EXCEPTIONAL_SITUATION, excel.yellow, TEMPORARILY_CLOSED, excel.orange, INACTIVE, excel.red ); List<Excel.TableColumn<UtilizationReportRow>> columns = asList( excelUtil.tcol("reports.usage.col.facility", (UtilizationReportRow r) -> r.key.facility.name), excelUtil.tcol("reports.usage.col.hub", (UtilizationReportRow r) -> ctx.hubsByFacilityId.getOrDefault(r.key.targetId, emptyList()).stream().map((Hub h) -> h.name.fi).collect(joining(", "))), excelUtil.tcol("reports.usage.col.region", (UtilizationReportRow r) -> ctx.regionByFacilityId.get(r.key.targetId).name), excelUtil.tcol("reports.usage.col.operator", (UtilizationReportRow r) -> operatorService.getOperator(r.key.facility.operatorId).name), excelUtil.tcol("reports.usage.col.usage", (UtilizationReportRow r) -> translationService.translate(r.key.usage)), excelUtil.tcol("reports.usage.col.capacityType", (UtilizationReportRow r) -> translationService.translate(r.key.capacityType)), excelUtil.tcol("reports.usage.col.status", (UtilizationReportRow r) -> translationService.translate(r.effectiveStatus), (UtilizationReportRow r) -> colors.get(r.effectiveStatus)), excelUtil.tcol("reports.usage.col.openingHoursBusinessDay", (UtilizationReportRow r) -> ExcelUtil.time(r.key.facility.openingHours.byDayType.get(BUSINESS_DAY))), excelUtil.tcol("reports.usage.col.openingHoursSaturday", (UtilizationReportRow r) -> ExcelUtil.time(r.key.facility.openingHours.byDayType.get(SATURDAY))), excelUtil.tcol("reports.usage.col.openingHoursSunday", (UtilizationReportRow r) -> ExcelUtil.time(r.key.facility.openingHours.byDayType.get(SUNDAY))), excelUtil.tcol("reports.usage.col.spacesAvailable", (UtilizationReportRow r) -> r.key.facility.builtCapacity.get(r.key.capacityType)), excelUtil.tcol("reports.usage.col.date", (UtilizationReportRow r) -> r.key.date) ); columns = new ArrayList<>(columns); final DateTime currentDateTime = DateTime.now(); for (int s = 0, i = 0; s < SECONDS_IN_DAY; s += intervalSeconds, i++) { final long millis = s * 1000; final int idx = i; columns.add(Excel.TableColumn.col(ofSecondOfDay(s).toString(), (UtilizationReportRow r) -> { // Special case, hide data for future columns final DateTime dateTime = r.key.date.toDateTime(LocalTime.fromMillisOfDay(millis)); return dateTime.isAfter(currentDateTime) ? "" : r.values[idx]; })); } excel.addSheet(excelUtil.getMessage("reports.usage.sheets.usage"), rows, columns); excel.addSheet(excelUtil.getMessage("reports.usage.sheets.legend"), excelUtil.getMessage("reports.usage.legend").split("\n")); return excel; } private void addDayToDayStatusInformation(Map<UtilizationReportKey, UtilizationReportRow> reportRows, LocalDate startDate, LocalDate endDate) { final Map<Long, Map<LocalDate, FacilityStatus>> statusHistory = reportRows.keySet().stream() .map(key -> key.facility.id) .distinct() .collect(toMap( identity(), id -> facilityHistoryService.getStatusHistoryByDay(id, startDate, endDate) )); reportRows.forEach((key, row) -> row.effectiveStatus = statusHistory.getOrDefault(key.facility.id, emptyMap()) .getOrDefault(key.date, key.facility.status)); } private Consumer<Utilization> setValueToLatestFreeSpacesInWindow(ReportContext ctx, int intervalSeconds, Map<UtilizationReportKey, UtilizationReportRow> reportRows) { return u -> { UtilizationReportKey key = new UtilizationReportKey(u); key.facility = ctx.facilities.get(u.facilityId); UtilizationReportRow value = reportRows.get(key); if (value == null) { UtilizationReportRow prevDayRow = reportRows.get(key.prevDay()); int initialValue = 0; if (prevDayRow != null) { initialValue = prevDayRow.values[prevDayRow.values.length - 1]; } value = new UtilizationReportRow(key, intervalSeconds, initialValue); reportRows.put(key, value); } value.setValue(u.timestamp, u.spacesAvailable); }; } static class UtilizationReportKey extends BasicUtilizationReportKey { LocalDate date; Facility facility; public UtilizationReportKey() { } public UtilizationReportKey(Utilization u) { super(u); date = u.timestamp.toLocalDate(); } public UtilizationReportKey prevDay() { UtilizationReportKey k = new UtilizationReportKey(); k.targetId = targetId; k.capacityType = capacityType; k.usage = usage; k.date = date.minusDays(1); return k; } @Override public int hashCode() { return super.hashCode() ^ date.hashCode(); } @Override public boolean equals(Object obj) { return super.equals(obj) && date.equals(((UtilizationReportKey) obj).date); } } static class UtilizationReportRow { private final int intervalSeconds; final UtilizationReportKey key; final int[] values; FacilityStatus effectiveStatus; UtilizationReportRow(UtilizationReportKey key, int intervalSeconds, int initialValue) { this.key = key; this.intervalSeconds = intervalSeconds; values = new int[SECONDS_IN_DAY / intervalSeconds]; fill(values, initialValue); } void setValue(DateTime ts, int value) { int idx = ts.getSecondOfDay() / intervalSeconds; fill(values, idx, values.length, value); } } }