com.berniesanders.fieldthebern.views.MapScreenView.java Source code

Java tutorial

Introduction

Here is the source code for com.berniesanders.fieldthebern.views.MapScreenView.java

Source

/*
 * Copyright (c) 2016 - Bernie 2016, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.berniesanders.fieldthebern.views;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.location.Address;
import android.location.Location;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import com.berniesanders.fieldthebern.R;
import com.berniesanders.fieldthebern.controllers.ActionBarService;
import com.berniesanders.fieldthebern.controllers.LocationService;
import com.berniesanders.fieldthebern.controllers.PermissionService;
import com.berniesanders.fieldthebern.media.ResponseColor;
import com.berniesanders.fieldthebern.models.ApiAddress;
import com.berniesanders.fieldthebern.mortar.DaggerService;
import com.berniesanders.fieldthebern.mortar.HandlesBack;
import com.berniesanders.fieldthebern.parsing.CanvassResponseEvaluator;
import com.berniesanders.fieldthebern.screens.MapScreen;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import flow.Flow;
import flow.path.Path;
import flow.path.PathContext;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.apache.commons.lang3.reflect.FieldUtils;
import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;

/**
 *
 */
public class MapScreenView extends FrameLayout implements HandlesBack {

    MapFragment mapFragment;
    GoogleMap googleMap;
    WeakReference<Activity> activityWeakReference;
    Subscription cameraSubscription;
    Subscription locationSubscription;
    Subscription geocodeSubscription;
    Handler handler;
    Observer<Location> locationObserver;
    ApiAddress address;

    @Bind(R.id.address)
    TextView addressTextView;

    @Bind(R.id.leaning)
    TextView leaningTextView;

    @Bind(R.id.pin_drop)
    ImageView pinDrop;

    @Bind(R.id.progressBar)
    ProgressBar progressBar;

    @Inject
    MapScreen.Presenter presenter;
    private OnCameraChange onCameraChangeListener;
    private OnAddressChange onAddressChangeListener;
    private OnMarkerClick onMarkerClick;
    private CameraPosition cameraPosition;
    private Map<String, ApiAddress> markerAddressMap = new HashMap<>();
    private List<ApiAddress> nearbyAddresses;
    private boolean isAttached = false;

    public MapScreenView(Context context) {
        super(context);
        injectSelf(context);
    }

    public MapScreenView(Context context, AttributeSet attrs) {
        super(context, attrs);
        injectSelf(context);
    }

