package org.croudtrip.trips; import com.google.common.base.Optional; import org.croudtrip.api.directions.NavigationResult; import org.croudtrip.api.directions.Route; import org.croudtrip.api.directions.RouteLocation; import org.croudtrip.api.trips.JoinTripRequest; import org.croudtrip.api.trips.SuperTripReservation; import org.croudtrip.api.trips.SuperTripSubQuery; import org.croudtrip.api.trips.TripOffer; import org.croudtrip.api.trips.TripOfferStatus; import org.croudtrip.api.trips.TripQuery; import org.croudtrip.api.trips.TripReservation; import org.croudtrip.api.trips.UserWayPoint; import org.croudtrip.db.JoinTripRequestDAO; import org.croudtrip.db.TripOfferDAO; import org.croudtrip.directions.DirectionsManager; import org.croudtrip.directions.RouteNotFoundException; import org.croudtrip.logs.LogManager; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import javax.inject.Inject; /** * Handles simple trips (those which have max one driver). */ class SimpleTripsMatcher implements TripsMatcher { protected final JoinTripRequestDAO joinTripRequestDAO; protected final TripOfferDAO tripOfferDAO; protected final TripsNavigationManager tripsNavigationManager; protected final DirectionsManager directionsManager; protected final TripsUtils tripsUtils; protected final LogManager logManager; @Inject SimpleTripsMatcher( JoinTripRequestDAO joinTripRequestDAO, TripOfferDAO tripOfferDAO, TripsNavigationManager tripsNavigationManager, DirectionsManager directionsManager, TripsUtils tripsUtils, LogManager logManager) { this.joinTripRequestDAO = joinTripRequestDAO; this.tripOfferDAO = tripOfferDAO; this.tripsNavigationManager = tripsNavigationManager; this.directionsManager = directionsManager; this.tripsUtils = tripsUtils; this.logManager = logManager; } @Override public List<SuperTripReservation> findPotentialTrips(List<TripOffer> offers, TripQuery query) { List<PotentialMatch> potentialMatches = new ArrayList<>(); for (TripOffer offer : offers) { Optional<PotentialMatch> potentialMatch = isPotentialMatch(offer, query); if ( potentialMatch.isPresent() ) { potentialMatches.add(potentialMatch.get()); } } return findCheapestMatch(query, potentialMatches); } @Override public Optional<PotentialMatch> isPotentialMatch(TripOffer offer, TripQuery query) { // check trip status if (!offer.getStatus().equals(TripOfferStatus.ACTIVE)){ return Optional.absent(); } // check that query has not been declined before if (!assertJoinRequestNotDeclined(offer, query)){ logManager.d("Offer " + offer.getId() + " is no potential match to query because passenger has been already declined."); return Optional.absent(); } // check current passenger count int passengerCount = tripsUtils.getActivePassengerCountForOffer(offer); if (passengerCount >= offer.getVehicle().getCapacity()){ logManager.d("Offer " + offer.getId() + " is no potential match to query because the offer is full"); return Optional.absent(); } // early reject based on airline; if (!assertWithinAirDistance(offer, query)){ logManager.d("Offer " + offer.getId() + " is no potential match to query due to its airline comparison"); return Optional.absent(); } // TODO: Early reject based on time // update driver route on new position update assertUpdatedDriverRoute(offer); // get complete new route NavigationResult totalRouteNavigationResult = null; try { totalRouteNavigationResult = tripsNavigationManager.getNavigationResultForOffer(offer, query); } catch (RouteNotFoundException e) { logManager.e(e.toString()); return Optional.absent(); } List<UserWayPoint> userWayPoints = totalRouteNavigationResult.getUserWayPoints(); if (userWayPoints.isEmpty()) return Optional.absent(); // check passenger max waiting time if (!assertRouteWithinPassengerMaxWaitingTime(offer, query, userWayPoints)){ logManager.d("Offer " + offer.getId() + " is no potential match to query due to the waiting times"); return Optional.absent(); } // check if passenger route is within max diversion long distanceToDriverInMeters = userWayPoints.get(userWayPoints.size() - 1).getDistanceToDriverInMeters(); if (distanceToDriverInMeters - offer.getDriverRoute().getDistanceInMeters() > offer.getMaxDiversionInMeters()){ logManager.d("Offer " + offer.getId() + " is no potential match to query due to the maximum diversions of the passenger"); return Optional.absent(); } return Optional.of( new PotentialMatch( offer, query, totalRouteNavigationResult )); } protected boolean assertJoinRequestNotDeclined(TripOffer offer, TripQuery query) { List<JoinTripRequest> declinedRequests = joinTripRequestDAO.findDeclinedRequests(query.getPassenger().getId()); for( JoinTripRequest request : declinedRequests ) { if( offer.getId() == request.getOffer().getId()) { return false; } } return true; } protected boolean assertWithinAirDistance(TripOffer offer, TripQuery query) { List<RouteLocation> driverWayPoints = offer.getDriverRoute().getWayPoints(); double airlineDriverRoute = driverWayPoints.get(0).distanceFrom( driverWayPoints.get( driverWayPoints.size() - 1 ) ); // compute airline if both waypoints are valid and also if only one waypoint ist valid double airlineTotalRoute = 0; if( query.getDestinationLocation() != null && query.getStartLocation() != null) { airlineTotalRoute = driverWayPoints.get(0).distanceFrom(query.getStartLocation()) + query.getStartLocation().distanceFrom(query.getDestinationLocation()) + query.getDestinationLocation().distanceFrom(driverWayPoints.get(driverWayPoints.size() - 1)); } else if( query.getDestinationLocation() != null ) airlineTotalRoute = driverWayPoints.get(0).distanceFrom(query.getDestinationLocation()) + query.getDestinationLocation().distanceFrom(driverWayPoints.get(driverWayPoints.size() - 1)); else { airlineTotalRoute = driverWayPoints.get(0).distanceFrom(query.getStartLocation()) + query.getStartLocation().distanceFrom(driverWayPoints.get(driverWayPoints.size() - 1)); } logManager.d("airlines compared: driverRoute: " + airlineDriverRoute + " totalRoute: " + airlineTotalRoute + " distance: " + (airlineTotalRoute - airlineDriverRoute) ); if( (airlineTotalRoute - airlineDriverRoute) > offer.getMaxDiversionInMeters() * 10 ) { logManager.w("REQUEST REJECTED BY AIRLINE DISTANCES"); return false; } return true; } protected void assertUpdatedDriverRoute(TripOffer offer) { Route driverRoute = offer.getDriverRoute(); if( driverRoute.getLastUpdateTimeInSeconds() < offer.getLastPositonUpdateInSeconds() ) { logManager.d(offer.getId() + ": driver route is out of date. Updating route..."); List<Route> updatedDriverRoutes = directionsManager.getDirections(offer.getCurrentLocation(), driverRoute.getWayPoints().get(driverRoute.getWayPoints().size() - 1)); if( updatedDriverRoutes == null || updatedDriverRoutes.isEmpty() ) { // that's quite bad; we will use the old route for matching for now. logManager.e("Could not compute a route for the driver after route update."); } else { driverRoute = updatedDriverRoutes.get(0); offer = new TripOffer( offer.getId(), driverRoute, System.currentTimeMillis()/1000+driverRoute.getDurationInSeconds(), offer.getCurrentLocation(), offer.getMaxDiversionInMeters(), offer.getPricePerKmInCents(), offer.getDriver(), offer.getVehicle(), offer.getStatus(), offer.getLastPositonUpdateInSeconds() ); tripOfferDAO.update(offer); } } } protected boolean assertRouteWithinPassengerMaxWaitingTime( TripOffer offer, TripQuery query, List<UserWayPoint> userWayPoints) { // ignore may waiting time if is -1. Use it, if you want to ignore max waiting times if( query.getMaxWaitingTimeInSeconds() == TripQuery.IGNORE_MAX_WAITING_TIME ) { logManager.d("Ignore Max waiting time"); return true; } // check max waiting time for each passenger for (UserWayPoint userWayPoint : userWayPoints) { if (!userWayPoint.isStartOfTrip()) continue; if (userWayPoint.getUser().equals(offer.getDriver())) continue; long passengerMaxWaitingTimestamp = 0; long passengerStartTimestamp = 0; if (userWayPoint.getUser().equals(query.getPassenger())) { passengerMaxWaitingTimestamp = query.getCreationTimestamp() + query.getMaxWaitingTimeInSeconds(); passengerStartTimestamp = query.getCreationTimestamp(); } else { for (JoinTripRequest joinTripRequest : joinTripRequestDAO.findByOfferId(offer.getId())) { TripQuery foundQuery = joinTripRequest.getSuperTrip().getQuery(); if (userWayPoint.getUser().equals(foundQuery.getPassenger())) { passengerMaxWaitingTimestamp = foundQuery.getCreationTimestamp() + foundQuery.getMaxWaitingTimeInSeconds(); passengerStartTimestamp = foundQuery.getCreationTimestamp(); break; } } } logManager.d("Passenger would have to wait " + (userWayPoint.getArrivalTimestamp() - passengerStartTimestamp) + "s. His max waiting time is: " + query.getMaxWaitingTimeInSeconds() + " -- driver will arrive at:" + new Date(userWayPoint.getArrivalTimestamp() * 1000).toLocaleString() + ", passenger will start his trip at: " + new Date(query.getCreationTimestamp() * 1000).toLocaleString()); // driver may not come before passenger is at his starting position (a small bias for the case that driver and passenger are at the same place) if( userWayPoint.getArrivalTimestamp() - passengerStartTimestamp < -20 ) return false; // passenger may not wait longer than his max waiting time if (userWayPoint.getArrivalTimestamp() > passengerMaxWaitingTimestamp) return false; } return true; } /** * Will compute a list of cheapest {@link org.croudtrip.api.trips.SuperTripReservation} for a * specific query out of a list of potential matches * @param query the query you want to get a match for * @param potentialMatches the list of potential matches for this query * @return a list of reservations for this query with the cheapest price */ private List<SuperTripReservation> findCheapestMatch(TripQuery query, List<PotentialMatch> potentialMatches) { if (potentialMatches.isEmpty()) return new ArrayList<>(); // sort by price per km Collections.sort(potentialMatches, new Comparator<PotentialMatch>() { @Override public int compare(PotentialMatch pm1, PotentialMatch pm2) { return Integer.valueOf(pm1.getOffer().getPricePerKmInCents()).compareTo(pm2.getOffer().getPricePerKmInCents()); } }); List<PotentialMatch> matches = new ArrayList<>(); // find prices int lowestPricePerKmInCents = potentialMatches.get(0).getOffer().getPricePerKmInCents(), secondLowestPricePerKmInCents = -1; for (PotentialMatch potentialMatch : potentialMatches) { if (potentialMatch.getOffer().getPricePerKmInCents() == lowestPricePerKmInCents) { // all cheapest trips are matches matches.add(potentialMatch); } else if (potentialMatch.getOffer().getPricePerKmInCents() != secondLowestPricePerKmInCents) { // second cheapest determines price secondLowestPricePerKmInCents = potentialMatch.getOffer().getPricePerKmInCents(); break; } } // calculate final price int pricePerKmInCents = lowestPricePerKmInCents; if (secondLowestPricePerKmInCents != -1) pricePerKmInCents = secondLowestPricePerKmInCents; int totalPriceInCents = (int) (pricePerKmInCents * query.getRouteDistanceDuration().getDistanceInMeters() / 1000); // create price reservations List<SuperTripReservation> reservations = new ArrayList<>(); for (PotentialMatch match : matches) { reservations.add(new SuperTripReservation.Builder() .setQuery(query) .addReservation(new TripReservation( new SuperTripSubQuery(query), totalPriceInCents, match.getOffer().getPricePerKmInCents(), match.getOffer().getId(), match.getTotalRouteNavigationResult().getEstimatedTripDurationInSecondsForUser( query.getPassenger() ), match.getOffer().getDriver()) ) .build()); } return reservations; } }