/* * Copyright (c) 2015 Ushahidi Inc * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program in the file LICENSE-AGPL. If not, see * https://www.gnu.org/licenses/agpl-3.0.html */ package com.ushahidi.platform.mobile.app.presentation.view.ui.fragment; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; 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.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.PolygonOptions; import com.google.android.gms.maps.model.PolylineOptions; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.ClusterManager; import com.google.maps.android.clustering.view.DefaultClusterRenderer; import com.google.maps.android.ui.IconGenerator; import com.google.maps.android.ui.SquareTextView; import com.addhen.android.raiburari.presentation.ui.fragment.BaseFragment; import com.cocoahero.android.geojson.FeatureCollection; import com.ushahidi.platform.mobile.app.R; import com.ushahidi.platform.mobile.app.presentation.UshahidiApplication; import com.ushahidi.platform.mobile.app.presentation.di.components.post.MapPostComponent; import com.ushahidi.platform.mobile.app.presentation.model.ClusterMarkerModel; import com.ushahidi.platform.mobile.app.presentation.model.GeoJsonModel; import com.ushahidi.platform.mobile.app.presentation.presenter.post.MapPostPresenter; import com.ushahidi.platform.mobile.app.presentation.state.ReloadPostEvent; import com.ushahidi.platform.mobile.app.presentation.state.RxEventBus; import com.ushahidi.platform.mobile.app.presentation.util.GeoJsonLoadUtility; import com.ushahidi.platform.mobile.app.presentation.view.post.MapPostView; import com.ushahidi.platform.mobile.app.presentation.view.ui.activity.PostActivity; import com.ushahidi.platform.mobile.app.presentation.view.ui.navigation.Launcher; import org.json.JSONException; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import javax.inject.Inject; import rx.subscriptions.CompositeSubscription; /** * Provides Google maps as a fragment in a {@link android.support.v4.view.ViewPager}. Has support * for clustering markers and drawing polygons. * * @author Ushahidi Team <team@ushahidi.com> */ public class MapPostFragment extends BaseFragment implements OnMapReadyCallback, ClusterManager.OnClusterClickListener<ClusterMarkerModel>, ClusterManager.OnClusterInfoWindowClickListener<ClusterMarkerModel>, ClusterManager.OnClusterItemInfoWindowClickListener<ClusterMarkerModel>, MapPostView { private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000; private static MapPostFragment mMapPostFragment; @Inject MapPostPresenter mMapPostPresenter; @Inject Launcher mLauncher; RxEventBus mRxEventBus; private ClusterManager<ClusterMarkerModel> mClusterManager; private MapFragment mMapFragment; private GoogleMap mMap; private HashMap<Marker, ClusterMarkerModel> markers = new HashMap<>(); private CompositeSubscription mSubscriptions = new CompositeSubscription(); private Snackbar mSnackbar; public MapPostFragment() { super(R.layout.map_post, 0); } public static MapPostFragment newInstance() { if (mMapPostFragment == null) { mMapPostFragment = new MapPostFragment(); } return mMapPostFragment; } public void onResume() { super.onResume(); mMapPostPresenter.resume(); // Set up Google map setUpMapIfNeeded(); } @Override public void onStart() { super.onStart(); mSubscriptions .add(mRxEventBus.toObservable().subscribe(event -> { if (event instanceof ReloadPostEvent) { ReloadPostEvent reloadPostEvent = (ReloadPostEvent) event; if (reloadPostEvent != null) { mMapPostPresenter.loadGeoJsonFromDb(); } } })); } @Override public void onPause() { super.onPause(); mMapPostPresenter.pause(); } @Override public void onStop() { super.onStop(); if (!mSubscriptions.isUnsubscribed()) { mSubscriptions.unsubscribe(); } } @Override public void onDestroy() { super.onDestroy(); if (!mSubscriptions.isUnsubscribed()) { mSubscriptions.unsubscribe(); } mMapPostPresenter.destroy(); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initialize(); mRxEventBus = UshahidiApplication.getRxEventBusInstance(); } private void initialize() { checkPlayServices(); getMapPostComponent(MapPostComponent.class).inject(this); mMapPostPresenter.setView(this); mMapFragment = (MapFragment) getActivity().getFragmentManager() .findFragmentById(R.id.post_map); } private void setUpMapIfNeeded() { // Do a null check to confirm that we have not already instantiated the map. if (mMap == null) { // Try to obtain the map from the SupportMapFragment. mMap = mMapFragment.getMap(); // Check if we were successful in obtaining the map. setUpClusterer(); } } private void setUpClusterer() { if (mMap != null) { mClusterManager = new ClusterManager<>(getActivity(), mMap); final PostModelRenderer postModelRenderer = new PostModelRenderer( getActivity().getApplicationContext(), new WeakReference<>(mMap), new WeakReference<>(mClusterManager), new WeakReference<>(markers)); mClusterManager.setRenderer(postModelRenderer); mMap.setOnCameraChangeListener(mClusterManager); mMap.setOnMarkerClickListener(mClusterManager); mMap.setOnInfoWindowClickListener(mClusterManager); //mMap.getUiSettings().setZoomControlsEnabled(true); mClusterManager.setOnClusterInfoWindowClickListener(this); mClusterManager.setOnClusterItemInfoWindowClickListener(this); } } /** * Check if Google play services is installed on the user's device. If it's not * prompt user about it. * * @return boolean */ private void checkPlayServices() { int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getActivity()); if (resultCode != ConnectionResult.SUCCESS) { if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { GooglePlayServicesUtil.getErrorDialog(resultCode, getActivity(), PLAY_SERVICES_RESOLUTION_REQUEST).show(); } else { getActivity().finish(); } } } @Override public void showGeoJson(GeoJsonModel geoJsonModel) { FeatureCollection featureCollection; ArrayList<Object> uiObjects = new ArrayList<>(); try { featureCollection = GeoJsonLoadUtility .parseGeoJson(geoJsonModel.getGeoJson()); uiObjects = GeoJsonLoadUtility .createUIObjectsFromGeoJSONObjects(featureCollection, getResources().getColor(R.color.white), getResources().getColor(R.color.color_accent)); } catch (JSONException e) { e.printStackTrace(); } mClusterManager.clearItems(); for (Object uiObj : uiObjects) { if (uiObj instanceof PolylineOptions) { mMap.addPolyline((PolylineOptions) uiObj); } else if (uiObj instanceof PolygonOptions) { mMap.addPolygon((PolygonOptions) uiObj); } else { if (mClusterManager != null) { mClusterManager.addItem((ClusterMarkerModel) uiObj); mClusterManager.cluster(); } } } } @Override public void showLoading() { // Do nothing } @Override public void hideLoading() { // Do nothing } @Override public void showRetry() { mSnackbar = Snackbar .make(getView(), getString(R.string.geojson_not_found), Snackbar.LENGTH_LONG) .setAction(R.string.retry, e -> mMapPostPresenter.loadGeoJsonFromOnline()); setSnackbarTextColor(); } @Override public void hideRetry() { if (mSnackbar != null) { mSnackbar.dismiss(); } } @Override public void showError(String s) { mSnackbar = Snackbar.make(getView(), s, Snackbar.LENGTH_LONG); setSnackbarTextColor(); } /** * Change Snackbar text color to orange by finding the TextView associated with * it. A bit of a hack to get this working as it doesn't come with a native API for doing this. */ private void setSnackbarTextColor() { View view = mSnackbar.getView(); TextView tv = (TextView) view.findViewById(android.support.design.R.id.snackbar_text); tv.setTextColor(getAppContext().getResources().getColor(R.color.orange)); mSnackbar.show(); } @Override public Context getAppContext() { return getActivity().getApplicationContext(); } private <C> C getMapPostComponent(Class<C> componentType) { return componentType.cast(((PostActivity) getActivity()).getMapPostComponent()); } @Override public boolean onClusterClick(Cluster<ClusterMarkerModel> cluster) { return false; } @Override public void onClusterInfoWindowClick(Cluster<ClusterMarkerModel> cluster) { // Do nothing } @Override public void onClusterItemInfoWindowClick(ClusterMarkerModel clusterMarkerModel) { mLauncher.launchDetailPost(clusterMarkerModel._id, clusterMarkerModel.getTitle()); } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; } /** * Draws custom colored circle for clustered pins. */ private static class PostModelRenderer extends DefaultClusterRenderer<ClusterMarkerModel> { private final IconGenerator mClusterIconGenerator; private final float mDensity; private ShapeDrawable mColoredCircleBackground; private Context mContext; private WeakReference<HashMap<Marker, ClusterMarkerModel>> mMarkers; /** * Icons for each bucket. */ private SparseArray<BitmapDescriptor> mIcons = new SparseArray<>(); public PostModelRenderer(Context context, WeakReference<GoogleMap> map, WeakReference<ClusterManager> clusterManager, WeakReference<HashMap<Marker, ClusterMarkerModel>> markers) { super(context, map.get(), clusterManager.get()); mContext = context; mDensity = context.getResources().getDisplayMetrics().density; mClusterIconGenerator = new IconGenerator(mContext); mClusterIconGenerator.setContentView(makeSquareTextView()); mClusterIconGenerator.setTextAppearance(R.style.CustomClusterIcon_TextAppearance); mClusterIconGenerator.setBackground(makeClusterBackground()); mMarkers = markers; } private SquareTextView makeSquareTextView() { SquareTextView squareTextView = new SquareTextView(mContext); ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); squareTextView.setLayoutParams(layoutParams); squareTextView.setId(R.id.text); squareTextView.setTextColor(mContext.getResources().getColor(R.color.black_darker)); int twelveDpi = (int) (12 * mDensity); squareTextView.setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi); return squareTextView; } private LayerDrawable makeClusterBackground() { mColoredCircleBackground = new ShapeDrawable(new OvalShape()); ShapeDrawable outline = new ShapeDrawable(new OvalShape()); outline.getPaint().setColor(mContext.getResources() .getColor(R.color.cluster_solid_color)); // Solid red LayerDrawable background = new LayerDrawable( new Drawable[]{outline, mColoredCircleBackground}); int strokeWidth = (int) (mDensity * 5); background.setLayerInset(1, strokeWidth, strokeWidth, strokeWidth, strokeWidth); return background; } private int getColor(int clusterSize) { final float hueRange = 100; final float sizeRange = 300; final float size = Math.min(clusterSize, sizeRange); final float hue = (sizeRange - size) * (sizeRange - size) / (sizeRange * sizeRange) * hueRange; return Color.HSVToColor(new float[]{ hue, 0f, 1f }); } @Override protected void onBeforeClusterItemRendered(ClusterMarkerModel geoJsonModel, MarkerOptions markerOptions) { super.onBeforeClusterItemRendered(geoJsonModel, markerOptions); markerOptions.snippet(geoJsonModel.getDescription()); markerOptions.title(geoJsonModel.getTitle()); } @Override protected void onBeforeClusterRendered(Cluster<ClusterMarkerModel> cluster, MarkerOptions markerOptions) { super.onBeforeClusterRendered(cluster, markerOptions); int bucket = getBucket(cluster); BitmapDescriptor descriptor = mIcons.get(bucket); if (descriptor == null) { mColoredCircleBackground.getPaint().setColor(getColor(bucket)); descriptor = BitmapDescriptorFactory .fromBitmap(mClusterIconGenerator.makeIcon(getClusterText(bucket))); mIcons.put(bucket, descriptor); } markerOptions.anchor(.5f, .5f); markerOptions.icon(descriptor); } @Override protected boolean shouldRenderAsCluster(Cluster cluster) { // Always render clusters. return cluster.getSize() > 1; } protected void onClusterRendered(Cluster<ClusterMarkerModel> cluster, Marker marker) { super.onClusterRendered(cluster, marker); for (ClusterMarkerModel geoJsonModel : cluster.getItems()) { mMarkers.get().put(marker, geoJsonModel); } } } }