/*
* The CroudTrip! application aims at revolutionizing the car-ride-sharing market with its easy,
* user-friendly and highly automated way of organizing shared Trips. Copyright (C) 2015 Nazeeh Ammari,
* Philipp Eichhorn, Ricarda Hohn, Vanessa Lange, Alexander Popp, Frederik Simon, Michael Weber
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU
* Affero General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package org.croudtrip.trips;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import org.croudtrip.account.VehicleManager;
import org.croudtrip.api.account.User;
import org.croudtrip.api.account.Vehicle;
import org.croudtrip.api.directions.Route;
import org.croudtrip.api.directions.RouteDistanceDuration;
import org.croudtrip.api.directions.RouteLocation;
import org.croudtrip.api.gcm.GcmConstants;
import org.croudtrip.api.trips.JoinTripRequest;
import org.croudtrip.api.trips.JoinTripStatus;
import org.croudtrip.api.trips.RunningTripQuery;
import org.croudtrip.api.trips.SuperTrip;
import org.croudtrip.api.trips.SuperTripReservation;
import org.croudtrip.api.trips.TripOffer;
import org.croudtrip.api.trips.TripOfferDescription;
import org.croudtrip.api.trips.TripOfferStatus;
import org.croudtrip.api.trips.TripOfferUpdate;
import org.croudtrip.api.trips.TripQuery;
import org.croudtrip.api.trips.TripQueryDescription;
import org.croudtrip.api.trips.TripQueryResult;
import org.croudtrip.api.trips.TripReservation;
import org.croudtrip.api.trips.UserWayPoint;
import org.croudtrip.db.JoinTripRequestDAO;
import org.croudtrip.db.SuperTripDAO;
import org.croudtrip.db.SuperTripReservationDAO;
import org.croudtrip.db.TripOfferDAO;
import org.croudtrip.directions.DirectionsManager;
import org.croudtrip.directions.RouteNotFoundException;
import org.croudtrip.gcm.GcmManager;
import org.croudtrip.logs.LogManager;
import org.croudtrip.utils.Assert;
import org.croudtrip.utils.Pair;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class TripsManager {
private final TripOfferDAO tripOfferDAO;
private final SuperTripReservationDAO superTripReservationDAO;
private final SuperTripDAO superTripDAO;
private final JoinTripRequestDAO joinTripRequestDAO;
private final DirectionsManager directionsManager;
private final VehicleManager vehicleManager;
private final GcmManager gcmManager;
private final TripsMatcher tripMatcher;
private final RunningTripQueriesManager runningTripQueriesManager;
private final TripsUtils tripsUtils;
private final LogManager logManager;
@Inject
TripsManager(
TripOfferDAO tripOfferDAO,
SuperTripReservationDAO superTripReservationDAO,
SuperTripDAO superTripDAO,
JoinTripRequestDAO joinTripRequestDAO,
DirectionsManager directionsManager,
VehicleManager vehicleManager,
GcmManager gcmManager,
TripsMatcher tripMatcher,
RunningTripQueriesManager runningTripQueriesManager,
TripsUtils tripsUtils,
LogManager logManager) {
this.tripOfferDAO = tripOfferDAO;
this.superTripReservationDAO = superTripReservationDAO;
this.superTripDAO = superTripDAO;
this.joinTripRequestDAO = joinTripRequestDAO;
this.directionsManager = directionsManager;
this.vehicleManager = vehicleManager;
this.gcmManager = gcmManager;
this.runningTripQueriesManager = runningTripQueriesManager;
this.tripMatcher = tripMatcher;
this.tripsUtils = tripsUtils;
this.logManager = logManager;
}
/**
* Adds an offer to the database.
* @param owner The driver that offers the trip
* @param description the description of the offer.
* @return the newly created {@link org.croudtrip.api.trips.TripOffer}
* @throws RouteNotFoundException is thrown, if there was no route from the requested start to
* the requested destination.
*/
public TripOffer addOffer(User owner, TripOfferDescription description) throws RouteNotFoundException {
logManager.d("Searching for routes");
// check if there is a route
List<Route> route = directionsManager.getDirections(description.getStart(), description.getEnd());
if (route.size() == 0) throw new RouteNotFoundException();
logManager.d("Found " + route.size() + " routes to " + description.getEnd());
// find vehicle
Optional<Vehicle>vehicle = vehicleManager.findVehicleById(description.getVehicleId());
Assert.assertTrue(vehicle.isPresent() && vehicle.get().getOwner().getId() == owner.getId(), "no vehilce for id " + description.getVehicleId());
// create and store offer
TripOffer offer = new TripOffer(
0,
route.get(0),
System.currentTimeMillis()/1000+route.get(0).getDurationInSeconds(),
description.getStart(),
description.getMaxDiversionInMeters(),
description.getPricePerKmInCents(),
owner,
vehicle.get(),
TripOfferStatus.ACTIVE,
System.currentTimeMillis()/1000);
tripOfferDAO.save(offer);
// check background search
runningTripQueriesManager.checkAndUpdateRunningQueries(offer);
return offer;
}
/**
* Lists all the offers that belong to a specific driver.
* @param driver The driver that you want to know the offers from.
* @return A list of {@link org.croudtrip.api.trips.TripOffer} with all the offers of the driver.
*/
public List<TripOffer> findOffersByDriver(User driver) {
return tripOfferDAO.findByDriverId(driver.getId());
}
/**
* Finds an offer by its id.
* @param offerId the id of the offer.
* @return An {@link com.google.common.base.Optional} that contains the offer if it exists.
*/
public Optional<TripOffer> findOffer(long offerId) {
return tripOfferDAO.findById(offerId);
}
/**
* Deletes an offer from the database
* @param offer the offer that should be deleted.
*/
public void deleteOffer(TripOffer offer) {
tripOfferDAO.delete(offer);
}
/**
* Update the status of an offer.
* @param offer the offer that should be updated.
* @param offerUpdate the update status that should be applied to the offer.
* @return The updated offer.
*/
public TripOffer updateOffer(TripOffer offer, TripOfferUpdate offerUpdate) {
RouteLocation newStart;
TripOfferStatus newStatus;
if (offerUpdate.getFinishOffer()) {
// finish offer
newStart = offer.getCurrentLocation();
newStatus = TripOfferStatus.FINISHED;
// decline pending passengers
for (JoinTripRequest request : findAllJoinRequests(offer.getId())) {
if (request.getStatus().equals(JoinTripStatus.PASSENGER_ACCEPTED)) {
updateJoinRequestAcceptance(request, false);
}
}
} else if (offerUpdate.getCancelOffer()) {
// cancel trip
newStart = offer.getCurrentLocation();
newStatus = TripOfferStatus.CANCELLED;
for (JoinTripRequest request : findAllJoinRequests(offer.getId())) {
if (request.getStatus().equals(JoinTripStatus.PASSENGER_ACCEPTED)) {
// decline pending passengers
updateJoinRequestAcceptance(request, false);
} else if (request.getStatus().equals(JoinTripStatus.DRIVER_ACCEPTED)) {
// alert accepted passengers
JoinTripRequest updatedRequest = new JoinTripRequest(request, JoinTripStatus.DRIVER_CANCELLED);
joinTripRequestDAO.update(updatedRequest);
gcmManager.sendDriverCancelledTripMsg(offer, request.getSuperTrip().getQuery().getPassenger());
}
}
} else {
// update start location
newStart = offerUpdate.getUpdatedStart();
newStatus = offer.getStatus();
}
// update and store offer
TripOffer updatedOffer = new TripOffer(
offer.getId(),
offer.getDriverRoute(),
offer.getEstimatedArrivalTimeInSeconds(),
newStart,
offer.getMaxDiversionInMeters(),
offer.getPricePerKmInCents(),
offer.getDriver(),
offer.getVehicle(),
newStatus,
System.currentTimeMillis() / 1000);
tripOfferDAO.update(updatedOffer);
return updatedOffer;
}
/**
* Requests a query for offers that match for this passenger.
* @param passenger The passenger that wants to join a trip.
* @param queryDescription The description of his query
* @return A {@link org.croudtrip.api.trips.TripQueryResult} for this request-
* @throws RouteNotFoundException is thrown, if there is no route from the passenger's starting position to his final destination.
*/
public TripQueryResult queryOffers(User passenger, TripQueryDescription queryDescription) throws RouteNotFoundException {
logManager.d("QUERY OFFER: User " + passenger.getId() + " (" + passenger.getFirstName() + " " + passenger.getLastName() + ") from " + queryDescription.getStart() + " to " + queryDescription.getEnd() + " with " + " max waiting time: " + queryDescription.getMaxWaitingTimeInSeconds());
System.out.println("QUERRY OFFER");
// compute passenger route + query
RouteDistanceDuration possiblePassengerRoutes = directionsManager.getDistanceAndDurationForDirection(queryDescription.getStart(), queryDescription.getEnd());
long queryCreationTimestamp = System.currentTimeMillis() / 1000;
TripQuery query = new TripQuery(possiblePassengerRoutes, queryDescription.getStart(), queryDescription.getEnd(), queryDescription.getMaxWaitingTimeInSeconds(), queryCreationTimestamp, passenger);
System.out.println("QUERRY OFFER: before matching");
// try finding match (regular + super)
List<SuperTripReservation> reservations = tripMatcher.findPotentialTrips(tripOfferDAO.findAllActive(), query);
for (SuperTripReservation reservation : reservations) {
logManager.d("Found a super trip: ");
for (TripReservation res : reservation.getReservations()) {
logManager.d("Driver: " + res.getDriver().getFirstName());
}
}
System.out.println("QUERRY OFFER: after matching");
// store all reservations in database
for (SuperTripReservation reservation : reservations) superTripReservationDAO.save(reservation);
// if no reservations start "background search"
RunningTripQuery runningQuery = null;
System.out.println("QUERRY OFFER: running query");
if (reservations.isEmpty()) {
runningQuery = runningTripQueriesManager.startRunningQuery(query);
}
System.out.println("QUERRY OFFER: after running query");
return new TripQueryResult(reservations, runningQuery);
}
/**
* Get a list of all the reservations that exists.
* @return returns all trip reservations from the database.
*/
public List<SuperTripReservation> findAllReservations() {
return superTripReservationDAO.findAll();
}
/**
* Find a specific reservation by its id.
* @param reservationId the id of the {@link org.croudtrip.api.trips.SuperTripReservation} that you want to get.
* @return An {@link com.google.common.base.Optional} that contains the reservation
*/
public Optional<SuperTripReservation> findReservation(long reservationId) {
return superTripReservationDAO.findById(reservationId);
}
/**
* Join a specific offer by a {@link org.croudtrip.api.trips.SuperTripReservation}.
* A {@link SuperTrip} for this query will be created and a
* notification is sent to the drivers (might be multiple!), that they have to either decline or accept.
* @param tripReservation The reservation for that the user want to join the trip
* @return An {@link com.google.common.base.Optional} that contains a join request if the
* reservation was still valid.
*/
public Optional<SuperTrip> joinTrip(SuperTripReservation tripReservation) {
// remove reservation (either it has now been accepted or is can be discarded)
superTripReservationDAO.delete(tripReservation);
// find and check all trips
List<SimpleTripsMatcher.PotentialMatch> matches = new ArrayList<>();
for( TripReservation reservation : tripReservation.getReservations() ) {
// find related offer in reservation
Optional<TripOffer> offerOptional = tripOfferDAO.findById(reservation.getOfferId());
if (!offerOptional.isPresent()) return Optional.absent();
TripOffer offer = offerOptional.get();
logManager.d("Found offer: " + offer.getId());
// check if the offer is still a valid match for this request (newly accepted requests may change this)
TripQuery temporalQuery = new TripQuery( null, reservation.getSubQuery().getStartLocation(), reservation.getSubQuery().getDestinationLocation(), TripQuery.IGNORE_MAX_WAITING_TIME, tripReservation.getQuery().getCreationTimestamp(), tripReservation.getQuery().getPassenger() );
Optional<SimpleTripsMatcher.PotentialMatch> potentialMatch = tripMatcher.isPotentialMatch(offer, temporalQuery);
if (!potentialMatch.isPresent()) {
logManager.d("Query is no potential match anymore for: " + offer.getId());
logManager.d("StartLocation: " + temporalQuery.getStartLocation().toString());
logManager.d("DestLocation: " + temporalQuery.getDestinationLocation().toString());
logManager.d("Query is no potential match anymore for: " + offer.getId() + "finish");
return Optional.absent();
}
matches.add(potentialMatch.get());
}
logManager.d("All trips are possible.");
// send notifications to all drivers
List<JoinTripRequest> jtRequests = new ArrayList<>();
SuperTrip superTrip = new SuperTrip.Builder().setQuery(tripReservation.getQuery()).build();
superTripDAO.save(superTrip);
for( int i = 0; i < matches.size(); ++i ) {
SimpleTripsMatcher.PotentialMatch match = matches.get(i);
// find estimated arrival time in list
long arrivalTimestamp = 0;
logManager.d("Potential match has " + match.getUserWayPoints().size() + " wps");
for( UserWayPoint wp : match.getUserWayPoints()) {
if( wp.getUser().getId() == tripReservation.getQuery().getPassenger().getId() ) {
arrivalTimestamp = wp.getArrivalTimestamp();
break;
}
}
// update join request
JoinTripRequest joinTripRequest = new JoinTripRequest(
0,
tripReservation.getReservations().get(i).getTotalPriceInCents(),
tripReservation.getReservations().get(i).getPricePerKmInCents(),
arrivalTimestamp,
match.getOffer(),
JoinTripStatus.PASSENGER_ACCEPTED,
superTrip,
tripReservation.getReservations().get(i).getSubQuery());
superTrip.addJoinRequest(joinTripRequest);
joinTripRequestDAO.save(joinTripRequest);
superTripDAO.update(superTrip);
// send push notification to driver
gcmManager.sendGcmMessageToUser(match.getOffer().getDriver(), GcmConstants.GCM_MSG_JOIN_REQUEST,
new Pair<String, String>(GcmConstants.GCM_MSG_JOIN_REQUEST, "There is a new request to join your trip"),
new Pair<String, String>(GcmConstants.GCM_MSG_USER_MAIL, "" + match.getOffer().getDriver().getEmail()),
new Pair<String, String>(GcmConstants.GCM_MSG_JOIN_REQUEST_ID, "" + joinTripRequest.getId()),
new Pair<String, String>(GcmConstants.GCM_MSG_JOIN_REQUEST_OFFER_ID, "" + match.getOffer().getId()));
}
return Optional.of(superTrip);
}
/**
* Returns a single {@link SuperTrip}.
*/
public Optional<SuperTrip> findTrip(long tripId) {
return superTripDAO.findById(tripId);
}
/**
* Returns all {@link SuperTrip}s that belong to a passenger.
*/
public List<SuperTrip> findAllTrips(User passenger) {
return superTripDAO.findByPassengerId(passenger.getId());
}
/**
* Returns all active (not cancelled by the passenger, passenger has not reached his destination)
* {@link SuperTrip}s that belong to a passenger.
*/
public List<SuperTrip> findAllActiveTrips(User passenger) {
List<SuperTrip> superTrips = superTripDAO.findByPassengerId(passenger.getId());
List<SuperTrip> activeSuperTrips = new ArrayList<>();
for( SuperTrip superTrip : superTrips ) {
if( superTrip.isActive() )
activeSuperTrips.add( superTrip );
}
return activeSuperTrips;
}
/**
* Returns all {@link JoinTripRequest}s, regardless of their state or user.
*/
public List<JoinTripRequest> findAllJoinRequests() {
return joinTripRequestDAO.findAll();
}
/**
* Returns all JoinRequests that are related to a specific user and possibly have as status
* {@link org.croudtrip.api.trips.JoinTripStatus#PASSENGER_ACCEPTED}.
* @param passengerOrDriver the user to which all JoinTripRequests should belong.
* @param showOnlyPassengerAccepted true if you want only get all the requests that are accepted by passenger side.
* @return a list of JoinTripRequests.
*/
public List<JoinTripRequest> findAllJoinRequests(User passengerOrDriver, boolean showOnlyPassengerAccepted) {
if (showOnlyPassengerAccepted) return joinTripRequestDAO.findByUserIdAndStatusPassengerAccepted(passengerOrDriver.getId());
else return joinTripRequestDAO.findByUserId(passengerOrDriver.getId());
}
/**
* Finds all {@link org.croudtrip.api.trips.JoinTripRequest} that are related to a certain offer.
* @param offerId the offer id for that all JoinTripRequests should be found.
* @return A List of JoinTripRequests for this specific offer.
*/
public List<JoinTripRequest> findAllJoinRequests(long offerId) {
return joinTripRequestDAO.findByOfferId(offerId);
}
/**
* Find all the {@link org.croudtrip.api.trips.JoinTripRequest} that were accepted by the driver (but are not in the car).
* @param passengerOrDriver Either the driver or the passenger that are related to the JoinTripRequest
* @return A list of JoinTripRequests that have as status {@link org.croudtrip.api.trips.JoinTripStatus#DRIVER_ACCEPTED}
* and are related to the passed user.
*/
public List<JoinTripRequest> findDriverAcceptedJoinRequests(User passengerOrDriver) {
return joinTripRequestDAO.findByUserIdAndStatusDriverAccepted(passengerOrDriver.getId());
}
/**
* Finds a {@link org.croudtrip.api.trips.JoinTripRequest} by its id.
* @param joinRequestId the id of the JoinTripRequest
* @return An {@link com.google.common.base.Optional} that contains the respective JoinTripRequest
* if it exists.
*/
public Optional<JoinTripRequest> findJoinRequest(long joinRequestId) {
return joinTripRequestDAO.findById(joinRequestId);
}
/**
* Updates a running join request if the driver either accepts or decline the passenger
* @param joinRequest The join request that needs to be updated.
* @param passengerAccepted true if the passenger should be accepted, false if not.
* @return the updated join request.
*/
public JoinTripRequest updateJoinRequestAcceptance(JoinTripRequest joinRequest, boolean passengerAccepted) {
Assert.assertTrue(joinRequest.getStatus().equals(JoinTripStatus.PASSENGER_ACCEPTED), "cannot modify join request");
// update join request status
JoinTripStatus newStatus;
if (passengerAccepted) newStatus = JoinTripStatus.DRIVER_ACCEPTED;
else newStatus = JoinTripStatus.DRIVER_DECLINED;
JoinTripRequest updatedRequest = new JoinTripRequest(joinRequest, newStatus);
joinTripRequestDAO.update(updatedRequest);
// notify the passenger about status
User passenger = joinRequest.getSuperTrip().getQuery().getPassenger();
logManager.d("User " + passenger.getId() + " (" + passenger.getFirstName() + " " + passenger.getLastName() + ") got status update for joinTripRequest.");
if(passengerAccepted) gcmManager.sendAcceptPassengerMsg(joinRequest);
else gcmManager.sendDeclinePassengerMsg(joinRequest);
// TODO: Check if other pending join requests are still valid
// Send all the passengers an arrival time update
if(passengerAccepted) {
tripsUtils.updateArrivalTimesForOffer(joinRequest.getOffer(), passenger);
}
return joinTripRequestDAO.findById(joinRequest.getId()).get();
}
/**
* Updates a running join request if the passenger has just entered the car.
* @param joinRequest The join request that needs to be updated.
* @return the updated join request.
*/
public JoinTripRequest updateJoinRequestPassengerEnterCar(JoinTripRequest joinRequest) {
JoinTripRequest updatedRequest = new JoinTripRequest(joinRequest, JoinTripStatus.PASSENGER_IN_CAR);
joinTripRequestDAO.update(updatedRequest);
// Send GCM to the driver to notify him that the passenger entered the car
gcmManager.sendPassengerEnterCarMsg(joinRequest);
return updatedRequest;
}
/**
* Updates a running join request if the passenger is at his destination and has left the car.
* @param joinRequest The join request that needs to be updated.
* @return the updated join request.
*/
public JoinTripRequest updateJoinRequestPassengerExitCar(JoinTripRequest joinRequest) {
JoinTripRequest updatedRequest = new JoinTripRequest(joinRequest, JoinTripStatus.PASSENGER_AT_DESTINATION);
joinTripRequestDAO.update(updatedRequest);
// Send GCM to the driver to notify him that the passenger left the car
gcmManager.sendPassengerExitCarMsg(joinRequest);
// check background search
runningTripQueriesManager.checkAndUpdateRunningQueries(joinRequest.getOffer());
return updatedRequest;
}
/**
* Updates a running join request if the passenger cancels the trip from his device.
* @param joinRequest The join request that needs to be updated.
* @return the updated join request.
*/
public JoinTripRequest updateJoinRequestPassengerCancel(JoinTripRequest joinRequest) {
JoinTripStatus oldStatus = joinRequest.getStatus();
if( oldStatus.equals( JoinTripStatus.DRIVER_DECLINED ) || oldStatus.equals( JoinTripStatus.PASSENGER_CANCELLED ))
return joinRequest;
JoinTripRequest updatedRequest = new JoinTripRequest(joinRequest, JoinTripStatus.PASSENGER_CANCELLED);
joinTripRequestDAO.update(updatedRequest);
// if the passenger has already finished or cancelled the trip, don't interact with the driver anymore
if( joinRequest.getOffer().getStatus().equals( TripOfferStatus.CANCELLED )
|| joinRequest.getOffer().getStatus().equals( TripOfferStatus.FINISHED ) )
return joinRequest;
gcmManager.sendPassengerCancelledTripMsg(joinRequest);
// Update all the passenger's arrival time only if the passenger was already accepted by the driver
if( oldStatus != null && oldStatus.equals( JoinTripStatus.DRIVER_ACCEPTED ) )
tripsUtils.updateArrivalTimesForOffer( joinRequest.getOffer() );
// check background search
runningTripQueriesManager.checkAndUpdateRunningQueries(joinRequest.getOffer());
return joinRequest;
}
/**
* Updates a super trip if the passenger cancels the super trip
* @param superTrip the super trip from the server's database that should be cancelled
* @return The updated super trip
*/
public SuperTrip updateSuperTripPassengerCancel( SuperTrip superTrip ) {
for( JoinTripRequest request : superTrip.getJoinRequests() ){
updateJoinRequestPassengerCancel( request );
}
superTripDAO.update( superTrip );
return superTrip;
}
}