rkuenzi-epfl/Wnder

View on GitHub
app/src/main/java/com/github/wnder/guessLocation/MapBoxHelper.java

Summary

Maintainability
A
0 mins
Test Coverage
B
84%
package com.github.wnder.guessLocation;

import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;

import com.github.wnder.R;
import com.github.wnder.user.GlobalUser;
import com.mapbox.api.staticmap.v1.MapboxStaticMap;
import com.mapbox.api.staticmap.v1.StaticMapCriteria;
import com.mapbox.api.staticmap.v1.models.StaticMarkerAnnotation;
import com.mapbox.geojson.LineString;
import com.mapbox.geojson.Point;
import com.mapbox.geojson.Polygon;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.style.layers.FillLayer;
import com.mapbox.mapboxsdk.style.layers.Property;
import com.mapbox.mapboxsdk.style.layers.PropertyFactory;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.style.sources.Source;
import com.mapbox.turf.TurfMeta;
import com.mapbox.turf.TurfTransformation;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

public class MapBoxHelper {

    private static final long POINT_ANIMATION_DURATION = 200;

    private static final String COLOR_RED = "f84d4d";
    private static final String COLOR_PURPLE = "7753eb";

    /**
     * Animate on a line a point from an origin to a destination for a MapBox GeoJsonSource.
     *
     * @param animator the animator
     * @param originPoint is the point where the animation begins
     * @param destinationPoint is the point where the animation ends
     * @param source of the GeoJsonSource on which to apply the animation
     * @return the new position reached at the end of the animation
     */
    protected static LatLng updatePositionByLineAnimation(GeoJsonSource source, ValueAnimator animator, LatLng originPoint, @NonNull LatLng destinationPoint) {

        if (animator != null && animator.isStarted()) {
            originPoint = (LatLng) animator.getAnimatedValue();
            animator.cancel();
        }

        animator = ObjectAnimator
                .ofObject(latLngEvaluator, originPoint, destinationPoint)
                .setDuration(POINT_ANIMATION_DURATION);
        animator.addUpdateListener(animatorUpdateListenerForGJSource(source));
        animator.start();

        return destinationPoint;
    }

