diff --git a/gradle.properties b/gradle.properties index 3e927b1..a03b354 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,4 @@ android.useAndroidX=true # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 81f6d7a..213a910 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri May 05 09:33:57 WIB 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/vrpsolver/build.gradle b/vrpsolver/build.gradle index 6195d9a..3e3cfe3 100644 --- a/vrpsolver/build.gradle +++ b/vrpsolver/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.12.4' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/DSMSolver.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/DSMSolver.java index fb2c112..2a1e980 100644 --- a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/DSMSolver.java +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/DSMSolver.java @@ -1,7 +1,5 @@ package id.my.dsm.vrpsolver; -import android.util.Log; - import androidx.annotation.NonNull; import java.util.ArrayList; @@ -15,6 +13,11 @@ import id.my.dsm.vrpsolver.model.MatrixElement; import id.my.dsm.vrpsolver.model.Solution; import id.my.dsm.vrpsolver.model.Vehicle; +import id.my.dsm.vrpsolver.optimization.NearestNeighborOptimizer; +import id.my.dsm.vrpsolver.optimization.Optimizer; +import id.my.dsm.vrpsolver.optimization.SavingMatrixOptimizer; +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.utils.MatrixUtils; public class DSMSolver { @@ -22,20 +25,25 @@ public class DSMSolver { private static final String TAG = DSMSolver.class.getSimpleName(); // Internal dependencies - private static OptimizationMethod optimizationMethod = OptimizationMethod.NEAREST_NEIGHBOR; + private Optimizer optimizer; + private Logger logger; + + public DSMSolver(Optimizer optimizer) { + this.optimizer = optimizer; + } - public DSMSolver() { + public void setLogger(Logger logger) { + this.logger = logger; } /** * Optimization builder for DSMSolver, recommended to be invoked under background thread */ - public static class OptimizationBuilder { + public class OptimizationBuilder { private final List matrix; private final List places; private final List vehicles; - private OptimizationMethod optimizationMethod; private boolean isRoundTrip = true; public OptimizationBuilder( @@ -48,11 +56,6 @@ public OptimizationBuilder( this.vehicles = vehicles; } - public OptimizationBuilder withMethod(@NonNull OptimizationMethod optimizationMethod) { - this.optimizationMethod = optimizationMethod; - return this; - } - public OptimizationBuilder withRoundTrip(boolean isRoundTrip) { this.isRoundTrip = isRoundTrip; return this; @@ -60,91 +63,26 @@ public OptimizationBuilder withRoundTrip(boolean isRoundTrip) { public void optimize() { - if (this.optimizationMethod == null) - this.optimizationMethod = DSMSolver.optimizationMethod; - else - DSMSolver.optimizationMethod = this.optimizationMethod; + if (optimizer == null) { + sendOptimizationFailedResponse(OptimizationResponseError.METHOD_NOT_IMPLEMENTED); + return; + } // Pick the first source because NN & SM only accepts one source Location source = Location.Toolbox.getByProfile(places, Location.Profile.SOURCE).get(0); int depotPlaceIndex = places.indexOf(source); - switch (optimizationMethod) { - case NEAREST_NEIGHBOR: - - // Post optimization response event (might take a while to process depends on the algorithm) - sendOptimizationSuccessResponse( - computeCapacitatedNearestNeighborResult( - matrix, - places, - vehicles, - depotPlaceIndex, - isRoundTrip - ) - ); -// EventBus.getDefault().post( -// new OnDSMSolverOptimizationResponse( -// OnDSMSolverOptimizationResponse.Status.SUCCESS, -// computeCapacitatedNearestNeighborResult( -// matrix, -// places, -// vehicles, -// depotPlaceIndex, -// isRoundTrip -// ) -// )); - - break; - case SAVING_MATRIX: - - // Post optimization response event - sendOptimizationSuccessResponse( - computeCapacitatedSavingMatrixResult( - matrix, - places, - vehicles, - depotPlaceIndex, - isRoundTrip - ) - ); -// EventBus.getDefault().post( -// new OnDSMSolverOptimizationResponse( -// OnDSMSolverOptimizationResponse.Status.SUCCESS, -// computeCapacitatedSavingMatrixResult( -// matrix, -// places, -// vehicles, -// depotPlaceIndex, -// isRoundTrip -// ) -// )); - break; - - default: - Log.e(TAG, "optimize: Method not yet implemented"); - - // Post optimization response event - sendOptimizationFailedResponse(OptimizationResponseError.METHOD_NOT_IMPLEMENTED); -// EventBus.getDefault().post( -// new OnDSMSolverOptimizationResponse( -// OnDSMSolverOptimizationResponse.Status.FAILED, -// OnDSMSolverOptimizationResponse.Error.MethodNotImplemented -// ) -// ); - break; - } - - // + sendOptimizationSuccessResponse(optimizer.optimize(matrix, places, vehicles, depotPlaceIndex, isRoundTrip)); } } - private static final List optimizationResponseListeners = new ArrayList<>(); - public static void setOnOptimizationResponseListener(OptimizationResponseListener listener) { + private final List optimizationResponseListeners = new ArrayList<>(); + public void setOnOptimizationResponseListener(OptimizationResponseListener listener) { optimizationResponseListeners.add(listener); } - private static void sendOptimizationSuccessResponse(List solutions) { + private void sendOptimizationSuccessResponse(List solutions) { for (OptimizationResponseListener listener : optimizationResponseListeners) { if (listener != null) listener.onOptimizationSuccess(solutions); @@ -152,7 +90,7 @@ private static void sendOptimizationSuccessResponse(List solutions) { optimizationResponseListeners.remove(listener); } } - private static void sendOptimizationFailedResponse(OptimizationResponseError error) { + private void sendOptimizationFailedResponse(OptimizationResponseError error) { for (OptimizationResponseListener listener : optimizationResponseListeners) { if (listener != null) listener.onOptimizationFailed(error); @@ -162,657 +100,4 @@ private static void sendOptimizationFailedResponse(OptimizationResponseError err } - public static ArrayList getDistanceFromPlaceSequence(ArrayList places, ArrayList distanceValues, ArrayList durationValues) { - - ArrayList matrixElements = new ArrayList<>(); // To keep the result in memory - - boolean isRoundTrip; - - if (places.size() == distanceValues.size() || places.size() == durationValues.size()) - isRoundTrip = true; - else if (places.size() == distanceValues.size() - 1 || places.size() == durationValues.size() - 1) - isRoundTrip = false; - else - return matrixElements; - - for (int i = 0; i < places.size() - 1; i++) { - - Location p = places.get(i); - Location p2 = places.get(i + 1); - - MatrixElement d = new MatrixElement(p, p2, distanceValues.get(i)); - - if (durationValues != null && durationValues.size() > 0) - d.setDuration(durationValues.get(i)); - - matrixElements.add(d); - - } - - if (isRoundTrip) { - // Assume the depot is the first place - MatrixElement d = new MatrixElement(places.get(places.size() - 1), places.get(0), distanceValues.get(distanceValues.size() - 1)); - - if (durationValues != null && durationValues.size() > 0) - d.setDuration(durationValues.get(durationValues.size() - 1)); - - matrixElements.add(d); - - } - - return matrixElements; - } - - // DEPENDENCIES FUNCTIONALITY - - // TODO: Finish thoroughly - public static void calculateDistanceSavingValue(@NonNull List places, List distancesArray) { - - // Pick the first source because Saving Matrix method only accepts one source - Location source = Location.Toolbox.getByProfile(places, Location.Profile.SOURCE).get(0); - int depotId = places.indexOf(source); - - // Saving MatrixElement (Saving matrix equivalent) TODO: Extract depotId as a parameter - for (MatrixElement matrixElement : distancesArray) { - - int oriId = places.indexOf(matrixElement.getOrigin()); - int destId = places.indexOf(matrixElement.getDestination()); - - if (oriId == depotId || destId == depotId) { - continue; - } - - double doi = 0; - double doj = 0; - double dij = 0; - - for (MatrixElement matrixElement2 : distancesArray) { - if (places.indexOf(matrixElement2.getOrigin()) == depotId && places.indexOf(matrixElement2.getDestination()) == oriId) { - doi = matrixElement2.getDistance(); - } else if (places.indexOf(matrixElement2.getOrigin()) == depotId && places.indexOf(matrixElement2.getDestination()) == destId) { - doj = matrixElement2.getDistance(); - } else if (places.indexOf(matrixElement2.getOrigin()) == oriId && places.indexOf(matrixElement2.getDestination()) == destId) { - dij = matrixElement2.getDistance(); - } - } - - double sij = doi + doj - dij; - - Log.d(TAG, "MatrixElement saving: " + doi + " + " + doj + " - " + dij + " = " + sij); - -// if (sij > 0) - matrixElement.setSavingDistance(sij); - - } - - } - - // DSMSolver - - /*** - * Filter a list of distances by a place. Must be called after populateEstimatedDistancesArray. - * - * @param distancesArray Arraylist of distances - * @param place Place object by which the distances filtered - * @return An arraylist of distance - */ - public static List filterDistancesArrayByPlace(@NonNull List distancesArray, Location place, boolean isOrigin) { - ArrayList filteredDistancesArray = new ArrayList<>(); - - for (MatrixElement matrixElement : distancesArray) { - if (isOrigin && matrixElement.getOrigin().equals(place)) { - filteredDistancesArray.add(matrixElement); - } - if (!isOrigin && matrixElement.getDestination().equals(place)) { - filteredDistancesArray.add(matrixElement); - } - } - - return filteredDistancesArray; - } - - /*** - * Filter the distances by an arraylist of "used places". Any distances object that contains one of "used places" array will not be returned. Therefore, reduces redundancy. - * - * @param distancesArray An arraylist of distances - * @param places An arraylist of places - * @return Arraylist of distances filtered by places - */ - public static List filterDistancesArrayByPlaces(List distancesArray, List places, boolean excludeLastPlace, boolean filterIn) { - ArrayList filteredDistancesArrayByPlaces = new ArrayList<>(); - ArrayList usedPlacesModified = new ArrayList<>(places); - - if (excludeLastPlace) { - usedPlacesModified.remove(usedPlacesModified.size() - 1); - } - - for (MatrixElement matrixElement : distancesArray) { - boolean acceptedDistance = !filterIn ? !usedPlacesModified.contains(matrixElement.getOrigin()) && !usedPlacesModified.contains(matrixElement.getDestination()) : usedPlacesModified.contains(matrixElement.getOrigin()) || usedPlacesModified.contains(matrixElement.getDestination()); - - if (acceptedDistance) { - filteredDistancesArrayByPlaces.add(matrixElement); - } - } - - return filteredDistancesArrayByPlaces; - } - - public static List filterDistancesArrayByPlacesStrict(List distancesArray, List places) { - ArrayList filteredDistancesArrayByPlaces = new ArrayList<>(); - - for (MatrixElement matrixElement : distancesArray) { - boolean acceptedDistance = places.contains(matrixElement.getOrigin()) && places.contains(matrixElement.getDestination()); - - if (acceptedDistance) { - filteredDistancesArrayByPlaces.add(matrixElement); - } - } - - return filteredDistancesArrayByPlaces; - - } - - public static List filterDistancesArrayBySufficientCapacity(@NonNull List distancesArray, double vehicleCapacity) { - ArrayList sufficientCapacityMatrixElements = new ArrayList<>(); - - for (MatrixElement matrixElement : distancesArray) { - - if ((vehicleCapacity - matrixElement.getDestination().getDemands()) >= 0) { - sufficientCapacityMatrixElements.add(matrixElement); - } - - } - - return sufficientCapacityMatrixElements; - } - - /*** - * Find the best (minimum) matrixElements among a list of matrixElements. Best in combination with filterDistance. - * - * @param matrixElements An arraylist of matrixElements - * @return An arraylist of matrixElements object that contain the minimum distance value - */ - public static List findBestDistances(@NonNull List matrixElements) { - ArrayList bestMatrixElements = new ArrayList<>(); - ArrayList distanceValue = new ArrayList<>(); - - // Assign distance value into distanceValue - for (MatrixElement matrixElement : matrixElements) { - distanceValue.add(matrixElement.getDistance()); - } - - // Get the best distance value - int bestId = distanceValue.indexOf(Collections.min(distanceValue)); - double bestDistanceValue = distanceValue.get(bestId); - - // Check each value of matrixElements for the min and store it into bestMatrixElements array - for (MatrixElement matrixElement : matrixElements) { - - // Find matrixElements with the most minimum value - if (matrixElement.getDistance() == bestDistanceValue) { - bestMatrixElements.add(matrixElement); - } - } - - return bestMatrixElements; - } - - // Optimization - - /** - * Capacitated TSP with Nearest Neighbor method. Returns a sequence of distances. MatrixElement traveled is not included, must be calculated manually. - * Written by Damar Syah Maulana - * - * @param distancesArray Arraylist of distances object - * @param places Arraylist of Destinations - * @param depotPlaceIndex Index of Place defined as a Depot or initial point - * @return Arraylist of Distances in Nearest Neighbor - */ - public static List computeCapacitatedNearestNeighborResult(List distancesArray, @NonNull List places, @NonNull List vehicles, int depotPlaceIndex, boolean isRoundTrip) { - - Log.d(TAG, "computeCapacitatedNearestNeighborResult: Start of NN method"); - Log.d(TAG, "computeCapacitatedNearestNeighborResult: Distances: " + distancesArray.size() + " | Places: " + places.size() + " | Vehicles: " + Vehicle.Toolbox.getDefaultVehicle(vehicles)); - - ArrayList solutions = new ArrayList<>(); // Store the Nearest Neighbor produced distance or the final result - - // Get the default vehicle - Vehicle vehicle = Vehicle.Toolbox.getDefaultVehicle(vehicles); - vehicle = vehicle != null ? vehicle : vehicles.get(0); - - // Used places needs to be stored inside arraylist to check whether the distance that is about to calculate is already used or not - ArrayList usedPlaces = new ArrayList<>(); - Location depotPlace = places.get(depotPlaceIndex); // Set the depot place by place index TODO: Change to placeID - usedPlaces.add(depotPlace); - - // Sort vehicles, move default vehicle to the first - ArrayList sortedVehicles = new ArrayList<>(vehicles); - sortedVehicles.remove(vehicle); - sortedVehicles.add(0, vehicle); - - // Clone vehicles for each dispatch limit - ArrayList fleet = new ArrayList<>(); - for (Vehicle v : sortedVehicles) - if (v.getDispatchLimit() > 0) - for (int i = 0; i < v.getDispatchLimit(); i ++) { - fleet.add(v); - } - - boolean startNewRoute = false; - - for (Vehicle v : fleet) { - - // Skip current vehicle if solutions - if (usedPlaces.containsAll(places)) - continue; - - //// - - // Vehicle remaining capacity should be visible in the entire scope for use multiple times - double vehicleRemainingCapacity = v.getCapacity(); - - // Compute solution for single trip - ArrayList trip = new ArrayList<>(); // Store the Nearest Neighbor produced distance or the final result - - for (int j = 0; vehicleRemainingCapacity >= 0; j++) { - - if (startNewRoute) - usedPlaces.add(depotPlace); - - // Filter the distancesArray by the last place used so that it will show the distance only FROM that place to ANY place because isOrigin is true. - List filteredMatrixElements = filterDistancesArrayByPlace(distancesArray, usedPlaces.get(usedPlaces.size() - 1), true); - if (startNewRoute) { - usedPlaces.remove(depotPlace); - startNewRoute = false; - } - - List processedMatrixElements = filterDistancesArrayByPlaces(filteredMatrixElements, usedPlaces, true, false); - - List sufficientCapacityMatrixElements = filterDistancesArrayBySufficientCapacity(processedMatrixElements, vehicleRemainingCapacity); - - if (sufficientCapacityMatrixElements.size() == 0) { - - if (!isRoundTrip) - break; - - // Find a distance that go straight to depot and add to solutions (ROUNDTRIP) - for (MatrixElement matrixElement : filteredMatrixElements) { - - if (matrixElement.getDestination().equals(depotPlace) && trip.size() > 0 && matrixElement.getOrigin().equals(trip.get(trip.size() - 1).getDestination())) { - Solution solution = Solution.fromMatrixElement(matrixElement); - solution.setCarry(vehicleRemainingCapacity); - solution.setVehicleId(v.getId()); // Assign vehicle id to solutionDistance - trip.add(solution); - startNewRoute = true; -// Log.d(TAG, "computeCapacitatedNearestNeighborResult: Last used places: " + usedPlaces.get(usedPlaces.size()-1).getName()); -// Log.d(TAG, "computeCapacitatedNearestNeighborResult: (ROUNDTRIP) Added matrixElement " + solution.getOrigin().getName() + " to " + solution.getDestination().getName() + ": " + solution.getDistance()); - - } - } - - break; - - } - - List bestMatrixElements = findBestDistances(sufficientCapacityMatrixElements); - Solution bestDistance = Solution.fromMatrixElement(bestMatrixElements.get(0)); - - double demands = bestDistance.getDestination().getDemands(); - vehicleRemainingCapacity -= demands; - - bestDistance.setDemand(demands); - bestDistance.setCarry(vehicleRemainingCapacity); - bestDistance.setVehicleId(v.getId()); // Assign vehicle id to solutionDistance - - usedPlaces.add(bestDistance.getDestination()); - trip.add(bestDistance); -// Log.e(TAG, "(NN): Added distance " + bestDistance.getOrigin().getName() + " to " + bestDistance.getDestination().getName() + ": " + bestDistance.getDistance()); - - } - - solutions.addAll(trip); - - - } - - return solutions; - } - - public static List computeCapacitatedSavingMatrixResult(List distancesArray, @NonNull List places, @NonNull List vehicles, int depotPlaceIndex, boolean isRoundTrip) { - - // Result distances - ArrayList solutions = new ArrayList<>(); - - // Filter distances to only those that has a savingDistance - Location depot = Location.Toolbox.getByProfile(places, Location.Profile.SOURCE).get(0); - ArrayList savingMatrixElements = new ArrayList<>(); - - for (MatrixElement matrixElement : distancesArray) { - - if (matrixElement.getSavingDistance() == 0 && (matrixElement.getOrigin().equals(depot) || matrixElement.getDestination().equals(depot))) - continue; - - // Validate whether the matrixElement is already exists - boolean isExists = false; - - for (MatrixElement d : savingMatrixElements) { - - // Looking for the reversed matrixElement - if (matrixElement.getOrigin().equals(d.getDestination()) && matrixElement.getDestination().equals(d.getOrigin())) { - isExists = true; - break; // Break loop if matrixElement is exists - } - - } - - if (!isExists) - savingMatrixElements.add(matrixElement); - - } - - // Log saving distances -// for (MatrixElement d : savingMatrixElements) { -// Log.e(TAG + "(SM)", "MatrixElement with saving: MatrixElement " + d.getOrigin().getName() + " to " + d.getDestination().getName()); -// } - - // Begin computation - ArrayList maxMatrixElements = new ArrayList<>(); - int totalDest = Location.Toolbox.getByProfile(places, Location.Profile.DESTINATION).size(); - int iteration = ((totalDest * totalDest) - totalDest) / 2; // Number of iteration self defined formula - - Log.e(TAG + "(SM)", "Iteration: " + iteration + " | Saving distances: " + savingMatrixElements.size()); - - // Sort saving distances descending - for (int i = 0; i < iteration; i++) { - - MatrixElement maxMatrixElement = savingMatrixElements.get(0); - - // Find the largest saving value - for (int j = 0; j < savingMatrixElements.size(); j++) { - - MatrixElement matrixElement = savingMatrixElements.get(j); - - // If matrixElement is larger than current maxMatrixElement - if (matrixElement.getSavingDistance() > maxMatrixElement.getSavingDistance()) { - maxMatrixElement = matrixElement; - } - - } - - savingMatrixElements.remove(maxMatrixElement); - - maxMatrixElements.add(maxMatrixElement); // Add the largest distance into array - - // Log largest distance -// Log.e(TAG + "(SM)", "Descending sorted distance: " + maxMatrixElement.getSavingDistance() + " | Origin: " + maxMatrixElement.getOrigin().getName() + " | Destination: " + maxMatrixElement.getDestination().getName()); - - } - - // Main iteration - ArrayList> placeGroups = new ArrayList<>(); - ArrayList usedPlaces = new ArrayList<>(); - - Vehicle vehicle = Vehicle.Toolbox.getDefaultVehicle(vehicles); - if (vehicle == null) - vehicle = vehicles.get(0); - - // Sort vehicles, move default vehicle to the first - ArrayList sortedVehicles = new ArrayList<>(vehicles); - sortedVehicles.remove(vehicle); - sortedVehicles.add(0, vehicle); - - // Clone vehicles for each dispatch limit - Log.e(TAG + "(SM)", "Start populating vehicles..."); - ArrayList dispatchableVehicles = new ArrayList<>(); - for (Vehicle v : sortedVehicles) - if (v.getDispatchLimit() > 0) - for (int i = 0; i < v.getDispatchLimit(); i ++) { - dispatchableVehicles.add(v); - } - - // Log dispatchableVehicles content - for (Vehicle v : dispatchableVehicles) - Log.e(TAG, "Vehicle added:" + v.getId()); - - boolean forceStopComputation = false; - - for (MatrixElement matrixElement : maxMatrixElements) { - - double vehicleCapacity; - - Location origin = matrixElement.getOrigin(); - Location destination = matrixElement.getDestination(); - - boolean isOriginExists = usedPlaces.contains(origin); - boolean isDestinationExists = usedPlaces.contains(destination); - -// Log.e(TAG + "(SM)", "Inspecting MatrixElement " + matrixElement.getSavingDistance() + " | Origin: " + matrixElement.getOrigin().getName() + " | Destination: " + matrixElement.getDestination().getName()); - - if (isOriginExists && isDestinationExists) { - Log.e(TAG + "(SM)", "MatrixElement " + matrixElement.getSavingDistance() + ": Skipped due to both of its contents has been used"); - continue; - } - - // Initialize container to list places that has grouped and existed in a placeGroup in the next loop - ArrayList groupedPlaces = new ArrayList<>(); - - /* - Loops through places group if any. - Useful if the iterated matrixElement's places whether origin or destination - is a part of an existing placeGroup. - If no placeGroup existed, skip immediately by the nature of foreach loop - itself. - */ - for (ArrayList placeGroup : placeGroups) { - - int i = placeGroups.indexOf(placeGroup); - - if (i > dispatchableVehicles.size() - 1) { - forceStopComputation = true; - continue; - } - - vehicleCapacity = dispatchableVehicles.get(i).getCapacity(); // Get vehicle capacity from the matching index - - Log.e(TAG + "(SM)", "Inspecting placeGroup " + i + " from MatrixElement " + matrixElement.getSavingDistance() + " | Using vehicle: " + dispatchableVehicles.get(i).getId()); - - groupedPlaces.addAll(placeGroup); // List places inside placeGroup into groupedPlaces for use outside of the loop - - // Check to see if whether the origin or destination existed in the current placeGroup - boolean isOriginExistsInPlaceGroup = placeGroup.contains(origin); - boolean isDestinationExistsInPlaceGroup = placeGroup.contains(destination); - -// if (isOriginExistsInPlaceGroup) -// Log.e(TAG + "(SM)", "Origin exists in placeGroup " + i); -// -// if (isDestinationExistsInPlaceGroup) -// Log.e(TAG + "(SM)", "Destination exists in placeGroup " + i); - - // Immediately skip if both origin and destination exist in the current placeGroup - if (isOriginExistsInPlaceGroup && isDestinationExistsInPlaceGroup) { - Log.e(TAG + "(SM)", "Both origin and destination exist in the placeGroup " + i); - break; - } - - // Sum all demands of the place group - double totalDemand = 0; - for (Location p : placeGroup) - totalDemand += p.getDemands(); - - // Check whether the demand of origin or destination and current placeGroup fits the vehicleCapacity - boolean isOriginCapacityFitsInPlaceGroup = totalDemand + origin.getDemands() <= vehicleCapacity; - boolean isDestinationCapacityFitsInPlaceGroup = totalDemand + destination.getDemands() <= vehicleCapacity; - -// if (isOriginCapacityFitsInPlaceGroup) -// Log.e(TAG + "(SM)", "Origin capacity fits in placeGroup " + i); -// -// if (isDestinationCapacityFitsInPlaceGroup) -// Log.e(TAG + "(SM)", "Destination capacity fits in placeGroup " + i); - - Log.e(TAG + "(SM)", "Total demands of placeGroup " + i + ": " + totalDemand); - - if (isOriginExistsInPlaceGroup && isDestinationCapacityFitsInPlaceGroup) { - placeGroup.add(destination); -// Log.e(TAG + "(SM)", "Added " + destination.getName() + " (destination) into placeGroup(" + placeGroups.indexOf(placeGroup) + ") from MatrixElement " + matrixElement.getSavingDistance()); - usedPlaces.add(destination); - } else if (isDestinationExistsInPlaceGroup && isOriginCapacityFitsInPlaceGroup) { - placeGroup.add(origin); -// Log.e(TAG + "(SM)", "Added " + origin.getName() + " (origin) into placeGroup(" + placeGroups.indexOf(placeGroup) + ") from MatrixElement " + matrixElement.getSavingDistance()); - usedPlaces.add(origin); - } - - } - - // If the placeGroups size is equal or more than dispatchableVehicles size, prevent creation of a new placeGroup - if (placeGroups.size() - 1 >= dispatchableVehicles.size() - 1) { - forceStopComputation = true; // By enabling the force stop computation, prevents unused places from being added to the solution - continue; // Must use continue to finish the inspection to all distances - } - - // Checks if both origin & destination capacity fits - vehicleCapacity = dispatchableVehicles.get(placeGroups.size() == 0 ? 0 : placeGroups.size() - 1).getCapacity(); // Get vehicle capacity from the matching index - boolean isDistanceCapacityFits = origin.getDemands() + destination.getDemands() <= vehicleCapacity; - - // Checks origin and destination presence in the existing groupedPlaces - boolean isOriginExistsInGroupedPlaces = groupedPlaces.contains(origin); - boolean isDestinationExistsInGroupedPlaces = groupedPlaces.contains(destination); - - // Insert both origin and destination for the first loop or if the origin & destination don't exist in the groupedPlaces, AND if the capacity fits - ArrayList ap = new ArrayList<>(); - if ( - (placeGroups.size() == 0 || (!isOriginExistsInGroupedPlaces && !isDestinationExistsInGroupedPlaces)) - && isDistanceCapacityFits - ) { - - ap.add(origin); - ap.add(destination); - - placeGroups.add(ap); - usedPlaces.addAll(ap); - -// Log.e(TAG + "(SM)", "Added " + origin.getName() + " and " + destination.getName() + " into a NEW placeGroup(" + placeGroups.indexOf(ap) + ") from MatrixElement " + matrixElement.getSavingDistance()); - } - - } - - Log.e(TAG + "(SM)", "Begin populating used places"); - -// for (DSMPlace p : usedPlaces) -// Log.e(TAG + "(SM)", "Used: " + p.getName()); - - // Populate unused places into one array if computation doesn't force stopped - if (!forceStopComputation) { - ArrayList unusedPlaces = new ArrayList<>(); - for (Location p : places) { - if (!usedPlaces.contains(p) && !p.equals(depot)) { - unusedPlaces.add(p); -// Log.e(TAG + "(SM)", "Unused: " + p.getName()); - } - } - - // Add unusedPlaces into placesGroup - if (unusedPlaces.size() > 0) { - /* - Possibilities on unusedPlaces: - - Filled with leftover places that have don't have pair due to large amount of demands - */ - for (Location p : unusedPlaces) - placeGroups.add(new ArrayList<>(Collections.singletonList(p))); - - // Warn if the number of unusedPlaces is more than 1 because there might be an error in the algorithm - if (unusedPlaces.size() > 2) - Log.e(TAG + "(SM)", "Unused place should be a maximum of one. The solution produce more than one unused places. There might be an error during optimization!"); - } - } - - Log.e(TAG + "(SM)", "Start of placeGroups log session"); - - for (int i = 0; i < placeGroups.size(); i++) { - ArrayList ap = placeGroups.get(i); - - ap.add(0, depot); // Adds depot every placeGroup's end - - Log.e(TAG + "(SM)", "Start of placeGroup " + i + " contents"); - -// for (DSMPlace p : ap) { -// Log.e(TAG + "(SM)", "placeGroup " + i + " content: " + p.getName()); -// } - - /* - Filter all distances from distancesArray which its origin and destination are members of - the current placeGroup. Required before continuing to optimization. - */ - List filteredMatrixElements = filterDistancesArrayByPlacesStrict(distancesArray, ap); - - ArrayList assignedVehicle = new ArrayList<>(Collections.singletonList(dispatchableVehicles.get(i))); - List optimizedDistances = computeCapacitatedNearestNeighborResult(filteredMatrixElements, ap, assignedVehicle, depotPlaceIndex, isRoundTrip); - solutions.addAll(optimizedDistances); - - Log.e(TAG + "(SM)", "Start of placeGroup " + i + " optimized distances"); -// for (Solution s : optimizedDistances) { -// Log.e(TAG + "(SM)", "placeGroup " + i + ": Place:" + s.getOrigin().getName() + " | Place: " + s.getDestination().getName()); -// } - - Log.e(TAG + "(SM)", "Start of placeGroup " + i + " demands"); - - double totalDemands = 0; -// for (DSMPlace p : ap) { -// totalDemands += p.getDemands(); -// Log.e(TAG + "(SM)", "placeGroup " + i + ": " + p.getName() + ", demands: " + totalDemands); -// } - - Log.e(TAG + "(SM)", "Total demands: " + totalDemands); - Log.e(TAG + "(SM)", "End of placeGroup " + i); - - } - - return solutions; - - } - - // Toolbox - - @NonNull - public static List symmetrizeMatrix(@NonNull List matrix) { - - ArrayList resultMatrix = new ArrayList<>(matrix); - - for (MatrixElement d : resultMatrix) { - - Location origin = d.getOrigin(); - Location destination = d.getDestination(); - double distance = d.getDistance(); - - // Iterate through matrix again and find the opposite distance and set the value to match the previous distance - for (MatrixElement d2 : resultMatrix) - if (d2.getOrigin().equals(destination) && d2.getDestination().equals(origin)) // If it's opposite distance - d2.setDistance(distance); - - } - - return resultMatrix; - - } - - @NonNull - private static ArrayList adaptPlacesToDistances(ArrayList distancesArray, ArrayList places) { - - ArrayList matrixElements = new ArrayList<>(); // To keep the result in memory - - for (int i = 0; i < places.size() - 1; i++) { - Location p = places.get(i); - Location p2 = places.get(i + 1); - - for (MatrixElement d : distancesArray) { - - if (d.getOrigin().equals(p) && d.getDestination().equals(p2)) - matrixElements.add(d); - - } - - } - - return matrixElements; - } - - } diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/AndroidLogger.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/AndroidLogger.java new file mode 100644 index 0000000..1f41183 --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/AndroidLogger.java @@ -0,0 +1,15 @@ +package id.my.dsm.vrpsolver.logging; + +import android.util.Log; + +public class AndroidLogger implements Logger { + @Override + public void d(String tag, String msg) { + Log.d(tag, msg); + } + + @Override + public void e(String tag, String msg) { + Log.e(tag, msg); + } +} diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/Logger.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/Logger.java new file mode 100644 index 0000000..a8ba11a --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/logging/Logger.java @@ -0,0 +1,6 @@ +package id.my.dsm.vrpsolver.logging; + +public interface Logger { + void d(String tag, String msg); + void e(String tag, String msg); +} diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/NearestNeighborOptimizer.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/NearestNeighborOptimizer.java new file mode 100644 index 0000000..b399b2f --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/NearestNeighborOptimizer.java @@ -0,0 +1,138 @@ +package id.my.dsm.vrpsolver.optimization; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; + +public class NearestNeighborOptimizer implements Optimizer { + + private static final String TAG = NearestNeighborOptimizer.class.getSimpleName(); + + private final Logger logger; + + public NearestNeighborOptimizer(Logger logger) { + this.logger = logger; + } + + @Override + public List optimize(List matrix, List places, List vehicles, int depotPlaceIndex, boolean isRoundTrip) { + return computeCapacitatedNearestNeighborResult(matrix, places, vehicles, depotPlaceIndex, isRoundTrip); + } + + public List computeCapacitatedNearestNeighborResult(List distancesArray, @NonNull List places, @NonNull List vehicles, int depotPlaceIndex, boolean isRoundTrip) { + + logger.d(TAG, "computeCapacitatedNearestNeighborResult: Start of NN method"); + logger.d(TAG, "computeCapacitatedNearestNeighborResult: Distances: " + distancesArray.size() + " | Places: " + places.size() + " | Vehicles: " + Vehicle.Toolbox.getDefaultVehicle(vehicles)); + + ArrayList solutions = new ArrayList<>(); // Store the Nearest Neighbor produced distance or the final result + + // Get the default vehicle + Vehicle vehicle = Vehicle.Toolbox.getDefaultVehicle(vehicles); + vehicle = vehicle != null ? vehicle : vehicles.get(0); + + // Used places needs to be stored inside arraylist to check whether the distance that is about to calculate is already used or not + ArrayList usedPlaces = new ArrayList<>(); + Location depotPlace = places.get(depotPlaceIndex); // Set the depot place by place index TODO: Change to placeID + usedPlaces.add(depotPlace); + + // Sort vehicles, move default vehicle to the first + ArrayList sortedVehicles = new ArrayList<>(vehicles); + sortedVehicles.remove(vehicle); + sortedVehicles.add(0, vehicle); + + // Clone vehicles for each dispatch limit + ArrayList fleet = new ArrayList<>(); + for (Vehicle v : sortedVehicles) + if (v.getDispatchLimit() > 0) + for (int i = 0; i < v.getDispatchLimit(); i ++) { + fleet.add(v); + } + + boolean startNewRoute = false; + + for (Vehicle v : fleet) { + + // Skip current vehicle if solutions + if (usedPlaces.containsAll(places)) + continue; + + //// + + // Vehicle remaining capacity should be visible in the entire scope for use multiple times + double vehicleRemainingCapacity = v.getCapacity(); + + // Compute solution for single trip + ArrayList trip = new ArrayList<>(); // Store the Nearest Neighbor produced distance or the final result + + for (int j = 0; vehicleRemainingCapacity >= 0; j++) { + + if (startNewRoute) + usedPlaces.add(depotPlace); + + // Filter the distancesArray by the last place used so that it will show the distance only FROM that place to ANY place because isOrigin is true. + List filteredMatrixElements = id.my.dsm.vrpsolver.utils.MatrixUtils.filterDistancesArrayByPlace(distancesArray, usedPlaces.get(usedPlaces.size() - 1), true); + if (startNewRoute) { + usedPlaces.remove(depotPlace); + startNewRoute = false; + } + + List processedMatrixElements = id.my.dsm.vrpsolver.utils.MatrixUtils.filterDistancesArrayByPlaces(filteredMatrixElements, usedPlaces, true, false); + + List sufficientCapacityMatrixElements = id.my.dsm.vrpsolver.utils.MatrixUtils.filterDistancesArrayBySufficientCapacity(processedMatrixElements, vehicleRemainingCapacity); + + if (sufficientCapacityMatrixElements.size() == 0) { + + if (!isRoundTrip) + break; + + // Find a distance that go straight to depot and add to solutions (ROUNDTRIP) + for (MatrixElement matrixElement : filteredMatrixElements) { + + if (matrixElement.getDestination().equals(depotPlace) && trip.size() > 0 && matrixElement.getOrigin().equals(trip.get(trip.size() - 1).getDestination())) { + Solution solution = Solution.fromMatrixElement(matrixElement); + solution.setCarry(vehicleRemainingCapacity); + solution.setVehicleId(v.getId()); // Assign vehicle id to solutionDistance + trip.add(solution); + startNewRoute = true; +// Log.d(TAG, "computeCapacitatedNearestNeighborResult: Last used places: " + usedPlaces.get(usedPlaces.size()-1).getName()); +// Log.d(TAG, "computeCapacitatedNearestNeighborResult: (ROUNDTRIP) Added matrixElement " + solution.getOrigin().getName() + " to " + solution.getDestination().getName() + ": " + solution.getDistance()); + + } + } + + break; + + } + + List bestMatrixElements = id.my.dsm.vrpsolver.utils.MatrixUtils.findBestDistances(sufficientCapacityMatrixElements); + Solution bestDistance = Solution.fromMatrixElement(bestMatrixElements.get(0)); + + double demands = bestDistance.getDestination().getDemands(); + vehicleRemainingCapacity -= demands; + + bestDistance.setDemand(demands); + bestDistance.setCarry(vehicleRemainingCapacity); + bestDistance.setVehicleId(v.getId()); // Assign vehicle id to solutionDistance + + usedPlaces.add(bestDistance.getDestination()); + trip.add(bestDistance); +// Log.e(TAG, "(NN): Added distance " + bestDistance.getOrigin().getName() + " to " + bestDistance.getDestination().getName() + ": " + bestDistance.getDistance()); + + } + + solutions.addAll(trip); + + + } + + return solutions; + } +} diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/Optimizer.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/Optimizer.java new file mode 100644 index 0000000..45a1d66 --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/Optimizer.java @@ -0,0 +1,12 @@ +package id.my.dsm.vrpsolver.optimization; + +import java.util.List; + +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; + +public interface Optimizer { + List optimize(List matrix, List places, List vehicles, int depotPlaceIndex, boolean isRoundTrip); +} diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/SavingMatrixOptimizer.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/SavingMatrixOptimizer.java new file mode 100644 index 0000000..62e2a23 --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/optimization/SavingMatrixOptimizer.java @@ -0,0 +1,325 @@ +package id.my.dsm.vrpsolver.optimization; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; + +public class SavingMatrixOptimizer implements Optimizer { + + private static final String TAG = SavingMatrixOptimizer.class.getSimpleName(); + + private final Logger logger; + private final Optimizer nearestNeighborOptimizer; + + public SavingMatrixOptimizer(Logger logger) { + this.logger = logger; + this.nearestNeighborOptimizer = new NearestNeighborOptimizer(logger); + } + + @Override + public List optimize(List matrix, List places, List vehicles, int depotPlaceIndex, boolean isRoundTrip) { + return computeCapacitatedSavingMatrixResult(matrix, places, vehicles, depotPlaceIndex, isRoundTrip); + } + + public List computeCapacitatedSavingMatrixResult(List distancesArray, @NonNull List places, @NonNull List vehicles, int depotPlaceIndex, boolean isRoundTrip) { + + // Result distances + ArrayList solutions = new ArrayList<>(); + + // Filter distances to only those that has a savingDistance + Location depot = Location.Toolbox.getByProfile(places, Location.Profile.SOURCE).get(0); + ArrayList savingMatrixElements = new ArrayList<>(); + + for (MatrixElement matrixElement : distancesArray) { + + if (matrixElement.getSavingDistance() == 0 && (matrixElement.getOrigin().equals(depot) || matrixElement.getDestination().equals(depot))) + continue; + + // Validate whether the matrixElement is already exists + boolean isExists = false; + + for (MatrixElement d : savingMatrixElements) { + + // Looking for the reversed matrixElement + if (matrixElement.getOrigin().equals(d.getDestination()) && matrixElement.getDestination().equals(d.getOrigin())) { + isExists = true; + break; // Break loop if matrixElement is exists + } + + } + + if (!isExists) + savingMatrixElements.add(matrixElement); + + } + + // Log saving distances +// for (MatrixElement d : savingMatrixElements) { +// Log.e(TAG + "(SM)", "MatrixElement with saving: MatrixElement " + d.getOrigin().getName() + " to " + d.getDestination().getName()); +// } + + // Begin computation + ArrayList maxMatrixElements = new ArrayList<>(); + int totalDest = Location.Toolbox.getByProfile(places, Location.Profile.DESTINATION).size(); + int iteration = ((totalDest * totalDest) - totalDest) / 2; // Number of iteration self defined formula + + logger.e(TAG + "(SM)", "Iteration: " + iteration + " | Saving distances: " + savingMatrixElements.size()); + + // Sort saving distances descending + for (int i = 0; i < iteration; i++) { + + MatrixElement maxMatrixElement = savingMatrixElements.get(0); + + // Find the largest saving value + for (int j = 0; j < savingMatrixElements.size(); j++) { + + MatrixElement matrixElement = savingMatrixElements.get(j); + + // If matrixElement is larger than current maxMatrixElement + if (matrixElement.getSavingDistance() > maxMatrixElement.getSavingDistance()) { + maxMatrixElement = matrixElement; + } + + } + + savingMatrixElements.remove(maxMatrixElement); + + maxMatrixElements.add(maxMatrixElement); // Add the largest distance into array + + // Log largest distance +// Log.e(TAG + "(SM)", "Descending sorted distance: " + maxMatrixElement.getSavingDistance() + " | Origin: " + maxMatrixElement.getOrigin().getName() + " | Destination: " + maxMatrixElement.getDestination().getName()); + + } + + // Main iteration + ArrayList> placeGroups = new ArrayList<>(); + ArrayList usedPlaces = new ArrayList<>(); + + Vehicle vehicle = Vehicle.Toolbox.getDefaultVehicle(vehicles); + if (vehicle == null) + vehicle = vehicles.get(0); + + // Sort vehicles, move default vehicle to the first + ArrayList sortedVehicles = new ArrayList<>(vehicles); + sortedVehicles.remove(vehicle); + sortedVehicles.add(0, vehicle); + + // Clone vehicles for each dispatch limit + logger.e(TAG + "(SM)", "Start populating vehicles..."); + ArrayList dispatchableVehicles = new ArrayList<>(); + for (Vehicle v : sortedVehicles) + if (v.getDispatchLimit() > 0) + for (int i = 0; i < v.getDispatchLimit(); i ++) { + dispatchableVehicles.add(v); + } + + // Log dispatchableVehicles content + for (Vehicle v : dispatchableVehicles) + logger.e(TAG, "Vehicle added:" + v.getId()); + + boolean forceStopComputation = false; + + for (MatrixElement matrixElement : maxMatrixElements) { + + double vehicleCapacity; + + Location origin = matrixElement.getOrigin(); + Location destination = matrixElement.getDestination(); + + boolean isOriginExists = usedPlaces.contains(origin); + boolean isDestinationExists = usedPlaces.contains(destination); + +// logger.e(TAG + "(SM)", "Inspecting MatrixElement " + matrixElement.getSavingDistance() + " | Origin: " + matrixElement.getOrigin().getName() + " | Destination: " + matrixElement.getDestination().getName()); + + if (isOriginExists && isDestinationExists) { + logger.e(TAG + "(SM)", "MatrixElement " + matrixElement.getSavingDistance() + ": Skipped due to both of its contents has been used"); + continue; + } + + // Initialize container to list places that has grouped and existed in a placeGroup in the next loop + ArrayList groupedPlaces = new ArrayList<>(); + + /* + Loops through places group if any. + Useful if the iterated matrixElement's places whether origin or destination + is a part of an existing placeGroup. + If no placeGroup existed, skip immediately by the nature of foreach loop + itself. + */ + for (ArrayList placeGroup : placeGroups) { + + int i = placeGroups.indexOf(placeGroup); + + if (i > dispatchableVehicles.size() - 1) { + forceStopComputation = true; + continue; + } + + vehicleCapacity = dispatchableVehicles.get(i).getCapacity(); // Get vehicle capacity from the matching index + + logger.e(TAG + "(SM)", "Inspecting placeGroup " + i + " from MatrixElement " + matrixElement.getSavingDistance() + " | Using vehicle: " + dispatchableVehicles.get(i).getId()); + + groupedPlaces.addAll(placeGroup); // List places inside placeGroup into groupedPlaces for use outside of the loop + + // Check to see if whether the origin or destination existed in the current placeGroup + boolean isOriginExistsInPlaceGroup = placeGroup.contains(origin); + boolean isDestinationExistsInPlaceGroup = placeGroup.contains(destination); + +// if (isOriginExistsInPlaceGroup) +// Log.e(TAG + "(SM)", "Origin exists in placeGroup " + i); +// +// if (isDestinationExistsInPlaceGroup) +// logger.e(TAG + "(SM)", "Destination exists in placeGroup " + i); + + // Immediately skip if both origin and destination exist in the current placeGroup + if (isOriginExistsInPlaceGroup && isDestinationExistsInPlaceGroup) { + logger.e(TAG + "(SM)", "Both origin and destination exist in the placeGroup " + i); + break; + } + + // Sum all demands of the place group + double totalDemand = 0; + for (Location p : placeGroup) + totalDemand += p.getDemands(); + + // Check whether the demand of origin or destination and current placeGroup fits the vehicleCapacity + boolean isOriginCapacityFitsInPlaceGroup = totalDemand + origin.getDemands() <= vehicleCapacity; + boolean isDestinationCapacityFitsInPlaceGroup = totalDemand + destination.getDemands() <= vehicleCapacity; + +// if (isOriginCapacityFitsInPlaceGroup) +// Log.e(TAG + "(SM)", "Origin capacity fits in placeGroup " + i); +// +// if (isDestinationCapacityFitsInPlaceGroup) +// logger.e(TAG + "(SM)", "Destination capacity fits in placeGroup " + i); + + logger.e(TAG + "(SM)", "Total demands of placeGroup " + i + ": " + totalDemand); + + if (isOriginExistsInPlaceGroup && isDestinationCapacityFitsInPlaceGroup) { + placeGroup.add(destination); +// Log.e(TAG + "(SM)", "Added " + destination.getName() + " (destination) into placeGroup(" + placeGroups.indexOf(placeGroup) + ") from MatrixElement " + matrixElement.getSavingDistance()); + usedPlaces.add(destination); + } else if (isDestinationExistsInPlaceGroup && isOriginCapacityFitsInPlaceGroup) { + placeGroup.add(origin); +// Log.e(TAG + "(SM)", "Added " + origin.getName() + " (origin) into placeGroup(" + placeGroups.indexOf(placeGroup) + ") from MatrixElement " + matrixElement.getSavingDistance()); + usedPlaces.add(origin); + } + + } + + // If the placeGroups size is equal or more than dispatchableVehicles size, prevent creation of a new placeGroup + if (placeGroups.size() - 1 >= dispatchableVehicles.size() - 1) { + forceStopComputation = true; // By enabling the force stop computation, prevents unused places from being added to the solution + continue; // Must use continue to finish the inspection to all distances + } + + // Checks if both origin & destination capacity fits + vehicleCapacity = dispatchableVehicles.get(placeGroups.size() == 0 ? 0 : placeGroups.size() - 1).getCapacity(); // Get vehicle capacity from the matching index + boolean isDistanceCapacityFits = origin.getDemands() + destination.getDemands() <= vehicleCapacity; + + // Checks origin and destination presence in the existing groupedPlaces + boolean isOriginExistsInGroupedPlaces = groupedPlaces.contains(origin); + boolean isDestinationExistsInGroupedPlaces = groupedPlaces.contains(destination); + + // Insert both origin and destination for the first loop or if the origin & destination don't exist in the groupedPlaces, AND if the capacity fits + ArrayList ap = new ArrayList<>(); + if ( + (placeGroups.size() == 0 || (!isOriginExistsInGroupedPlaces && !isDestinationExistsInGroupedPlaces)) + && isDistanceCapacityFits + ) { + + ap.add(origin); + ap.add(destination); + + placeGroups.add(ap); + usedPlaces.addAll(ap); + +// Log.e(TAG + "(SM)", "Added " + origin.getName() + " and " + destination.getName() + " into a NEW placeGroup(" + placeGroups.indexOf(ap) + ") from MatrixElement " + matrixElement.getSavingDistance()); + } + + } + + logger.e(TAG + "(SM)", "Begin populating used places"); + +// for (DSMPlace p : usedPlaces) +// logger.e(TAG + "(SM)", "Used: " + p.getName()); + + // Populate unused places into one array if computation doesn't force stopped + if (!forceStopComputation) { + ArrayList unusedPlaces = new ArrayList<>(); + for (Location p : places) { + if (!usedPlaces.contains(p) && !p.equals(depot)) { + unusedPlaces.add(p); +// Log.e(TAG + "(SM)", "Unused: " + p.getName()); + } + } + + // Add unusedPlaces into placesGroup + if (unusedPlaces.size() > 0) { + /* + Possibilities on unusedPlaces: + - Filled with leftover places that have don't have pair due to large amount of demands + */ + for (Location p : unusedPlaces) + placeGroups.add(new ArrayList<>(Collections.singletonList(p))); + + // Warn if the number of unusedPlaces is more than 1 because there might be an error in the algorithm + if (unusedPlaces.size() > 2) + logger.e(TAG + "(SM)", "Unused place should be a maximum of one. The solution produce more than one unused places. There might be an error during optimization!"); + } + } + + logger.e(TAG + "(SM)", "Start of placeGroups log session"); + + for (int i = 0; i < placeGroups.size(); i++) { + ArrayList ap = placeGroups.get(i); + + ap.add(0, depot); // Adds depot every placeGroup's end + + logger.e(TAG + "(SM)", "Start of placeGroup " + i + " contents"); + +// for (DSMPlace p : ap) { +// logger.e(TAG + "(SM)", "placeGroup " + i + " content: " + p.getName()); +// } + + /* + Filter all distances from distancesArray which its origin and destination are members of + the current placeGroup. Required before continuing to optimization. + */ + List filteredMatrixElements = id.my.dsm.vrpsolver.utils.MatrixUtils.filterDistancesArrayByPlacesStrict(distancesArray, ap); + + ArrayList assignedVehicle = new ArrayList<>(Collections.singletonList(dispatchableVehicles.get(i))); + + List optimizedDistances = nearestNeighborOptimizer.optimize(filteredMatrixElements, ap, assignedVehicle, 0, isRoundTrip); + solutions.addAll(optimizedDistances); + + logger.e(TAG + "(SM)", "Start of placeGroup " + i + " optimized distances"); +// for (Solution s : optimizedDistances) { +// logger.e(TAG + "(SM)", "placeGroup " + i + ": Place:" + s.getOrigin().getName() + " | Place: " + s.getDestination().getName()); +// } + + logger.e(TAG + "(SM)", "Start of placeGroup " + i + " demands"); + + double totalDemands = 0; +// for (DSMPlace p : ap) { +// totalDemands += p.getDemands(); +// logger.e(TAG + "(SM)", "placeGroup " + i + ": " + p.getName() + ", demands: " + totalDemands); +// } + + logger.e(TAG + "(SM)", "Total demands: " + totalDemands); + logger.e(TAG + "(SM)", "End of placeGroup " + i); + + } + + return solutions; + + } + +} diff --git a/vrpsolver/src/main/java/id/my/dsm/vrpsolver/utils/MatrixUtils.java b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/utils/MatrixUtils.java new file mode 100644 index 0000000..8f94b40 --- /dev/null +++ b/vrpsolver/src/main/java/id/my/dsm/vrpsolver/utils/MatrixUtils.java @@ -0,0 +1,262 @@ +package id.my.dsm.vrpsolver.utils; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; + +public class MatrixUtils { + + private static final String TAG = MatrixUtils.class.getSimpleName(); + private static Logger logger; + + public static void setLogger(Logger logger) { + MatrixUtils.logger = logger; + } + + public static ArrayList getDistanceFromPlaceSequence(ArrayList places, ArrayList distanceValues, ArrayList durationValues) { + + ArrayList matrixElements = new ArrayList<>(); // To keep the result in memory + + boolean isRoundTrip; + + if (places.size() == distanceValues.size() || places.size() == durationValues.size()) + isRoundTrip = true; + else if (places.size() == distanceValues.size() - 1 || places.size() == durationValues.size() - 1) + isRoundTrip = false; + else + return matrixElements; + + for (int i = 0; i < places.size() - 1; i++) { + + Location p = places.get(i); + Location p2 = places.get(i + 1); + + MatrixElement d = new MatrixElement(p, p2, distanceValues.get(i)); + + if (durationValues != null && durationValues.size() > 0) + d.setDuration(durationValues.get(i)); + + matrixElements.add(d); + + } + + if (isRoundTrip) { + // Assume the depot is the first place + MatrixElement d = new MatrixElement(places.get(places.size() - 1), places.get(0), distanceValues.get(distanceValues.size() - 1)); + + if (durationValues != null && durationValues.size() > 0) + d.setDuration(durationValues.get(durationValues.size() - 1)); + + matrixElements.add(d); + + } + + return matrixElements; + } + + // DEPENDENCIES FUNCTIONALITY + + // TODO: Finish thoroughly + public static void calculateDistanceSavingValue(@NonNull List places, List distancesArray) { + + // Pick the first source because Saving Matrix method only accepts one source + Location source = Location.Toolbox.getByProfile(places, Location.Profile.SOURCE).get(0); + int depotId = places.indexOf(source); + + // Saving MatrixElement (Saving matrix equivalent) TODO: Extract depotId as a parameter + for (MatrixElement matrixElement : distancesArray) { + + int oriId = places.indexOf(matrixElement.getOrigin()); + int destId = places.indexOf(matrixElement.getDestination()); + + if (oriId == depotId || destId == depotId) { + continue; + } + + double doi = 0; + double doj = 0; + double dij = 0; + + for (MatrixElement matrixElement2 : distancesArray) { + if (places.indexOf(matrixElement2.getOrigin()) == depotId && places.indexOf(matrixElement2.getDestination()) == oriId) { + doi = matrixElement2.getDistance(); + } else if (places.indexOf(matrixElement2.getOrigin()) == depotId && places.indexOf(matrixElement2.getDestination()) == destId) { + doj = matrixElement2.getDistance(); + } else if (places.indexOf(matrixElement2.getOrigin()) == oriId && places.indexOf(matrixElement2.getDestination()) == destId) { + dij = matrixElement2.getDistance(); + } + } + + double sij = doi + doj - dij; + + if (logger != null) { + logger.d(TAG, "MatrixElement saving: " + doi + " + " + doj + " - " + dij + " = " + sij); + } + +// if (sij > 0) + matrixElement.setSavingDistance(sij); + + } + + } + + /*** + * Filter a list of distances by a place. Must be called after populateEstimatedDistancesArray. + * + * @param distancesArray Arraylist of distances + * @param place Place object by which the distances filtered + * @return An arraylist of distance + */ + public static List filterDistancesArrayByPlace(@NonNull List distancesArray, Location place, boolean isOrigin) { + ArrayList filteredDistancesArray = new ArrayList<>(); + + for (MatrixElement matrixElement : distancesArray) { + if (isOrigin && matrixElement.getOrigin().equals(place)) { + filteredDistancesArray.add(matrixElement); + } + if (!isOrigin && matrixElement.getDestination().equals(place)) { + filteredDistancesArray.add(matrixElement); + } + } + + return filteredDistancesArray; + } + + /*** + * Filter the distances by an arraylist of "used places". Any distances object that contains one of "used places" array will not be returned. Therefore, reduces redundancy. + * + * @param distancesArray An arraylist of distances + * @param places An arraylist of places + * @return Arraylist of distances filtered by places + */ + public static List filterDistancesArrayByPlaces(List distancesArray, List places, boolean excludeLastPlace, boolean filterIn) { + ArrayList filteredDistancesArrayByPlaces = new ArrayList<>(); + ArrayList usedPlacesModified = new ArrayList<>(places); + + if (excludeLastPlace) { + usedPlacesModified.remove(usedPlacesModified.size() - 1); + } + + for (MatrixElement matrixElement : distancesArray) { + boolean acceptedDistance = !filterIn ? !usedPlacesModified.contains(matrixElement.getOrigin()) && !usedPlacesModified.contains(matrixElement.getDestination()) : usedPlacesModified.contains(matrixElement.getOrigin()) || usedPlacesModified.contains(matrixElement.getDestination()); + + if (acceptedDistance) { + filteredDistancesArrayByPlaces.add(matrixElement); + } + } + + return filteredDistancesArrayByPlaces; + } + + public static List filterDistancesArrayByPlacesStrict(List distancesArray, List places) { + ArrayList filteredDistancesArrayByPlaces = new ArrayList<>(); + + for (MatrixElement matrixElement : distancesArray) { + boolean acceptedDistance = places.contains(matrixElement.getOrigin()) && places.contains(matrixElement.getDestination()); + + if (acceptedDistance) { + filteredDistancesArrayByPlaces.add(matrixElement); + } + } + + return filteredDistancesArrayByPlaces; + + } + + public static List filterDistancesArrayBySufficientCapacity(@NonNull List distancesArray, double vehicleCapacity) { + ArrayList sufficientCapacityMatrixElements = new ArrayList<>(); + + for (MatrixElement matrixElement : distancesArray) { + + if ((vehicleCapacity - matrixElement.getDestination().getDemands()) >= 0) { + sufficientCapacityMatrixElements.add(matrixElement); + } + + } + + return sufficientCapacityMatrixElements; + } + + /*** + * Find the best (minimum) matrixElements among a list of matrixElements. Best in combination with filterDistance. + * + * @param matrixElements An arraylist of matrixElements + * @return An arraylist of matrixElements object that contain the minimum distance value + */ + public static List findBestDistances(@NonNull List matrixElements) { + ArrayList bestMatrixElements = new ArrayList<>(); + ArrayList distanceValue = new ArrayList<>(); + + // Assign distance value into distanceValue + for (MatrixElement matrixElement : matrixElements) { + distanceValue.add(matrixElement.getDistance()); + } + + // Get the best distance value + int bestId = distanceValue.indexOf(Collections.min(distanceValue)); + double bestDistanceValue = distanceValue.get(bestId); + + // Check each value of matrixElements for the min and store it into bestMatrixElements array + for (MatrixElement matrixElement : matrixElements) { + + // Find matrixElements with the most minimum value + if (matrixElement.getDistance() == bestDistanceValue) { + bestMatrixElements.add(matrixElement); + } + } + + return bestMatrixElements; + } + + @NonNull + public static List symmetrizeMatrix(@NonNull List matrix) { + + ArrayList resultMatrix = new ArrayList<>(matrix); + + for (MatrixElement d : resultMatrix) { + + Location origin = d.getOrigin(); + Location destination = d.getDestination(); + double distance = d.getDistance(); + + // Iterate through matrix again and find the opposite distance and set the value to match the previous distance + for (MatrixElement d2 : resultMatrix) + if (d2.getOrigin().equals(destination) && d2.getDestination().equals(origin)) // If it's opposite distance + d2.setDistance(distance); + + } + + return resultMatrix; + + } + + @NonNull + private static ArrayList adaptPlacesToDistances(ArrayList distancesArray, ArrayList places) { + + ArrayList matrixElements = new ArrayList<>(); // To keep the result in memory + + for (int i = 0; i < places.size() - 1; i++) { + Location p = places.get(i); + Location p2 = places.get(i + 1); + + for (MatrixElement d : distancesArray) { + + if (d.getOrigin().equals(p) && d.getDestination().equals(p2)) + matrixElements.add(d); + + } + + } + + return matrixElements; + } + +} diff --git a/vrpsolver/src/test/java/id/my/dsm/vrpsolver/NearestNeighborOptimizerTest.java b/vrpsolver/src/test/java/id/my/dsm/vrpsolver/NearestNeighborOptimizerTest.java new file mode 100644 index 0000000..9ff956d --- /dev/null +++ b/vrpsolver/src/test/java/id/my/dsm/vrpsolver/NearestNeighborOptimizerTest.java @@ -0,0 +1,65 @@ +package id.my.dsm.vrpsolver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; +import id.my.dsm.vrpsolver.optimization.NearestNeighborOptimizer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; + +public class NearestNeighborOptimizerTest { + + @Mock + private Logger logger; + + private NearestNeighborOptimizer optimizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + doNothing().when(logger).d(anyString(), anyString()); + optimizer = new NearestNeighborOptimizer(logger); + } + + @Test + public void testOptimize() { + // Create a simple test case + List places = new ArrayList<>(); + places.add(new Location(null, Location.Profile.SOURCE, 0)); + places.add(new Location(null, Location.Profile.DESTINATION, 10)); + places.add(new Location(null, Location.Profile.DESTINATION, 10)); + + List matrix = new ArrayList<>(); + matrix.add(new MatrixElement(places.get(0), places.get(1), 10)); + matrix.add(new MatrixElement(places.get(0), places.get(2), 20)); + matrix.add(new MatrixElement(places.get(1), places.get(0), 10)); + matrix.add(new MatrixElement(places.get(1), places.get(2), 5)); + matrix.add(new MatrixElement(places.get(2), places.get(0), 20)); + matrix.add(new MatrixElement(places.get(2), places.get(1), 5)); + + List vehicles = new ArrayList<>(); + vehicles.add(new Vehicle.Builder().withCapacity(20).build()); + + List solutions = optimizer.optimize(matrix, places, vehicles, 0, true); + + assertEquals(3, solutions.size()); + assertEquals(places.get(0), solutions.get(0).getOrigin()); + assertEquals(places.get(1), solutions.get(0).getDestination()); + assertEquals(places.get(1), solutions.get(1).getOrigin()); + assertEquals(places.get(2), solutions.get(1).getDestination()); + assertEquals(places.get(2), solutions.get(2).getOrigin()); + assertEquals(places.get(0), solutions.get(2).getDestination()); + } +} diff --git a/vrpsolver/src/test/java/id/my/dsm/vrpsolver/SavingMatrixOptimizerTest.java b/vrpsolver/src/test/java/id/my/dsm/vrpsolver/SavingMatrixOptimizerTest.java new file mode 100644 index 0000000..20ab42d --- /dev/null +++ b/vrpsolver/src/test/java/id/my/dsm/vrpsolver/SavingMatrixOptimizerTest.java @@ -0,0 +1,70 @@ +package id.my.dsm.vrpsolver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +import id.my.dsm.vrpsolver.logging.Logger; +import id.my.dsm.vrpsolver.model.Location; +import id.my.dsm.vrpsolver.model.MatrixElement; +import id.my.dsm.vrpsolver.model.Solution; +import id.my.dsm.vrpsolver.model.Vehicle; +import id.my.dsm.vrpsolver.optimization.SavingMatrixOptimizer; +import id.my.dsm.vrpsolver.utils.MatrixUtils; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; + +public class SavingMatrixOptimizerTest { + + @Mock + private Logger logger; + + private SavingMatrixOptimizer optimizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + doNothing().when(logger).d(anyString(), anyString()); + doNothing().when(logger).e(anyString(), anyString()); + optimizer = new SavingMatrixOptimizer(logger); + MatrixUtils.setLogger(logger); + } + + @Test + public void testOptimize() { + // Create a simple test case + List places = new ArrayList<>(); + places.add(new Location(null, Location.Profile.SOURCE, 0)); + places.add(new Location(null, Location.Profile.DESTINATION, 10)); + places.add(new Location(null, Location.Profile.DESTINATION, 10)); + + List matrix = new ArrayList<>(); + matrix.add(new MatrixElement(places.get(0), places.get(1), 10)); + matrix.add(new MatrixElement(places.get(0), places.get(2), 20)); + matrix.add(new MatrixElement(places.get(1), places.get(0), 10)); + matrix.add(new MatrixElement(places.get(1), places.get(2), 5)); + matrix.add(new MatrixElement(places.get(2), places.get(0), 20)); + matrix.add(new MatrixElement(places.get(2), places.get(1), 5)); + + MatrixUtils.calculateDistanceSavingValue(places, matrix); + + List vehicles = new ArrayList<>(); + vehicles.add(new Vehicle.Builder().withCapacity(20).build()); + + List solutions = optimizer.optimize(matrix, places, vehicles, 0, true); + + assertEquals(3, solutions.size()); + assertEquals(places.get(0), solutions.get(0).getOrigin()); + assertEquals(places.get(1), solutions.get(0).getDestination()); + assertEquals(places.get(1), solutions.get(1).getOrigin()); + assertEquals(places.get(2), solutions.get(1).getDestination()); + assertEquals(places.get(2), solutions.get(2).getOrigin()); + assertEquals(places.get(0), solutions.get(2).getDestination()); + } +}