SDPepe/AppArt

View on GitHub
app/src/main/java/ch/epfl/sdp/appart/place/PlaceService.java

Summary

Maintainability
B
4 hrs
Test Coverage
A
93%
package ch.epfl.sdp.appart.place;

import android.util.Pair;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.inject.Inject;

import ch.epfl.sdp.appart.location.Location;
import ch.epfl.sdp.appart.location.geocoding.GeocodingService;
import ch.epfl.sdp.appart.location.place.address.Address;
import ch.epfl.sdp.appart.place.helper.PlaceHelper;
import dagger.hilt.android.scopes.ActivityScoped;

@ActivityScoped
public class PlaceService {

    private final GeocodingService geocoder;
    private final PlaceHelper helper;

    @Inject
    public PlaceService(PlaceHelper helper, GeocodingService geocoder) {
        this.helper = helper;
        this.geocoder = geocoder;
    }

    /**
     * Retrieve the top (at most 20) places of interests in addition to their
     * respective distance
     * to the given address.
     *
     * @param address The address from which the query originate
     * @param radius  the radius in which we search for the place.
     * @param type    the type of place we want to find.
     * @param top     the quantity of places we want to retrieve (at most 20).
     * @return
     */
    public CompletableFuture<List<Pair<PlaceOfInterest, Float>>>
    getNearbyPlacesWithDistances(Address address, int radius, String type,
                                 int top) {


        return geocoder.getLocation(address).thenCompose(location -> getNearbyPlacesWithDistances(location, radius, type, top));
    }

    /**
     * Retrieve the nearest places of interest with their respective distance
     * to the Location
     * Given as argument.
     *
     * @param location The location from which the request originate
     * @param radius   The radius in which the query originate
     * @param type     the type of object you want to query
     * @return CompletableFuture<List < Pair < PlaceOfInterest, Float>>> the
     * places with the distances (at most 20).
     */
    public CompletableFuture<List<Pair<PlaceOfInterest, Float>>>
    getNearbyPlacesWithDistances(Location location, int radius, String type,
                                 int top) {


        CompletableFuture<List<PlaceOfInterest>> placesFuture =
                getNearbyPlaces(location, radius, type, top);


        return computeDistances(placesFuture, location);
    }

    /**
     * Retrieve the nearest places of interest with their respective distance
     * to the Location
     * Given as argument.
     *
     * @param location The location from which the request originate
     * @param type     the type of object you want to query
     * @return CompletableFuture<List < Pair < PlaceOfInterest, Float>>> the
     * places with the distances (at most 20).
     */
    public CompletableFuture<List<Pair<PlaceOfInterest, Float>>>
    getNearbyPlacesWithDistances(Location location, String type, int top) {


        CompletableFuture<List<PlaceOfInterest>> placesFuture =
                getNearbyPlaces(location, type, top);


        return computeDistances(placesFuture, location);
    }

    /**
     * Retrieve the top (at most 20) places of interests in addition to their
     * respective distance
     * to the given address.
     *
     * @param address The address from which the query originate
     * @param type    the type of place we want to find.
     * @param top     the quantity of places we want to retrieve (at most 20).
     * @return
     */
    public CompletableFuture<List<Pair<PlaceOfInterest, Float>>>
    getNearbyPlacesWithDistances(Address address, String type, int top) {

        return CompletableFuture.supplyAsync(() -> {
            try {
                Location location = geocoder.getLocation(address).get();
                return getNearbyPlacesWithDistances(location, type, top).get();
            } catch (Exception e) {
                throw new CompletionException(e);
            }
        });
    }