    //Animator update listener creator for a GeoJsonSource
    private static ValueAnimator.AnimatorUpdateListener animatorUpdateListenerForGJSource(GeoJsonSource source) {
        return new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                LatLng animatedPosition = (LatLng) valueAnimator.getAnimatedValue();
                source.setGeoJson(Point.fromLngLat(animatedPosition.getLongitude(), animatedPosition.getLatitude()));
            }
        };
    }

    // Class is used to interpolate the marker animation.
    private static final TypeEvaluator<LatLng> latLngEvaluator = new TypeEvaluator<LatLng>() {
        private final LatLng latLng = new LatLng();

        @Override
        public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
            latLng.setLatitude(startValue.getLatitude() + ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
            latLng.setLongitude(startValue.getLongitude() + ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
            return latLng;
        }
    };

    /**
     * Draw a hollow red circle on a MapBox
     *
     * @param context context of the application
     * @param mapboxMap MapBox map on which to draw
     * @param position position of the center of the circle
     */
    protected static void drawCircle(@NonNull android.content.Context context, MapboxMap mapboxMap, LatLng position) {
        int distanceDiameter = GlobalUser.getUser().getRadius();

        //Create circles
        Point center = Point.fromLngLat(position.getLongitude(), position.getLatitude());
        Polygon outerCirclePolygon = TurfTransformation.circle(center,  distanceDiameter + distanceDiameter/15.0, "kilometers");
        Polygon innerCirclePolygon = TurfTransformation.circle(center, (double) distanceDiameter, "kilometers");

        GeoJsonSource outerCircleSource = new GeoJsonSource(String.valueOf(R.string.RED_CIRCLE_SOURCE_ID), outerCirclePolygon);

        //Create hollow circle
        if (outerCircleSource != null) {
            outerCircleSource.setGeoJson(Polygon.fromOuterInner(
                    LineString.fromLngLats(TurfMeta.coordAll(outerCirclePolygon, false)),
                    LineString.fromLngLats(TurfMeta.coordAll(innerCirclePolygon, false))
            ));
        }

        //Set mapbox style
        Style style = mapboxMap.getStyle();
        style.addSource(outerCircleSource);
        style.addLayer(new FillLayer(String.valueOf(R.string.RED_CIRCLE_LAYER_ID), String.valueOf(R.string.RED_CIRCLE_SOURCE_ID)).withProperties(
                PropertyFactory.fillColor(ContextCompat.getColor(context, R.color.red)),
                PropertyFactory.fillOpacity(0.4f)
        ));
    }

    /**
     * Updates the hollow red circle on MapBox
     *
     * @param context context of the application
     * @param mapboxMap MapBox map on which to draw
     * @param position position of the center of the circle
     */
    protected static void updateCircle(@NonNull android.content.Context context, MapboxMap mapboxMap, LatLng position){
        //Remove old mapbox style
        Style style = mapboxMap.getStyle();
        style.removeLayer(String.valueOf(R.string.RED_CIRCLE_LAYER_ID));
        style.removeSource(String.valueOf(R.string.RED_CIRCLE_SOURCE_ID));
        //Add new mapbox style
        drawCircle(context, mapboxMap, position);
    }

    /**
     * Compute the zoom to use on mapbox knowing how big the region to see is.
     *
     * @param cameraPosition the mamera position on MapBox
     * @param kilometers radius of the circle we want to be able to see
     * @return the zoom to use on mapbox
     */
    protected static double zoomFromKilometers(LatLng cameraPosition, int kilometers) {
        int absLat = Math.abs((int) cameraPosition.getLatitude());

        //Documentation used for this calcul: https://docs.mapbox.com/help/glossary/zoom-level/
        double zoomFromKilometerTheory = 13.6 - Math.log((double) kilometers)/Math.log(2); //The zoom to apply given an amount of kilometer in Theory for latitude 0
        double latitudeDeformation = -0.00046*Math.pow(absLat, 2); //The latitude deformation effect to apply on the zoom parameter if the latitude isn't 0

        return zoomFromKilometerTheory + latitudeDeformation;
    }

    /**
     * Set the Progress bar to a predefined set of color (red, yellow, cyan and blue)
     * @param bar progress bar
     * @param barValue bar value
     */
    protected static void setHotBarColor(ProgressBar bar, int barValue){
        final int ONE_QUARTER = 25;
        final int TWO_QUARTER = 50;
        final int THREE_QUARTER = 75;

        bar.setProgress(barValue);
        bar.setProgressTintList(ColorStateList.valueOf(Color.BLUE));
        if (barValue > THREE_QUARTER){
            bar.setProgressTintList(ColorStateList.valueOf(Color.RED));
        }
        else if (barValue > TWO_QUARTER){
            bar.setProgressTintList(ColorStateList.valueOf(Color.YELLOW));
        }
        else if (barValue > ONE_QUARTER){
            bar.setProgressTintList(ColorStateList.valueOf(Color.CYAN));
        }
    }

    /**
     * Adds the guess source to the style
     * @param context context
     * @param style style to add source to
     * @param guessSource guess source to add
     */
    protected static void addGuessToStyle(Context context, Style style, Source guessSource){
        style.addImage((String.valueOf(R.string.GUESS_ICON_ID)), BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_marker_icon_60px_red));
        style.addSource(guessSource);
        style.addLayer(new SymbolLayer(String.valueOf(R.string.GUESS_LAYER_ID), String.valueOf(R.string.GUESS_SOURCE_ID))
                .withProperties(
                        PropertyFactory.iconImage(String.valueOf(R.string.GUESS_ICON_ID)),
                        PropertyFactory.iconIgnorePlacement(true),
                        PropertyFactory.iconAllowOverlap(true)
                ));
    }

    /**
     * Adds the arrow source to the style
     * @param context context
     * @param style style to add source to
     * @param arrowSource arrow source to add
     * @param mapboxMap mapbox map
     */
    protected static void addArrowToStyle(Context context, Style style, Source arrowSource, MapboxMap mapboxMap){
        style.addImage((String.valueOf(R.string.ORANGE_ARROW_ICON_ID)), BitmapFactory.decodeResource(context.getResources(), R.drawable.orange_arrow));
        style.addSource(arrowSource);
        style.addLayer(new SymbolLayer(String.valueOf(R.string.ORANGE_ARROW_LAYER_ID), String.valueOf(R.string.ORANGE_ARROW_SOURCE_ID))
                .withProperties(
                        PropertyFactory.visibility(Property.NONE),
                        PropertyFactory.iconImage(String.valueOf(R.string.ORANGE_ARROW_ICON_ID)),
                        PropertyFactory.iconRotate((float) mapboxMap.getCameraPosition().bearing),
                        PropertyFactory.iconIgnorePlacement(true),
                        PropertyFactory.iconAllowOverlap(true)
                ));
    }

    /**
     * Adds the picture source to the style
     * @param context context
     * @param style style to add source to
     * @param pictureSource picture source to add
     */
    protected static void addPictureToStyle(Context context, Style style, Source pictureSource){
        style.addImage((String.valueOf(R.string.PICTURE_ICON_ID)), BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_marker_icon_60px_purple));
        style.addSource(pictureSource);
        style.addLayer(new SymbolLayer(String.valueOf(R.string.PICTURE_LAYER_ID), String.valueOf(R.string.PICTURE_SOURCE_ID))
                .withProperties(
                        PropertyFactory.visibility(Property.NONE),
                        PropertyFactory.iconImage(String.valueOf(R.string.PICTURE_ICON_ID)),
                        PropertyFactory.iconIgnorePlacement(true),
                        PropertyFactory.iconAllowOverlap(true)
                ));
    }

    /**
     * Apply consumer function when a map snapshot for a particular guess is available
     * @param context application context
     * @param guessLatLng the position of the guess
     * @param pictureLatLng the actual position of the picture
     * @param mapSnapshotAvailable consumer function
     * @return a Future of all user scores
     */
    public static void onMapSnapshotAvailable(Context context, LatLng guessLatLng, LatLng pictureLatLng, Consumer<Bitmap> mapSnapshotAvailable) {
        StaticMarkerAnnotation guessMarker = buildMarker(guessLatLng, COLOR_RED);
        StaticMarkerAnnotation pictureMarker = buildMarker(pictureLatLng, COLOR_PURPLE);

        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int width = displayMetrics.widthPixels;
        int height = displayMetrics.heightPixels;
        boolean retina = displayMetrics.densityDpi >= DisplayMetrics.DENSITY_HIGH;
        if (retina) {
            width /= 2;
            height /= 2;
        }

        MapboxStaticMap staticMap = MapboxStaticMap.builder()
                .accessToken(context.getString(R.string.mapbox_access_token))
                .styleId(StaticMapCriteria.SATELLITE_STREETS_STYLE)
                .staticMarkerAnnotations(Arrays.asList(guessMarker, pictureMarker))
                .cameraAuto(true)
                .width(width)
                .height(height)
                .retina(retina)
                .build();

        onDownloadedBitmapAvailable(staticMap.url().url(), mapSnapshotAvailable);
    }

    private static StaticMarkerAnnotation buildMarker(LatLng latLng, String color) {
        return StaticMarkerAnnotation.builder()
                .name(StaticMapCriteria.LARGE_PIN)
                .color(color)
                .lnglat(Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude()))
                .build();
    }

    private static void onDownloadedBitmapAvailable(URL url, Consumer<Bitmap> downloadedBitmapAvailable) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Handler handler = new Handler(Looper.getMainLooper());

        executor.execute(() -> {
            try {
                Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
                handler.post(() -> {
                    downloadedBitmapAvailable.accept(bitmap);
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}