package org.croudtrip.trips; import com.google.common.base.Optional; import com.google.maps.model.LatLng; import org.croudtrip.api.directions.NavigationResult; import org.croudtrip.api.directions.RouteDistanceDuration; import org.croudtrip.api.directions.RouteLocation; 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.closestpair.ClosestPair; import org.croudtrip.closestpair.ClosestPairResult; 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 org.croudtrip.places.Place; import org.croudtrip.places.PlaceRanking; import org.croudtrip.places.PlacesApi; import org.croudtrip.places.PlacesManager; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.inject.Inject; /** * Responsible for finding and creating super trips. */ class SuperTripsMatcher extends SimpleTripsMatcher { private static final int MAX_DEPTH = 3; public static class PotentialSuperTripMatch { private final TripOffer offer; private final TripQuery query; private final RouteLocation singleWaypoint; private final NavigationResult diversionNavigationResult; public PotentialSuperTripMatch(TripOffer offer, TripQuery query, RouteLocation singleWaypoint, NavigationResult diversionNavigationResult) { this.offer = offer; this.query = query; this.singleWaypoint = singleWaypoint; this.diversionNavigationResult = diversionNavigationResult; } public TripOffer getOffer() { return offer; } public TripQuery getQuery() { return query; } public RouteLocation getSingleWaypoint() { return singleWaypoint; } public NavigationResult getDiversionNavigationResult() { return diversionNavigationResult; } } private static class PotentialRecursiveSuperTrip { private final PotentialSuperTripMatch pickUpMatch; private final PotentialSuperTripMatch dropMatch; private final ClosestPairResult closestPairResult; public PotentialRecursiveSuperTrip(PotentialSuperTripMatch pickUpMatch, PotentialSuperTripMatch dropMatch, ClosestPairResult closestPairResult) { this.pickUpMatch = pickUpMatch; this.dropMatch = dropMatch; this.closestPairResult = closestPairResult; } public PotentialSuperTripMatch getPickUpMatch() { return pickUpMatch; } public PotentialSuperTripMatch getDropMatch() { return dropMatch; } public ClosestPairResult getClosestPairResult() { return closestPairResult; } } private final ClosestPair closestPair; private final PlacesManager placesManager; @Inject SuperTripsMatcher( JoinTripRequestDAO joinTripRequestDAO, TripOfferDAO tripOfferDAO, TripsNavigationManager tripsNavigationManager, DirectionsManager directionsManager, TripsUtils tripsUtils, ClosestPair closestPair, PlacesManager placesManager, LogManager logManager) { super(joinTripRequestDAO, tripOfferDAO, tripsNavigationManager, directionsManager, tripsUtils, logManager); this.closestPair = closestPair; this.placesManager = placesManager; } @Override public List<SuperTripReservation> findPotentialTrips(List<TripOffer> offers, TripQuery query) { return findPotentialTrips(offers, query, 0); } private List<SuperTripReservation> findPotentialTrips(List<TripOffer> offers, TripQuery query, int currentDepth) { if( currentDepth > MAX_DEPTH ) return new ArrayList<>(); List<SuperTripReservation> simpleReservations = super.findPotentialTrips(offers, query); if (!simpleReservations.isEmpty()) return simpleReservations; logManager.d("STARTED SEARCHING FOR SUPER TRIPS"); // compute all offers that would be able to pickup or drop the passenger along their route List<TripOffer> potentialSuperTripPickUpMatches = new ArrayList<>(); List<TripOffer> potentialSuperTripDropMatches = new ArrayList<>(); for( TripOffer offer : offers ) { boolean pickupMatch = isRoughPotentialSuperTripMatchForOneWaypoint(offer, query, true); if( pickupMatch ) potentialSuperTripPickUpMatches.add( offer ); boolean dropMatch = isRoughPotentialSuperTripMatchForOneWaypoint(offer, query, false); if( dropMatch ) potentialSuperTripDropMatches.add( offer ); } logManager.d("found " + potentialSuperTripPickUpMatches.size() + " rough pickUp matches." ); logManager.d("found " + potentialSuperTripDropMatches.size() + " rough drop matches." ); // compute the closest pair of those trip offers. As soon as we find a matching pair, we return the super trip List<SuperTripReservation> reservations = new ArrayList<>(); List<PotentialRecursiveSuperTrip> potentialRecursiveSuperTrips = new ArrayList<>(); pickUp: for( TripOffer pickUpOffer : potentialSuperTripPickUpMatches ) { // now do the real check if the trip offer is a potential match Optional<PotentialSuperTripMatch> tripMatchOptional = isPotentialSuperTripMatchForOneWaypoint( pickUpOffer, query, true ); if( !tripMatchOptional.isPresent() ) continue; PotentialSuperTripMatch pickUpMatch = tripMatchOptional.get(); for( TripOffer dropOffer : potentialSuperTripDropMatches ) { // don't match the same offer to itself if( dropOffer.getId() == pickUpOffer.getId() ) continue; // now do the real check if the trip offer is a potential match tripMatchOptional = isPotentialSuperTripMatchForOneWaypoint( dropOffer, query, false ); if( !tripMatchOptional.isPresent() ) continue; PotentialSuperTripMatch dropMatch = tripMatchOptional.get(); // find the closest pair for both routes ClosestPairResult closestPairResult = closestPair.findClosestPair(query.getPassenger(), pickUpMatch.getDiversionNavigationResult(), dropMatch.getDiversionNavigationResult()); if( closestPairResult.getDropLocation() == null || closestPairResult.getPickupLocation() == null ) continue; // TODO: If closest pair distance is greater thant the max diversion of the driver we don't have to check if it is a valid reserveration since it simply cannot be. // check if one of the drivers may take the diversion to drive to the connection point Optional<SuperTripReservation> reservation = isValidReservation( pickUpMatch, dropMatch, query, closestPairResult ); if( reservation.isPresent() ){ reservations.add( reservation.get() ); break pickUp; } // no reservation has been found, but we can try to find a recursive super trip for this match later on. potentialRecursiveSuperTrips.add( new PotentialRecursiveSuperTrip( pickUpMatch, dropMatch, closestPairResult ) ); } } // if we have not found a regular super trip, we may find one using recursive search if( reservations.isEmpty() && currentDepth < MAX_DEPTH ) { for( PotentialRecursiveSuperTrip recursiveSuperTrip : potentialRecursiveSuperTrips ){ // adjust passenger route from pickup to drop location RouteDistanceDuration passengerDistanceDuration = directionsManager.getDistanceAndDurationForDirection(recursiveSuperTrip.getClosestPairResult().getPickupLocation(), recursiveSuperTrip.getClosestPairResult().getDropLocation()); // pickup query TripQuery pickupQuery = new TripQuery.Builder() .setPassenger( query.getPassenger() ) .setStartLocation(query.getStartLocation()) .setDestinationLocation(recursiveSuperTrip.getClosestPairResult().getPickupLocation()) .setCreationTimestamp( query.getCreationTimestamp() ) .setMaxWaitingTimeInSeconds(query.getMaxWaitingTimeInSeconds()) .build(); // compute route from passenger start point to passenger destination NavigationResult pickupNavigationResult; try { pickupNavigationResult = tripsNavigationManager.getNavigationResultForOffer( recursiveSuperTrip.getPickUpMatch().getOffer(), pickupQuery ); } catch (RouteNotFoundException e) { e.printStackTrace(); continue; } // long tripDurationInSeconds = pickupNavigationResult.getEstimatedTripDurationInSecondsForUser(pickupQuery.getPassenger()); // compute results from closest pickup point to closes drop point TripQuery adaptedQuery = new TripQuery.Builder().setPasengerRouteDistanceDuration( passengerDistanceDuration ) .setPassenger( query.getPassenger() ) .setStartLocation( recursiveSuperTrip.getClosestPairResult().getPickupLocation() ) .setDestinationLocation( recursiveSuperTrip.getClosestPairResult().getDropLocation() ) .setCreationTimestamp( query.getCreationTimestamp() + tripDurationInSeconds ) /* Just set this value to the arrival timestamp of the driver at the pickup location*/ .setMaxWaitingTimeInSeconds(query.getMaxWaitingTimeInSeconds()) .build(); // temporarily remove found offers from offers list offers.remove(recursiveSuperTrip.pickUpMatch.getOffer()); offers.remove(recursiveSuperTrip.dropMatch.getOffer()); List<SuperTripReservation> recursiveReservations = findPotentialTrips( offers, adaptedQuery, currentDepth + 1 ); offers.add(recursiveSuperTrip.pickUpMatch.getOffer()); offers.add(recursiveSuperTrip.dropMatch.getOffer()); // if no recursive solution was found we can stop further searching if( recursiveReservations.isEmpty() ) continue; for( SuperTripReservation res : recursiveReservations ) { SuperTripReservation.Builder reservationBuilder = new SuperTripReservation.Builder() .setQuery(query) .addReservation(new TripReservation( new SuperTripSubQuery.Builder() .setStartLocation(query.getStartLocation()) .setDestinationLocation(recursiveSuperTrip.getClosestPairResult().getPickupLocation()) .build(), (int)( recursiveSuperTrip.getPickUpMatch().getOffer().getPricePerKmInCents() * (pickupNavigationResult.getEstimatedTripDistanceInMetersForUser(query.getPassenger())/1000) ), recursiveSuperTrip.getPickUpMatch().getOffer().getPricePerKmInCents(), recursiveSuperTrip.getPickUpMatch().getOffer().getId(), pickupNavigationResult.getEstimatedTripDurationInSecondsForUser( query.getPassenger() ), recursiveSuperTrip.pickUpMatch.getOffer().getDriver())); for( TripReservation tripReservation : res.getReservations() ){ reservationBuilder.addReservation( tripReservation ); tripDurationInSeconds += tripReservation.getEstimatedTripDurationInSeconds(); } // drop query TripQuery dropQuery = new TripQuery.Builder() .setPassenger( query.getPassenger() ) .setStartLocation( recursiveSuperTrip.getClosestPairResult().getDropLocation()) .setDestinationLocation(query.getDestinationLocation()) .setCreationTimestamp( query.getCreationTimestamp() + tripDurationInSeconds ) /*estimated arrival time at closest drop point*/ .setMaxWaitingTimeInSeconds(query.getMaxWaitingTimeInSeconds()) .build(); NavigationResult dropNavigationResult; try { dropNavigationResult = tripsNavigationManager.getNavigationResultForOffer( recursiveSuperTrip.getDropMatch().getOffer(), dropQuery ); } catch (RouteNotFoundException e) { e.printStackTrace(); continue; } reservationBuilder.addReservation(new TripReservation( new SuperTripSubQuery.Builder() .setStartLocation(adaptedQuery.getDestinationLocation()) .setDestinationLocation(query.getDestinationLocation()) .build(), (int)( recursiveSuperTrip.getPickUpMatch().getOffer().getPricePerKmInCents() * (dropNavigationResult.getEstimatedTripDistanceInMetersForUser(query.getPassenger())/1000) ), recursiveSuperTrip.dropMatch.getOffer().getPricePerKmInCents(), recursiveSuperTrip.dropMatch.getOffer().getId(), pickupNavigationResult.getEstimatedTripDurationInSecondsForUser( query.getPassenger() ), recursiveSuperTrip.dropMatch.getOffer().getDriver())); reservations.add( reservationBuilder.build()); } } } logManager.d("STOPPED SEARCHING FOR SUPER TRIPS"); return reservations; } protected boolean isRoughPotentialSuperTripMatchForOneWaypoint(TripOffer offer, TripQuery query, boolean useStartWaypoint) { // check trip status if (!offer.getStatus().equals(TripOfferStatus.ACTIVE)) return false; // check that query has not been declined before if (!assertJoinRequestNotDeclined(offer, query)) return false; // check current passenger count int passengerCount = tripsUtils.getActivePassengerCountForOffer(offer); if (passengerCount >= offer.getVehicle().getCapacity()) return false; // create a fake query with only one point to compute matches TripQuery onePointQuery; if( useStartWaypoint) onePointQuery = new TripQuery.Builder().setPassenger(query.getPassenger()).setStartLocation( query.getStartLocation() ).build(); else onePointQuery = new TripQuery.Builder().setPassenger(query.getPassenger()).setDestinationLocation(query.getDestinationLocation()).build(); // early reject based on airline; return assertWithinAirDistance(offer, onePointQuery); } protected Optional<SuperTripReservation> isValidReservation(PotentialSuperTripMatch pickUpMatch, PotentialSuperTripMatch dropMatch, TripQuery query, ClosestPairResult closestPairResult) { Map queryMap = new PlacesApi.QueryMapBuilder().location(new LatLng(closestPairResult.getPickupLocation().getLat(), closestPairResult.getPickupLocation().getLng())) .rankBy(PlaceRanking.RANK_BY_DISTANCE) .build(); List<Place> places = placesManager.getNearbyPlaces( queryMap, 3); queryMap = new PlacesApi.QueryMapBuilder().location(new LatLng( closestPairResult.getDropLocation().getLat(),closestPairResult.getDropLocation().getLng())) .rankBy(PlaceRanking.RANK_BY_DISTANCE) .build(); places.addAll(placesManager.getNearbyPlaces(queryMap, 3)); logManager.d("Got Places next to: " + closestPairResult.getPickupLocation()); logManager.d("Got Places next to: " + closestPairResult.getDropLocation()); for( Place place : places ) { logManager.d("Check place: " + place.getName() + " at " + place.getLocation()); Optional<SuperTripReservation> reservationOptional = isValidReservationForConnectionPoint(query, place.getLocation(), pickUpMatch, dropMatch); if( reservationOptional.isPresent() ) { logManager.d( "Found a connection point at a place: " + place.getName() + " at location: " + place.getLocation() ); return reservationOptional; } } Optional<SuperTripReservation> reservationOptional = isValidReservationForConnectionPoint(query, closestPairResult.getDropLocation(), pickUpMatch, dropMatch); if( reservationOptional.isPresent() ) { logManager.d( "Found a connection point at drop location: " + closestPairResult.getDropLocation() ); return reservationOptional; } return isValidReservationForConnectionPoint(query, closestPairResult.getPickupLocation(), pickUpMatch, dropMatch); } private Optional<SuperTripReservation> isValidReservationForConnectionPoint(TripQuery query, RouteLocation connectionPoint, PotentialSuperTripMatch pickUpMatch, PotentialSuperTripMatch dropMatch) { // compute the time, when the first driver will be at his connection point and check if it's a valid match TripQuery adaptedQuery = new TripQuery.Builder().setPassenger(query.getPassenger()) .setStartLocation(query.getStartLocation()) .setDestinationLocation(connectionPoint) .setCreationTimestamp(query.getCreationTimestamp()) .setMaxWaitingTimeInSeconds(query.getMaxWaitingTimeInSeconds()) .build(); Optional<PotentialMatch> potentialMatch = isPotentialMatch(pickUpMatch.getOffer(), adaptedQuery); if( !potentialMatch.isPresent() ) return Optional.absent(); long estimatedPickupDuration = potentialMatch.get().getTotalRouteNavigationResult().getEstimatedTripDurationInSecondsForUser( query.getPassenger() ); // compute the time, when the second driver will be at the connection point and check if it's in range and a valid match adaptedQuery = new TripQuery.Builder().setPassenger(query.getPassenger()) .setStartLocation(connectionPoint) .setDestinationLocation(query.getDestinationLocation()) .setCreationTimestamp( query.getCreationTimestamp() + estimatedPickupDuration ) .setMaxWaitingTimeInSeconds( query.getMaxWaitingTimeInSeconds() ) .build(); potentialMatch = isPotentialMatch(dropMatch.getOffer(), adaptedQuery); if( !potentialMatch.isPresent() ) return Optional.absent(); long estimatedDropDuration = potentialMatch.get().getTotalRouteNavigationResult().getEstimatedTripDurationInSecondsForUser(query.getPassenger()); // don't use direction calls here, but use distance matrix calls RouteDistanceDuration passengerPickUp = directionsManager.getDistanceAndDurationForDirection(query.getStartLocation(), adaptedQuery.getStartLocation()); RouteDistanceDuration passengerDrop = directionsManager.getDistanceAndDurationForDirection(adaptedQuery.getStartLocation(), query.getDestinationLocation()); int totalPickUpPriceInCents = (int) (pickUpMatch.getOffer().getPricePerKmInCents() * passengerPickUp.getDistanceInMeters() / 1000); int totalDropPriceInCents = (int) (dropMatch.getOffer().getPricePerKmInCents() * passengerDrop.getDistanceInMeters() / 1000); logManager.d("SuperTripReservation: " + totalPickUpPriceInCents + "ct, " + totalDropPriceInCents + "ct"); return Optional.of( createSuperTripReservation( query, connectionPoint, pickUpMatch.getOffer(), dropMatch.getOffer(), totalPickUpPriceInCents, estimatedPickupDuration, totalDropPriceInCents, estimatedDropDuration) ); } private SuperTripReservation createSuperTripReservation( TripQuery query, RouteLocation connectionPoint, TripOffer pickUpOffer, TripOffer dropOffer, int totalPickUpPriceInCents, long estimatedPickupDuration, int totalDropPriceInCents, long estimatedDropDuration) { return new SuperTripReservation.Builder() .setQuery(query) .addReservation(new TripReservation.Builder() .setSubQuery(new SuperTripSubQuery.Builder() .setStartLocation(query.getStartLocation()) .setDestinationLocation(connectionPoint) .build()) .setTotalPriceInCents(totalPickUpPriceInCents) .setPricePerKmInCents(pickUpOffer.getPricePerKmInCents()) .setOfferId(pickUpOffer.getId()) .setEstimatedTripDurationInSeconds(estimatedPickupDuration) .setDriver(pickUpOffer.getDriver()) .build()) .addReservation(new TripReservation.Builder() .setSubQuery(new SuperTripSubQuery.Builder() .setStartLocation(connectionPoint) .setDestinationLocation(query.getDestinationLocation()) .build()) .setTotalPriceInCents(totalDropPriceInCents) .setPricePerKmInCents(dropOffer.getPricePerKmInCents()) .setOfferId(dropOffer.getId()) .setEstimatedTripDurationInSeconds(estimatedDropDuration) .setDriver(dropOffer.getDriver()) .build()) .build(); } /** * Checks if the given offer is a potential match for the given query for one particular waypoint * It will be used in super trips to compute offers that are able to pick up or drop passenger. * @param offer The offer that should be checked * @param query The query that should be checked * @return If the trip is a potential match a {@link SuperTripsMatcher.PotentialSuperTripMatch} will be returned. */ protected Optional<SuperTripsMatcher.PotentialSuperTripMatch> isPotentialSuperTripMatchForOneWaypoint( TripOffer offer, TripQuery query, boolean useStartWaypoint ) { if( !isRoughPotentialSuperTripMatchForOneWaypoint( offer, query, useStartWaypoint ) ) return Optional.absent(); // update driver route on new position update assertUpdatedDriverRoute(offer); // create a fake query with only one point to compute matches TripQuery onePointQuery; if( useStartWaypoint) onePointQuery = new TripQuery.Builder().setPassenger(query.getPassenger()).setStartLocation( query.getStartLocation() ).build(); else onePointQuery = new TripQuery.Builder().setPassenger(query.getPassenger()).setDestinationLocation(query.getDestinationLocation()).build(); // get complete new route NavigationResult navigationResult; try { navigationResult = tripsNavigationManager.getNavigationResultForOffer(offer, onePointQuery); if (navigationResult.getUserWayPoints().isEmpty()) return Optional.absent(); } catch (RouteNotFoundException e) { return Optional.absent(); } // check if the user is picked up in time if( useStartWaypoint) if (!assertRouteWithinPassengerMaxWaitingTime(offer, query, navigationResult.getUserWayPoints())) return Optional.absent(); // check if passenger route is within max diversion long distanceToDriverInMeters = navigationResult.getUserWayPoints().get(navigationResult.getUserWayPoints().size() - 1).getDistanceToDriverInMeters(); if (distanceToDriverInMeters - offer.getDriverRoute().getDistanceInMeters() > offer.getMaxDiversionInMeters()) return Optional.absent(); return Optional.of( new SuperTripsMatcher.PotentialSuperTripMatch( offer, query, useStartWaypoint ? query.getStartLocation() : query.getDestinationLocation(), navigationResult )); } }