    private CompletableFuture<List<Pair<PlaceOfInterest, Float>>> computeDistances(CompletableFuture<List<PlaceOfInterest>> placesFuture, Location location) {
        CompletableFuture<List<Pair<PlaceOfInterest, Float>>> result =
                new CompletableFuture<>();
        placesFuture.thenAccept(placesOfInterests -> {

            List<PlaceOfInterest> placesWithLocation =
                    placesOfInterests.stream()
                            .filter(PlaceOfInterest::hasLocation)
                            .collect(Collectors.toList());

            List<CompletableFuture<Float>> locationsFutures =
                    placesWithLocation.stream()
                            .map(place -> geocoder.getDistance(location,
                                    place.getLocation()))
                            .collect(Collectors.toList());

            CompletableFuture.allOf(locationsFutures.toArray(new CompletableFuture[locationsFutures.size()])).thenAccept(aVoid -> {

                List<Pair<PlaceOfInterest, Float>> placesWithDistances =
                        IntStream.range(0, placesWithLocation.size()).mapToObj(value -> {
                            return new Pair<>(
                                    placesWithLocation.get(value),
                                    locationsFutures.get(value).join()
                            );
                        }).collect(Collectors.toList());
                result.complete(placesWithDistances);

            });

        });

        placesFuture.exceptionally(throwable -> {
            result.completeExceptionally(throwable);
            return null;
        });
        return result;
    }

    /**
     * Retrieve the top nearby location within the radius range.
     *
     * @param location The <type>Location</type> from which the search is made.
     * @param radius   an <type>int</type> corresponding to the radius of
     *                 search in meters.
     * @param type     the <type>String</type> that represent the type to
     *                 search for.
     * @param top      if you want to get only a subset of results, an
     *                 <type>int</type>
     * @return A CompletableFuture<List<PlaceOfInterest>> the places of
     * interest in a future.
     */
    private CompletableFuture<List<PlaceOfInterest>> getNearbyPlaces(Location location, int radius, String type, int top) {
        CompletableFuture<List<PlaceOfInterest>> placesFuture =
                getNearbyPlaces(location, radius, type);
        CompletableFuture<List<PlaceOfInterest>> result =
                new CompletableFuture<>();
        placesFuture.thenAccept(places -> {
            int topAdjusted = Math.min(top, places.size());
            result.complete(places.subList(0, topAdjusted));
        });
        placesFuture.exceptionally(e -> {
            result.completeExceptionally(e);
            return null;
        });
        return result;
    }

    /**
     * Retrieve the top nearby locations ranked by distance.
     *
     * @param location The <type>Location</type> from which the search is made.
     * @param type     the <type>String</type> that represent the type to
     *                 search for.
     * @param top      if you want to get only a subset of results, an
     *                 <type>int</type>
     * @return A CompletableFuture<List<PlaceOfInterest>> the places of
     * interest in a future.
     */
    private CompletableFuture<List<PlaceOfInterest>> getNearbyPlaces(Location location, String type, int top) {
        CompletableFuture<List<PlaceOfInterest>> placesFuture =
                getNearbyPlaces(location, type);
        CompletableFuture<List<PlaceOfInterest>> result =
                new CompletableFuture<>();
        placesFuture.thenAccept(places -> {
            int topAdjusted = Math.min(top, places.size());
            result.complete(places.subList(0, topAdjusted));
        });
        placesFuture.exceptionally(e -> {
            result.completeExceptionally(e);
            return null;
        });
        return result;
    }


    /**
     * Retrieve the nearby location within the radius range.
     *
     * @param location The <type>Location</type> from which the search is made.
     * @param radius   an <type>int</type> corresponding to the radius of
     *                 search in meters.
     * @param type     the <type>String</type> that represent the type to
     *                 search for.
     * @return A CompletableFuture<List<PlaceOfInterest>> the places of
     * interest in a future.
     */
    private CompletableFuture<List<PlaceOfInterest>> getNearbyPlaces(Location location, int radius, String type) {
        CompletableFuture<List<PlaceOfInterest>> result =
                new CompletableFuture<>();

        //retrieve the raw results from the query as a Json string
        CompletableFuture<String> rawResult = helper.query(location, radius,
                type);

        getResult(rawResult, result);

        return result;
    }