    public MapScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        injectSelf(context);
    }

    private void injectSelf(Context context) {
        //This is a hack to safely get a reference to the activity.
        //Flow is internally already passing around the references in a private Map<Path,Context>
        //So we use a little hacky reflection tool to steal the activity ref
        //but hold it in a weak ref
        //TODO: we could probably change this to a "controller"
        PathContext pathContext = (PathContext) context;
        Map<Path, Context> contextMap = new HashMap<>();
        try {
            contextMap = (Map<Path, Context>) FieldUtils.readDeclaredField(context, "contexts", true);
        } catch (IllegalAccessException e) {
            Timber.e(e, "error reading path contexts...");
            e.printStackTrace();
        }

        for (Context ctx : contextMap.values()) {
            if (ctx instanceof Activity) {
                Timber.v("We found an activity!");
                activityWeakReference = new WeakReference<>((Activity) ctx);
            }
        }

        DaggerService.<MapScreen.Component>getDaggerComponent(context, DaggerService.DAGGER_SERVICE).inject(this);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Timber.v("onFinishInflate");

        if (activityWeakReference != null) {
            mapFragment = (MapFragment) activityWeakReference.get().getFragmentManager()
                    .findFragmentById(R.id.map_frag);

            mapFragment.getMapAsync(new OnMapReadyCallback() {

                @Override
                public void onMapReady(GoogleMap gmap) {
                    Timber.v("OnMapReadyCallback");
                    if (PermissionService.get(MapScreenView.this).isGranted()) {
                        MapScreenView.this.googleMap = gmap;
                        gmap.setMyLocationEnabled(true);
                        gmap.getUiSettings().setMapToolbarEnabled(false);
                        initCameraPosition(gmap);
                    } else {
                        // Display a SnackBar with an explanation and a button to trigger the request.
                        Snackbar.make(MapScreenView.this, R.string.permission_contacts_rationale,
                                Snackbar.LENGTH_INDEFINITE)
                                .setAction(android.R.string.ok, new View.OnClickListener() {
                                    @Override
                                    public void onClick(View view) {
                                        PermissionService.get(MapScreenView.this).requestPermission();
                                    }
                                }).show();
                    }
                }
            });
        }

        ButterKnife.bind(this, this);
        handler = new Handler();
    }

    @Override
    protected void onAttachedToWindow() {
        Timber.v("onAttachToWindow");
        super.onAttachedToWindow();
        presenter.takeView(this);
        isAttached = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        isAttached = false;
        presenter.dropView(this);
        super.onDetachedFromWindow();
        if (activityWeakReference != null) {

            MapFragment f = (MapFragment) activityWeakReference.get().getFragmentManager()
                    .findFragmentById(R.id.map_frag);

            if (f != null) {
                activityWeakReference.get().getFragmentManager().beginTransaction().remove(f).commit();

                ///???
                //                activityWeakReference
                //                        .get()
                //                        .getFragmentManager().executePendingTransactions();
            }

            activityWeakReference.clear();
        }

        unsubscribe();
        googleMap = null;
    }

    private void unsubscribe() {
        handler.removeCallbacksAndMessages(null);

        if (cameraSubscription != null) {
            cameraSubscription.unsubscribe();
        }
        if (locationSubscription != null) {
            locationSubscription.unsubscribe();
        }
        if (geocodeSubscription != null) {
            geocodeSubscription.unsubscribe();
        }

        if (googleMap != null) {//thanks fragments
            googleMap.setOnCameraChangeListener(null);
        }
    }

    private void initCameraPosition(final GoogleMap map) {

        Timber.v("initCameraPosition");
        if (nearbyAddresses != null) {
            setNearbyAddresses(nearbyAddresses);
        }

        if (cameraPosition != null) {
            //if we're already there, bail early
            if (map.getCameraPosition().equals(cameraPosition)) {
                return;
            }

            map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));

            if (address == null) {
                connectCameraObservable(map);
            } else {
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        connectCameraObservable(map);
                    }
                }, 1000);
            }
            return;
        }

        locationObserver = new Observer<Location>() {
            @Override
            public void onCompleted() {
                Timber.v("initCameraPosition onCompleted");
            }

            @Override
            public void onError(Throwable e) {
                Timber.e(e, "initCameraPosition onError");
            }

            @Override
            public void onNext(Location location) {
                map.moveCamera(CameraUpdateFactory.newCameraPosition(getCameraPosition(location)));
                connectCameraObservable(map);

                geocodeSubscription = LocationService.get(MapScreenView.this)
                        .reverseGeocode(new LatLng(location.getLatitude(), location.getLongitude()))
                        .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).single()
                        .subscribe(geocodeObserver);
            }
        };

        locationSubscription = LocationService.get(this).get().subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(locationObserver);
    }

    private void connectCameraObservable(final GoogleMap map) {

        if (cameraSubscription == null || cameraSubscription.isUnsubscribed()) {
            cameraSubscription = watchCamera(map).subscribeOn(AndroidSchedulers.mainThread())
                    .observeOn(AndroidSchedulers.mainThread()).debounce(1, TimeUnit.SECONDS)
                    .subscribe(cameraObserver);
        }
    }

    Observer<CameraPosition> cameraObserver = new Observer<CameraPosition>() {
        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {
            Timber.e(e, "onError");
        }

        @Override
        public void onNext(final CameraPosition cameraPosition) {

            post(new Runnable() {
                @Override
                public void run() {

                    if (isAttached) {
                        LatLng latLng = cameraPosition.target;

                        if (onCameraChangeListener != null) {
                            onCameraChangeListener.onCameraChange(cameraPosition, true, getRadius());
                        }

                        geocodeSubscription = LocationService.get(MapScreenView.this).reverseGeocode(latLng)
                                .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                                .subscribe(geocodeObserver);
                    }
                }
            });
        }
    };

    Observer<Address> geocodeObserver = new Observer<Address>() {

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable e) {
            Timber.e(e, "geocodeObserver onError");
        }

        @Override
        public void onNext(Address address) {
            MapScreenView.this.address = ApiAddress.from(address, address.getPremises());

            setAddress(MapScreenView.this.address);
            if (onAddressChangeListener != null) {
                onAddressChangeListener.onAddressChange(MapScreenView.this.address);
            }
        }
    };

    private CameraPosition getCameraPosition(Location location) {
        return new CameraPosition.Builder().target(new LatLng(location.getLatitude(), location.getLongitude()))
                .zoom(18f).build();
    }

    private Observable<CameraPosition> watchCamera(final GoogleMap map) {

        return Observable.create(new Observable.OnSubscribe<CameraPosition>() {

            @Override
            public void call(final Subscriber<? super CameraPosition> subscriber) {

                GoogleMap.OnCameraChangeListener camChangeListener = new GoogleMap.OnCameraChangeListener() {

                    @Override
                    public void onCameraChange(CameraPosition camPosition) {
                        address = null;
                        addressTextView.setText("");
                        leaningTextView.setText("");
                        progressBar.setVisibility(View.VISIBLE);
                        pinDrop.setVisibility(View.GONE);
                        subscriber.onNext(camPosition);
                    }
                };

                map.setOnCameraChangeListener(camChangeListener);
            }
        });
    }

    @Override
    public boolean onBackPressed() {

        ActionBarService.get(this).showToolbar();
        return false;
    }

    public void setAddress(ApiAddress address) {
        Timber.v("setAddress: %s", address.toString());
        leaningTextView.setText("");
        this.address = address;
        addressTextView.setText(address.attributes().street1());
        progressBar.setVisibility(View.GONE);
        pinDrop.setVisibility(View.VISIBLE);
    }

    public void setCameraPosition(CameraPosition cameraPosition) {
        Timber.v("setCameraPosition: %s", cameraPosition.toString());
        this.cameraPosition = cameraPosition;
        if (googleMap != null) {
            initCameraPosition(googleMap);
        }
    }

    public void setOnCameraChangeListener(OnCameraChange onCameraChangeListener) {
        this.onCameraChangeListener = onCameraChangeListener;
    }

    public void setOnAddressChangeListener(OnAddressChange onAddressChangeListener) {
        this.onAddressChangeListener = onAddressChangeListener;
    }

    public void setOnMarkerClick(OnMarkerClick onMarkerClick) {
        this.onMarkerClick = onMarkerClick;
    }

    public void setNearbyAddresses(List<ApiAddress> nearbyAddresses) {
        if (nearbyAddresses == null) {
            return;
        }
        this.nearbyAddresses = nearbyAddresses;
        if (googleMap == null) {
            return;
        }
        googleMap.clear();
        markerAddressMap.clear();

        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_place_white_24dp);

        for (ApiAddress apiAddress : nearbyAddresses) {

            Double lat = apiAddress.attributes().latitude();
            Double lng = apiAddress.attributes().longitude();

            if (lat != null && lng != null) {
                String lastResponse = apiAddress.attributes().bestCanvassResponse();
                Bitmap icon = colorBitmap(bmp, ResponseColor.getColor(lastResponse, getContext()));

                Marker marker = googleMap
                        .addMarker(new MarkerOptions().icon(BitmapDescriptorFactory.fromBitmap(icon))
                                .position(new LatLng(lat, lng)).title(CanvassResponseEvaluator.getText(lastResponse,
                                        getResources().getStringArray(R.array.interest))));

                markerAddressMap.put(marker.getId(), apiAddress);
            }
        }

        googleMap.setOnMarkerClickListener(onMarkerClickListener);
    }

    public int getRadius() {

        double startLat = googleMap.getProjection().getVisibleRegion().latLngBounds.northeast.latitude;
        double startLng = googleMap.getProjection().getVisibleRegion().latLngBounds.northeast.longitude;
        double endLat = googleMap.getProjection().getVisibleRegion().latLngBounds.southwest.latitude;
        double endLng = googleMap.getProjection().getVisibleRegion().latLngBounds.southwest.longitude;

        float[] results = new float[10];
        Location.distanceBetween(startLat, startLng, endLat, endLng, results);
        Timber.v("getRadius: %f", Math.ceil(results[0]));
        return (int) Math.ceil(results[0]);
    }

    GoogleMap.OnMarkerClickListener onMarkerClickListener = new GoogleMap.OnMarkerClickListener() {
        @Override
        public boolean onMarkerClick(Marker marker) {

            //stop watching the camera change while the map moves to the maker
            unsubscribe();
            if (onMarkerClick != null) {
                onMarkerClick.onMarkerClick();
            }

            //set the address manually
            ApiAddress apiAddress = markerAddressMap.get(marker.getId());
            setAddress(apiAddress);
            onAddressChangeListener.onAddressChange(apiAddress);
            leaningTextView.setText(CanvassResponseEvaluator.getText(apiAddress.attributes().bestCanvassResponse(),
                    getResources().getStringArray(R.array.interest)));

            //re-enable the camera watch
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (onCameraChangeListener != null) {
                        //notify the listener that the camera moved
                        onCameraChangeListener.onCameraChange(googleMap.getCameraPosition(), false, 100);
                        connectCameraObservable(googleMap);
                    }
                }
            }, 1500);
            return false;
        }
    };

    private Bitmap colorBitmap(final Bitmap bm, int color) {

        Paint paint = new Paint();
        ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
        paint.setColorFilter(filter);
        Bitmap copiedBitmap = bm.copy(Bitmap.Config.ARGB_8888, true);
        Canvas canvas = new Canvas(copiedBitmap);
        canvas.drawBitmap(bm, 0, 0, paint);
        return copiedBitmap;
    }

    public ApiAddress getCurrentAddress() {
        return address;
    }

    public interface OnCameraChange {
        void onCameraChange(CameraPosition cameraPosition, boolean shouldRefreshAddresses, int radius);
    }

    public interface OnAddressChange {
        void onAddressChange(ApiAddress apiAddress);
    }

    public interface OnMarkerClick {
        void onMarkerClick();
    }
}