// Copyright © 2016 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.itest; import com.google.common.collect.ImmutableMap; import com.jayway.restassured.response.Response; import fi.hsl.parkandride.core.domain.*; import fi.hsl.parkandride.core.service.reporting.ReportParameters; import org.apache.poi.ss.usermodel.Sheet; import org.joda.time.DateTime; import org.joda.time.Days; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; import java.util.stream.IntStream; import java.util.stream.Stream; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.Lists.newArrayList; import static fi.hsl.parkandride.core.domain.CapacityType.CAR; import static fi.hsl.parkandride.core.domain.CapacityType.ELECTRIC_CAR; import static fi.hsl.parkandride.core.domain.FacilityStatus.*; import static fi.hsl.parkandride.core.service.reporting.ExcelUtil.time; import static fi.hsl.parkandride.itest.FacilityUsageReportITest.ListBuilder.listFrom; import static fi.hsl.parkandride.test.DateTimeTestUtils.withDate; import static fi.hsl.parkandride.util.Iterators.iterateFor; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; public class FacilityUsageReportITest extends AbstractReportingITest { private static final int FACILITYUSAGE_FIRST_TIME_COLUMN = 12; private static final String FACILITY_USAGE = "FacilityUsage"; private static final int MILLIS_IN_MINUTE = 60 * 1000; // Empty at 00:00, full at 12:00 private static final BiFunction<Integer, Integer, Integer> SPACES_AT = (capacity, hour) -> (int)(capacity * ((Math.abs(hour - 12) / 12.0))); // --------------------- // FACILITY USAGE REPORT // --------------------- @Before @Override public void initialize() { // Needed to ensure history linearity withDate(initial, this::initFixture); } @Test public void report_FacilityUsage_asAdmin_oneFacility() { final ReportParameters params = baseParams(); params.interval = 60; params.facilities = singleton(facility1.id); params.capacityTypes = singleton(CAR); params.usages = singleton(Usage.PARK_AND_RIDE); params.operators = singleton(facility1.operatorId); registerMockFacilityUsages(facility1, apiUser, 24, 2); registerMockFacilityUsages(facility2, apiUser2, 24, 2); final Response whenPostingToReportUrl = postToReportUrl(params, FACILITY_USAGE, adminUser); checkSheetContents(whenPostingToReportUrl, 0, headersWithTimes(params.interval), facilityRow(facility1, operator1, hub, Usage.PARK_AND_RIDE, CAR, params.interval, 24) ); } @Test public void report_FacilityUsage_asAdmin() { final ReportParameters params = baseParams(); params.interval = 24*60; registerMockFacilityUsages(facility1, apiUser, 24, 2); registerMockFacilityUsages(facility2, apiUser2, 24, 2); final Response whenPostingToReportUrl = postToReportUrl(params, FACILITY_USAGE, adminUser); checkSheetContents(whenPostingToReportUrl, 0, headersWithTimes(params.interval), facilityRow(facility1, operator1, hub, facility1.usages.first(), CAR, params.interval, 24), facilityRow(facility1, operator1, hub, facility1.usages.first(), ELECTRIC_CAR, params.interval, 2), facilityRow(facility2, operator2, hub, facility2.usages.first(), CAR, params.interval, 24), facilityRow(facility2, operator2, hub, facility2.usages.first(), ELECTRIC_CAR, params.interval, 2) ); } @Test public void report_FacilityUsage_asOperator2() { final ReportParameters params = baseParams(); params.interval = 3*60; registerMockFacilityUsages(facility1, apiUser, 24, 2); registerMockFacilityUsages(facility2, apiUser2, 24, 2); final Response whenPostingToReportUrl = postToReportUrl(params, FACILITY_USAGE, operator2User); withWorkbook(whenPostingToReportUrl, workbook -> { assertThat(getSheetNames(workbook)).containsExactly("Täyttöasteraportti", "Selite"); checkSheetContents(workbook, 0, headersWithTimes(params.interval), // No facility 1 as this is for different operator facilityRow(facility2, operator2, hub, facility2.usages.first(), CAR, params.interval, 24), facilityRow(facility2, operator2, hub, facility2.usages.first(), ELECTRIC_CAR, params.interval, 2) ); }); } @Test public void report_FacilityUsage_withStatusHistory() { final ReportParameters params = baseParams(); params.interval = Days.ONE.toStandardMinutes().getMinutes(); final LocalDate start = params.startDate; // Add status history, relates to the time window of 6-10 // 1st day -> IN_OPERATION // 2nd and 3rd day -> TEMPORARILY_CLOSED // 4th day -> INACTIVE // 5th day onwards -> IN_OPERATION updateStatus(start.toDateTime(new LocalTime("07:00")), IN_OPERATION); updateStatus(start.plusDays(1).toDateTime(new LocalTime("07:59")), TEMPORARILY_CLOSED); updateStatus(start.plusDays(2).toDateTime(new LocalTime("08:00")), INACTIVE); updateStatus(start.plusDays(3).toDateTime(new LocalTime("08:01")), IN_OPERATION); // Add data for whole month to get all rows final List<LocalDate> dates = newArrayList(iterateFor(start, p -> !p.isAfter(params.endDate), d -> d.plusDays(1))); dates.forEach(d -> facilityService.registerUtilization(facility1.id, singletonList( utilize(CAR, 10, d.toDateTime(new LocalTime("13:37")), facility1) ), apiUser)); final Response whenPostingToReportUrl = postToReportUrl(params, FACILITY_USAGE, adminUser); withWorkbook(whenPostingToReportUrl, wb -> { Sheet sheet = wb.getSheetAt(0); final List<String> expectedStates = listFrom("Status") .add(translationService.translate(IN_OPERATION)) .addTimes(2, translationService.translate(TEMPORARILY_CLOSED)) .add(translationService.translate(INACTIVE)) .addTimes(dates.size() - 4, translationService.translate(IN_OPERATION)) .build(); assertThat(getDataFromColumn(sheet, ReportColumns.DATE)) .containsSequence(dates.stream().map(d -> d.toString("d.M.yyyy")).toArray(String[]::new)); assertThat(getDataFromColumn(sheet, ReportColumns.STATUS)) .containsExactlyElementsOf(expectedStates); }); } private void updateStatus(DateTime dateTime, FacilityStatus status) { withDate(dateTime, () -> { facility1.status = status; facility1.builtCapacity = ImmutableMap.of(CAR, 50); facilityService.updateFacility(facility1.id, facility1, adminUser); }); } static class ListBuilder<T> { private final List<T> list = new ArrayList<>(); public static <T> ListBuilder<T> listFrom(T... items) { final ListBuilder<T> builder = new ListBuilder<>(); return builder.addAll(items); } public ListBuilder<T> add(T item) { list.add(item); return this; } public ListBuilder<T> addAll(T... items) { Arrays.stream(items).forEach(this::add); return this; } public ListBuilder<T> addTimes(int times, T item) { IntStream.range(0, times).forEach(i -> list.add(item)); return this; } public List<T> build() { return list; } } private List<String> headersWithTimes(int intervalMinutes) { final Stream<String> times = getReportTimes(intervalMinutes).map(t -> t.toString("HH:mm")); return Stream.concat(headersWithoutTimes().stream(), times).collect(toList()); } private List<String> facilityRow(Facility f, Operator o, Hub hub, Usage usage, CapacityType type, int intervalMinutes, Integer maxSpacesAvailable) { final List<String> cells = newArrayList( f.name.fi, hub.name.fi, "Helsinki", o.name.fi, translationService.translate(usage), translationService.translate(type), translationService.translate(f.status), firstNonNull(time(f.openingHours.byDayType.get(DayType.BUSINESS_DAY)), ""), firstNonNull(time(f.openingHours.byDayType.get(DayType.SATURDAY)), ""), firstNonNull(time(f.openingHours.byDayType.get(DayType.SUNDAY)), ""), f.builtCapacity.get(type).toString(), BASE_DATE.toString(DATE_FORMAT) ); cells.addAll(getReportTimes(intervalMinutes) // Get the latest utilization value for each window, e.g., for 00:00-03:00, the value is from 02:00 .map(time -> time.plusMinutes(intervalMinutes).minusHours(1)) .map(time -> SPACES_AT.apply(maxSpacesAvailable, time.getHourOfDay())) .map(String::valueOf) .collect(toList())); return cells; } private List<String> headersWithoutTimes() { return asList("Pysäköintipaikan nimi", "Alue", "Kunta", "Operaattori", "Käyttötapa", "Ajoneuvotyyppi", "Status", "Aukiolo, arki", "Aukiolo, la", "Aukiolo, su", "Pysäköintipaikkojen määrä", "Päivämäärä" ); } private Stream<LocalTime> getReportTimes(int intervalMinutes) { return newArrayList(iterateFor(0, i -> i < 24 * 60 * MILLIS_IN_MINUTE, i -> i += intervalMinutes * MILLIS_IN_MINUTE)) .stream() .map(LocalTime::fromMillisOfDay); } private void registerMockFacilityUsages(Facility facility, User user, int spacesAvailableCar, int spacesAvailableElectric) { DateTime startOfDay = BASE_DATE.toDateTimeAtStartOfDay(); for (int i = 0; i < 24; i++) { facilityService.registerUtilization(facility.id, asList( utilize(CAR, SPACES_AT.apply(spacesAvailableCar, i), startOfDay.withHourOfDay(i), facility), utilize(ELECTRIC_CAR, SPACES_AT.apply(spacesAvailableElectric, i), startOfDay.withHourOfDay(i), facility) ), user); } } private interface ReportColumns { int STATUS = 6; int DATE = 11; } }