    private void getResult(CompletableFuture<String> rawResult,
                           CompletableFuture<List<PlaceOfInterest>> result) {
        //parse the Json String to a JSONArray to work with
        CompletableFuture<JSONArray> queriesResults =
                rawResult.thenCompose(this::parseRawResults);

        queriesResults.thenAccept(queriesJson -> {

            List<PlaceOfInterest> places = new ArrayList<>();
            List<CompletableFuture<Void>> bitmaps = new ArrayList<>();

            for (int i = 0; i < queriesJson.length(); i++) {
                try {
                    PlaceOfInterest place = new PlaceOfInterest();
                    JSONObject element = (JSONObject) queriesJson.get(i);
                    place.setId(element.optString("place_id"));
                    place.setName(element.optString("name"));
                    place.setAddress(element.optString("vicinity"));
                    place.setRating(element.optDouble("rating"));

                    JSONArray typesArray = element.getJSONArray("types");
                    Set<String> types = new HashSet<>();
                    for (int j = 0; j < typesArray.length(); j++) {
                        types.add(typesArray.optString(i));
                    }

                    place.setTypes(types);

                    JSONObject geometryJson = element.getJSONObject("geometry");
                    JSONObject locationJson = geometryJson.getJSONObject(
                            "location");
                    place.setLocation(locationJson.optDouble("lng"),
                            locationJson.optDouble("lat"));

                    /**
                     * Add bitmap to place
                     */
                    JSONArray photosArraysJson = element.optJSONArray("photos");
                    if (photosArraysJson == null) {
                        places.add(place);
                        continue;
                    }

                    JSONObject photosJson =
                            (JSONObject) photosArraysJson.get(0);

                    int maxHeight = photosJson.getInt("height");
                    int maxWidth = photosJson.getInt("width");

                    String photoReference = photosJson.getString(
                            "photo_reference");

                    bitmaps.add(helper.queryImage(photoReference, maxHeight,
                            maxWidth).thenAccept(place::setBitmap));

                    places.add(place);

                } catch (JSONException e) {
                    result.completeExceptionally(e);
                }

            }
            CompletableFuture.allOf(bitmaps.toArray(new CompletableFuture[bitmaps.size()])).thenAccept(arg -> {
                result.complete(places);
            });
        });
        queriesResults.exceptionally(e -> {
            result.completeExceptionally(e);
            return null;
        });
    }

    /**
     * Retrieve the nearby location within the radius range.
     *
     * @param location The <type>Location</type> from which the search is made.
     * @param type     the <type>String</type> that represent the type to
     *                 search for.
     * @return A CompletableFuture<List<PlaceOfInterest>> the places of
     * interest in a future.
     */
    private CompletableFuture<List<PlaceOfInterest>> getNearbyPlaces(Location location, String type) {
        CompletableFuture<List<PlaceOfInterest>> result =
                new CompletableFuture<>();

        //retrieve the raw results from the query as a Json string
        CompletableFuture<String> rawResult = helper.query(location, type);

        getResult(rawResult, result);

        return result;
    }

    /**
     * Parse the JSON string given in argument to a JSON Array
     *
     * @param rawSearch the Json String
     * @return CompletableFuture<JSONArray> the parsed data
     */
    private CompletableFuture<JSONArray> parseRawResults(String rawSearch) {
        CompletableFuture<JSONArray> result = new CompletableFuture<>();

        JSONObject json = null;

        try {
            json = (JSONObject) new JSONTokener(rawSearch).nextValue();
            String status = (String) json.get("status");
            if (!status.equals("OK") && !status.equals("ZERO_RESULTS")) {
                result.completeExceptionally(new PlaceServiceException(
                        "failed to get the query"));
            }
            JSONArray resultsJson = json.getJSONArray("results");

            if (resultsJson == null) {
                result.completeExceptionally(new PlaceServiceException(
                        "failed to convert candidates to json object"));
            } else {
                result.complete(resultsJson);
            }

        } catch (JSONException e) {
            result.completeExceptionally(e);
        }
        return result;
    }

}