Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Car park opening hours #43

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -822,5 +822,10 @@
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>io.leonard</groupId>
<artifactId>opening-hours-evaluator</artifactId>
<version>0.0.7</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -619,15 +619,18 @@ private static List<Leg> addFreeFloatingBicycleDropOffAlerts(List<Leg> legs, Sta
return legs;
}

private static void addCarParkAlerts(Leg leg, State[] state, Locale locale) {
var isTripPlannedForNow = Arrays.stream(state)
private static void addCarParkAlerts(Leg leg, State[] states, Locale locale) {
var isTripPlannedForNow = Arrays.stream(states)
.findFirst()
.map(s -> s.getOptions().isTripPlannedForNow())
.orElse(false);

if(isTripPlannedForNow && containsCarParkWithFewSpaces(state)){
if(isTripPlannedForNow && containsCarParkWithFewSpaces(states)){
leg.addAlert(Alert.createLowCarParkSpacesAlert(), locale);
}
if(containsCarParkWhichIsClosingSoon(states)){
leg.addAlert(Alert.createCarParkClosingSoonAlert(), locale);
}
}

private static boolean containsCarParkWithFewSpaces(State[] state) {
Expand All @@ -637,6 +640,16 @@ private static boolean containsCarParkWithFewSpaces(State[] state) {
.anyMatch(ParkAndRideVertex::hasFewSpacesAvailable);
}

private static boolean containsCarParkWhichIsClosingSoon(State[] states) {
return Arrays.stream(states).anyMatch(state -> {
if(state.getVertex() instanceof ParkAndRideVertex && state.getBackMode().isDriving()) {
ParkAndRideVertex vertex = (ParkAndRideVertex) state.getVertex();
return vertex.isClosedAt(state.getLocalDateTime().plusMinutes(30));
}
else return false;
} );
}

/**
* Add trip-related fields to a {@link Leg}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class Alert implements Serializable {

public enum AlertId {
CAR_PARK_FULL("car_park_full"),
CAR_PARK_CLOSING_SOON("car_closing_soone"),
BIKE_RENTAL_FREE_FLOATING_DROP_OFF("bike_rental_free_floating_drop_off");

public String value;
Expand Down Expand Up @@ -68,6 +69,12 @@ public static Alert createLowCarParkSpacesAlert() {
return alert;
}

public static Alert createCarParkClosingSoonAlert() {
Alert alert = createTranslatedAlert("car_park.closing_soon");
alert.alertId = AlertId.CAR_PARK_CLOSING_SOON;
return alert;
}

private static List<Locale> locales = ImmutableList.of(Locale.ENGLISH, Locale.GERMAN);

private static Alert createTranslatedAlert(String translationKey) {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/org/opentripplanner/routing/car_park/CarPark.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ the License, or (at your option) any later version.

package org.opentripplanner.routing.car_park;

import ch.poole.openinghoursparser.OpeningHoursParseException;
import ch.poole.openinghoursparser.OpeningHoursParser;
import ch.poole.openinghoursparser.Rule;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.base.Strings;
import io.leonard.OpeningHoursEvaluator;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.util.I18NString;

import javax.xml.bind.annotation.XmlAttribute;
import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Locale;


Expand Down Expand Up @@ -68,6 +77,8 @@ public class CarPark implements Serializable {

public Geometry geometry;

private List<Rule> parsedOpeningHours = null;

public boolean equals(Object o) {
if (!(o instanceof CarPark)) {
return false;
Expand All @@ -88,6 +99,31 @@ public boolean hasFewSpacesAvailable() {
return hasFewSpacesAvailable(spacesAvailable, maxCapacity);
}

public boolean isClosedAt(LocalDateTime time) {
parseOpeningHours();
if(parsedOpeningHours == null) return false;
else return !OpeningHoursEvaluator.isOpenAt(time, parsedOpeningHours);
}

private void parseOpeningHours() {
if(parsedOpeningHours == null && ! Strings.isNullOrEmpty(openingHours)) {
var parser = new OpeningHoursParser(new ByteArrayInputStream(openingHours.getBytes()));
try {
parsedOpeningHours = parser.rules(true);
} catch (OpeningHoursParseException e) {
parsedOpeningHours = Collections.emptyList();
}
}
}

public LocalDateTime opensNext(LocalDateTime time) {
parseOpeningHours();
if(parsedOpeningHours == null) {
return LocalDateTime.MIN;
}
else return OpeningHoursEvaluator.isOpenNext(time, parsedOpeningHours).orElse(LocalDateTime.MIN);
}

public static boolean hasFewSpacesAvailable(int spacesAvailable, int maxCapacity) {
// special handling if it is a very small car park
if(maxCapacity < 10) {
Expand All @@ -101,4 +137,5 @@ public static boolean hasFewSpacesAvailable(int spacesAvailable, int maxCapacity
return !(Double.isNaN(percentFree)) && percentFree <= 0.1f;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ public class RoutingRequest implements Cloneable, Serializable {
*/
public int carDropoffTime = 120;

/**
* The maximum number of seconds to wait for a car park to open if it is closed at the time when the car
* is arriving at it.
*/
public int maxCarParkOpeningWaitTime = 300;

/**
* How much worse is waiting for a transit vehicle than being on a transit vehicle, as a multiplier. The default value treats wait and on-vehicle
* time as the same.
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/opentripplanner/routing/core/State.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -195,6 +197,12 @@ public long getTimeSeconds() {
return time / 1000;
}

/** Returns the time of the state in the graph's timezone **/
public LocalDateTime getLocalDateTime() {
var zoneId = getOptions().rctx.graph.getTimeZone().toZoneId();
return Instant.ofEpochSecond(getTimeSeconds()).atZone(zoneId).toLocalDateTime();
}

/** returns the length of the trip in seconds up to this state */
public long getElapsedTimeSeconds() {
return Math.abs(getTimeSeconds() - stateData.startTime);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opentripplanner.routing.edgetype;

import io.leonard.OpeningHoursEvaluator;
import org.opentripplanner.common.MavenVersion;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
Expand All @@ -9,6 +10,9 @@
import org.opentripplanner.routing.vertextype.ParkAndRideVertex;

import org.locationtech.jts.geom.LineString;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Locale;

/**
Expand Down Expand Up @@ -38,7 +42,10 @@ public State traverse(State s0) {
}
if(request.useCarParkAvailabilityInformation
&& request.isTripPlannedForNow()
&& ((ParkAndRideVertex) tov).hasFewSpacesAvailable()){
&& ((ParkAndRideVertex) tov).hasFewSpacesAvailable() ) {
return null;
}
if (isClosedAt(s0.getLocalDateTime()) && isClosedAt(s0.getLocalDateTime().plusSeconds(request.maxCarParkOpeningWaitTime))) {
return null;
}
if (request.arriveBy) {
Expand Down Expand Up @@ -69,8 +76,17 @@ public State traverse(State s0) {
throw new IllegalStateException("Can't drive 2 cars");
}
StateEditor s1 = s0.edit(this);

int time = request.carDropoffTime;

// if the car park is opening soon (ie. withing maxCarParkOpeningWaitTime) then wait outside for it to open
int waitingTime = 0;

if(isClosedAt(s0.getLocalDateTime())) {
var openNext = opensNext(s0.getLocalDateTime());
var seconds = (int) Duration.between(s0.getLocalDateTime(), openNext).toSeconds();
waitingTime = Math.max(0, seconds); // a little bit of defensive coding against.
}

int time = request.carDropoffTime + waitingTime;
s1.incrementWeight(time);
final double multiplier = (request.carParkCarLegWeight - 1);
s1.incrementWeight(s0.getWeight() * multiplier);
Expand All @@ -81,6 +97,14 @@ public State traverse(State s0) {
}
}

private boolean isClosedAt(LocalDateTime time) {
return ((ParkAndRideVertex) tov).isClosedAt(time);
}

private LocalDateTime opensNext(LocalDateTime time) {
return ((ParkAndRideVertex) tov).opensNext(time);
}

@Override
public double getDistance() {
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.util.I18NString;

import java.time.LocalDateTime;
import java.util.Optional;

/**
* A vertex for a park and ride area.
* Connected to streets by ParkAndRideLinkEdge.
Expand Down Expand Up @@ -53,4 +56,12 @@ public void updateCapacity(int maxCapacity, int spacesAvailable) {
public boolean hasFewSpacesAvailable() {
return carPark.hasFewSpacesAvailable();
}

public boolean isClosedAt(LocalDateTime time) {
return Optional.ofNullable(carPark).map(c -> c.isClosedAt(time)).orElse(false);
}

public LocalDateTime opensNext(LocalDateTime time) {
return Optional.ofNullable(carPark).map(c -> c.opensNext(time)).orElse(LocalDateTime.MIN);
}
}
4 changes: 4 additions & 0 deletions src/main/resources/WayProperties.properties
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@ name.park_and_ride_station=P+R
alert.car_park.full.header = Few parking spaces available
alert.car_park.full.description = The selected car park has only few spaces available. Please add extra time to your trip.

alert.car_park.closing_soon.header = Car park is closing soon.
alert.car_park.closing_soon.description = Car park is closing soon.

alert.bicycle_rental.free_floating_dropoff.header = Destination is not a designated drop-off area.
alert.bicycle_rental.free_floating_dropoff.description = Rental cannot be completed here. Please check terms & conditions for additional fees.

3 changes: 3 additions & 0 deletions src/main/resources/WayProperties_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ name.park_and_ride_station=P+R
alert.car_park.full.header = Wenige Stellplätze frei.
alert.car_park.full.description = Der Parkplatz hat wenige Stellplätze frei. Bitte planen sie einige Minuten Wartezeit ein.

alert.car_park.closing_soon.header = Parkplatz schließt bald.
alert.car_park.closing_soon.description = Parkplatz schließt bald.

alert.bicycle_rental.free_floating_dropoff.header = Ziel ist keine Rückgabestation.
alert.bicycle_rental.free_floating_dropoff.description = Ziel ist keine Rückgabestation. Ausleihe kann hier nicht abgeschlossen werden. Anbieterabhängig fallen weiter Gebühren bis zur Rückgabe an einer Station an.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.junit.Test;

import java.time.LocalDateTime;

import static org.junit.Assert.*;
import static org.opentripplanner.routing.car_park.CarPark.hasFewSpacesAvailable;

Expand All @@ -27,4 +29,22 @@ public void shouldCalculateFewSpacesAvailable() {
assertTrue(hasFewSpacesAvailable(19, 201));
assertFalse(hasFewSpacesAvailable(20, 500));
}

@Test
public void shouldCalculateIfCarParkIsOpen() {
var carPark = new CarPark();
carPark.openingHours = "Mo-Fr 09:00-12:00";
var before12 = LocalDateTime.parse("2020-08-07T11:24:04");
var after12 = LocalDateTime.parse("2020-08-07T12:24:04");
assertFalse(carPark.isClosedAt(before12));
assertTrue(carPark.isClosedAt(after12));

// car parks with no opening hours should be always open
var carPark2 = new CarPark();
assertFalse(carPark2.isClosedAt(after12));

var carPark3 = new CarPark();
carPark3.openingHours = "";
assertFalse(carPark3.isClosedAt(after12));
}
}
Loading