Java tutorial
/* * Copyright (c) 2017 Washington State Department of Transportation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package gov.wa.wsdot.android.wsdot.ui.trafficmap; import android.Manifest; import android.animation.Animator; import android.annotation.SuppressLint; import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.location.Location; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat.OnRequestPermissionsResultCallback; import android.support.v4.content.ContextCompat; import android.support.v4.content.res.ResourcesCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.InputType; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.RotateAnimation; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.crashlytics.android.Crashlytics; import com.getkeepsafe.taptargetview.TapTarget; import com.getkeepsafe.taptargetview.TapTargetView; import com.google.android.gms.analytics.HitBuilders; import com.google.android.gms.analytics.Tracker; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.common.api.ResolvableApiException; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationCallback; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationSettingsResponse; import com.google.android.gms.location.SettingsClient; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener; import com.google.android.gms.maps.GoogleMap.OnMyLocationButtonClickListener; 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.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.Task; import com.google.gson.Gson; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.ClusterItem; 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 org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Timer; import java.util.TimerTask; import java.util.zip.GZIPInputStream; import javax.inject.Inject; import dagger.android.AndroidInjection; import gov.wa.wsdot.android.wsdot.R; import gov.wa.wsdot.android.wsdot.database.trafficmap.MapLocationEntity; import gov.wa.wsdot.android.wsdot.shared.CameraItem; import gov.wa.wsdot.android.wsdot.shared.HighwayAlertsItem; import gov.wa.wsdot.android.wsdot.shared.LatLonItem; import gov.wa.wsdot.android.wsdot.shared.RestAreaItem; import gov.wa.wsdot.android.wsdot.ui.BaseActivity; import gov.wa.wsdot.android.wsdot.ui.WsdotApplication; import gov.wa.wsdot.android.wsdot.ui.alert.detail.HighwayAlertDetailsActivity; import gov.wa.wsdot.android.wsdot.ui.alert.map.MapHighwayAlertViewModel; import gov.wa.wsdot.android.wsdot.ui.camera.CameraActivity; import gov.wa.wsdot.android.wsdot.ui.camera.CameraListActivity; import gov.wa.wsdot.android.wsdot.ui.camera.MapCameraViewModel; import gov.wa.wsdot.android.wsdot.ui.trafficmap.alertsinarea.HighwayAlertListActivity; import gov.wa.wsdot.android.wsdot.ui.trafficmap.besttimestotravel.TravelChartsActivity; import gov.wa.wsdot.android.wsdot.ui.trafficmap.expresslanes.SeattleExpressLanesActivity; import gov.wa.wsdot.android.wsdot.ui.trafficmap.news.NewsActivity; import gov.wa.wsdot.android.wsdot.ui.trafficmap.restareas.RestAreaActivity; import gov.wa.wsdot.android.wsdot.ui.trafficmap.socialmedia.SocialMediaTabActivity; import gov.wa.wsdot.android.wsdot.ui.traveltimes.TravelTimesActivity; import gov.wa.wsdot.android.wsdot.util.APIEndPoints; import gov.wa.wsdot.android.wsdot.util.MyLogger; import gov.wa.wsdot.android.wsdot.util.UIUtils; import gov.wa.wsdot.android.wsdot.util.map.RestAreasOverlay; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static com.google.android.gms.location.LocationServices.getFusedLocationProviderClient; public class TrafficMapActivity extends BaseActivity implements OnMarkerClickListener, OnMyLocationButtonClickListener, ConnectionCallbacks, OnConnectionFailedListener, OnRequestPermissionsResultCallback, OnMapReadyCallback, ClusterManager.OnClusterItemClickListener<CameraItem>, ClusterManager.OnClusterClickListener<CameraItem> { private static final String TAG = TrafficMapActivity.class.getSimpleName(); protected static final int REQUEST_CHECK_SETTINGS = 0x1; private GoogleMap mMap; private Handler handler = new Handler(); private Timer timer; private RestAreasOverlay restAreasOverlay = null; private List<CameraItem> cameras = new ArrayList<>(); private List<HighwayAlertsItem> alerts = new ArrayList<>(); private List<RestAreaItem> restAreas = new ArrayList<>(); private HashMap<Marker, String> markers = new HashMap<>(); boolean clusterCameras; boolean showCameras; boolean showAlerts; boolean showCallouts; boolean showRestAreas; FloatingActionButton fabLayers; FloatingActionButton fabCameras; FloatingActionButton fabClusters; FloatingActionButton fabAlerts; FloatingActionButton fabRestareas; TextView fabLabelCameras; TextView fabLabelClusters; TextView fabLabelAlerts; TextView fabLabelRestareas; LinearLayout fabLayoutCameras; LinearLayout fabLayoutClusters; LinearLayout fabLayoutAlerts; LinearLayout fabLayoutRestareas; private ProgressBar mProgressBar; boolean isFABOpen = false; boolean bestTimesAvailable = false; String bestTimesTitle = ""; private ArrayList<LatLonItem> seattleArea = new ArrayList<>(); static private int menu_item_refresh = 1; private static AsyncTask<Void, Void, Void> mRestAreasOverlayTask = null; private LatLngBounds bounds; private double latitude; private double longitude; private int zoom; private GoogleApiClient mGoogleApiClient; private final static int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000; private final int REQUEST_ACCESS_FINE_LOCATION = 100; private static MapHighwayAlertViewModel mapHighwayAlertViewModel; private static MapCameraViewModel mapCameraViewModel; private static FavoriteMapLocationViewModel favoriteMapLocationViewModel; private boolean extrasRead = false; @Inject ViewModelProvider.Factory viewModelFactory; private ClusterManager<CameraItem> mClusterManager; private Toolbar mToolbar; private Tracker mTracker; private LocationCallback mLocationCallback; @Override protected void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); setContentView(R.layout.map); enableAds(getString(R.string.traffic_ad_target)); mProgressBar = findViewById(R.id.progress_bar); mToolbar = findViewById(R.id.toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); } // Setup bounding box for Seattle area. seattleArea.add(new LatLonItem(48.01749, -122.46185)); seattleArea.add(new LatLonItem(48.01565, -121.86584)); seattleArea.add(new LatLonItem(47.27737, -121.86310)); seattleArea.add(new LatLonItem(47.28109, -122.45911)); // Initialize AsyncTasks mRestAreasOverlayTask = new RestAreasOverlayTask(); // Check preferences and set defaults if none set SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); clusterCameras = settings.getBoolean("KEY_CLUSTER_CAMERAS", false); showCameras = settings.getBoolean("KEY_SHOW_CAMERAS", true); showAlerts = settings.getBoolean("KEY_SHOW_ALERTS", true); showCallouts = settings.getBoolean("KEY_SHOW_CALLOUTS", true); showRestAreas = settings.getBoolean("KEY_SHOW_REST_AREAS", false); latitude = Double.parseDouble(settings.getString("KEY_TRAFFICMAP_LAT", "47.5990")); longitude = Double.parseDouble(settings.getString("KEY_TRAFFICMAP_LON", "-122.3350")); zoom = settings.getInt("KEY_TRAFFICMAP_ZOOM", 12); // Check if we came from favorites/MyRoutes/alert if (savedInstanceState != null) { extrasRead = savedInstanceState.getBoolean("read_extras", false); } if (!extrasRead) { Bundle b = getIntent().getExtras(); if (b != null) { if (getIntent().hasExtra("lat")) latitude = b.getDouble("lat", latitude); if (getIntent().hasExtra("long")) longitude = b.getDouble("long", longitude); if (getIntent().hasExtra("zoom")) zoom = b.getInt("zoom", zoom); getIntent().getExtras().clear(); } extrasRead = true; } mGoogleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).addApi(LocationServices.API).build(); mLocationCallback = new LocationCallback() { @Override public void onLocationResult(LocationResult locationResult) { if (locationResult == null) { return; } onNewLocation(locationResult.getLastLocation()); } }; MapFragment mapFragment = (MapFragment) getFragmentManager().findFragmentById(R.id.mapview); mapFragment.getMapAsync(this); favoriteMapLocationViewModel = ViewModelProviders.of(this, viewModelFactory) .get(FavoriteMapLocationViewModel.class); mapCameraViewModel = ViewModelProviders.of(this, viewModelFactory).get(MapCameraViewModel.class); mapCameraViewModel.init(null); setUpFabMenu(); MyLogger.crashlyticsLog("Traffic", "Screen View", "TrafficMapActivity", 1); // check for travel charts new TravelChartsAvailableTask().execute(); } @Override protected void onResume() { super.onResume(); mGoogleApiClient.connect(); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); boolean seenTip = settings.getBoolean("KEY_SEEN_TRAFFIC_LAYERS_TIP", false); if (!seenTip) { try { TapTargetView.showFor(this, // `this` is an Activity TapTarget .forView(fabLayers, "Map Layers", "Tap to edit what information displays on the Traffic Map.") // All options below are optional .outerCircleColor(R.color.primary_default).titleTextSize(20) .titleTextColor(R.color.white).descriptionTextSize(15).textColor(R.color.white) .textTypeface(Typeface.SANS_SERIF).dimColor(R.color.black).drawShadow(true) .cancelable(true).tintTarget(true).transparentTarget(true).targetRadius(40), new TapTargetView.Listener() { @Override public void onTargetClick(TapTargetView view) { super.onTargetClick(view); // This call is optional showFABMenu(); } }); } catch (NullPointerException | IllegalArgumentException e) { Log.e(TAG, "Exception while trying to show tip view"); Log.e(TAG, e.getMessage()); } } settings.edit().putBoolean("KEY_SEEN_TRAFFIC_LAYERS_TIP", true).apply(); } @Override protected void onPause() { super.onPause(); closeFABMenu(); if (mGoogleApiClient.isConnected()) { getFusedLocationProviderClient(this).removeLocationUpdates(mLocationCallback); mGoogleApiClient.disconnect(); } if (timer != null) { timer.cancel(); } // Save last map location and zoom level. SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); try { editor.putString("KEY_TRAFFICMAP_LAT", String.valueOf(mMap.getProjection().getVisibleRegion().latLngBounds.getCenter().latitude)); editor.putString("KEY_TRAFFICMAP_LON", String.valueOf(mMap.getProjection().getVisibleRegion().latLngBounds.getCenter().longitude)); editor.putInt("KEY_TRAFFICMAP_ZOOM", (int) mMap.getCameraPosition().zoom); } catch (NullPointerException e) { Log.e(TAG, "Error getting map bounds. Setting defaults to Seattle instead."); editor.putString("KEY_TRAFFICMAP_LAT", "47.5990"); editor.putString("KEY_TRAFFICMAP_LON", "-122.3350"); editor.putInt("KEY_TRAFFICMAP_ZOOM", 12); } editor.apply(); } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); mMap.getUiSettings().setCompassEnabled(true); mMap.getUiSettings().setMyLocationButtonEnabled(true); mMap.setTrafficEnabled(true); mMap.setOnMyLocationButtonClickListener(this); mMap.setOnMarkerClickListener(this); mMap.setOnMapClickListener(latLng -> closeFABMenu()); mMap.setOnCameraMoveStartedListener(i -> closeFABMenu()); setUpClusterer(); mMap.setOnCameraIdleListener(() -> { mapCameraViewModel.setMapBounds(mMap.getProjection().getVisibleRegion().latLngBounds); mapHighwayAlertViewModel.setMapBounds(mMap.getProjection().getVisibleRegion().latLngBounds); mClusterManager.onCameraIdle(); }); mapHighwayAlertViewModel = ViewModelProviders.of(this, viewModelFactory) .get(MapHighwayAlertViewModel.class); mapHighwayAlertViewModel.getResourceStatus().observe(this, resourceStatus -> { if (resourceStatus != null) { switch (resourceStatus.status) { case LOADING: break; case SUCCESS: if (mToolbar.getMenu().size() > menu_item_refresh) { if (mToolbar.getMenu().getItem(menu_item_refresh).getActionView() != null) { mToolbar.getMenu().getItem(menu_item_refresh).getActionView().getAnimation() .setRepeatCount(0); } } break; case ERROR: Toast.makeText(this, "connection error, failed to load alerts", Toast.LENGTH_SHORT).show(); } } }); mapHighwayAlertViewModel.getDisplayAlerts().observe(this, alertItems -> { Iterator<Entry<Marker, String>> iter = markers.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<Marker, String> entry = iter.next(); if (entry.getValue().equalsIgnoreCase("alert")) { entry.getKey().remove(); iter.remove(); } } alerts.clear(); alerts = alertItems; if (alerts != null) { if (alerts.size() != 0) { for (int i = 0; i < alerts.size(); i++) { LatLng latLng = new LatLng(alerts.get(i).getStartLatitude(), alerts.get(i).getStartLongitude()); Marker marker = mMap.addMarker(new MarkerOptions().position(latLng) .title(alerts.get(i).getEventCategory()).snippet(alerts.get(i).getAlertId()) .icon(BitmapDescriptorFactory.fromResource(alerts.get(i).getCategoryIcon())) .visible(showAlerts)); markers.put(marker, "alert"); } } } }); mapHighwayAlertViewModel.setMapBounds(mMap.getProjection().getVisibleRegion().latLngBounds); mapCameraViewModel.getResourceStatus().observe(this, resourceStatus -> { if (resourceStatus != null) { switch (resourceStatus.status) { case LOADING: break; case SUCCESS: if (mToolbar.getMenu().size() > menu_item_refresh) { if (mToolbar.getMenu().getItem(menu_item_refresh).getActionView() != null) { mToolbar.getMenu().getItem(menu_item_refresh).getActionView().getAnimation() .setRepeatCount(0); } } break; case ERROR: Toast.makeText(this, "connection error, failed to load cameras", Toast.LENGTH_SHORT).show(); } } }); mapCameraViewModel.getDisplayCameras().observe(this, cameraItems -> { if (cameraItems != null) { Iterator<Entry<Marker, String>> iter = markers.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<Marker, String> entry = iter.next(); if (entry.getValue().equalsIgnoreCase("camera")) { entry.getKey().remove(); iter.remove(); } } cameras.clear(); cameras = cameraItems; if (clusterCameras) { mClusterManager.clearItems(); if (showCameras) { mClusterManager.addItems(cameras); } mClusterManager.cluster(); } else { addCameraMarkers(cameras); } } }); mapCameraViewModel.setMapBounds(mMap.getProjection().getVisibleRegion().latLngBounds); LatLng latLng = new LatLng(latitude, longitude); mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom)); timer = new Timer(); timer.schedule(new AlertsTimerTask(), 0, 300000); // Schedule alerts to update every 5 minutes enableMyLocation(); if (mRestAreasOverlayTask.getStatus() == AsyncTask.Status.FINISHED) { mRestAreasOverlayTask = new RestAreasOverlayTask().execute(); } else if (mRestAreasOverlayTask.getStatus() == AsyncTask.Status.PENDING) { mRestAreasOverlayTask.execute(); } } private void setUpFabMenu() { // set up layers FAB menu fabLayers = findViewById(R.id.fab); fabLayoutCameras = findViewById(R.id.fabLayoutCameras); fabLayoutClusters = findViewById(R.id.fabLayoutClusters); fabLayoutAlerts = findViewById(R.id.fabLayoutAlerts); fabLayoutRestareas = findViewById(R.id.fabLayoutRestareas); fabLabelCameras = findViewById(R.id.fabLabelCameras); fabLabelClusters = findViewById(R.id.fabLabelClusters); fabLabelAlerts = findViewById(R.id.fabLabelAlerts); fabLabelRestareas = findViewById(R.id.fabLabelRestareas); fabCameras = findViewById(R.id.fabCameras); fabClusters = findViewById(R.id.fabClusters); fabAlerts = findViewById(R.id.fabAlerts); fabRestareas = findViewById(R.id.fabRestareas); if (!showCameras) { toggleFabOff(fabCameras); } if (!clusterCameras) { toggleFabOff(fabClusters); } if (!showAlerts) { toggleFabOff(fabAlerts); } if (!showRestAreas) { toggleFabOff(fabRestareas); } fabLayers.setOnClickListener(view -> { if (!isFABOpen) { showFABMenu(); } else { closeFABMenu(); } }); fabCameras.setOnClickListener(v -> { toggleCameras(fabCameras); closeFABMenu(); }); fabClusters.setOnClickListener(v -> { toggleCluster(fabClusters); closeFABMenu(); }); fabAlerts.setOnClickListener(v -> { toggleAlerts(fabAlerts); closeFABMenu(); }); fabRestareas.setOnClickListener(v -> { toggleRestAreas(fabRestareas); closeFABMenu(); }); } // Icon Clustering helpers private void setUpClusterer() { // Initialize the manager with the context and the map. // (Activity extends context, so we can pass 'this' in the constructor.) mClusterManager = new ClusterManager<>(this, mMap); mClusterManager.setRenderer(new CameraRenderer()); mClusterManager.setOnClusterItemClickListener(this); mClusterManager.setOnClusterClickListener(this); mClusterManager.cluster(); } public boolean onMarkerClick(Marker marker) { Bundle b = new Bundle(); Intent intent; if (markers.get(marker) == null) { // Not in our markers, must be cluster icon mClusterManager.onMarkerClick(marker); } else if (markers.get(marker).equalsIgnoreCase("camera")) { MyLogger.crashlyticsLog("Traffic", "Tap", "Camera", 1); // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); mTracker.setScreenName("/Traffic Map/Cameras"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); intent = new Intent(this, CameraActivity.class); b.putInt("id", Integer.parseInt(marker.getSnippet())); b.putString("advertisingTarget", getString(R.string.traffic_ad_target)); intent.putExtras(b); TrafficMapActivity.this.startActivity(intent); } else if (markers.get(marker).equalsIgnoreCase("alert")) { MyLogger.crashlyticsLog("Traffic", "Tap", "Alert", 1); intent = new Intent(this, HighwayAlertDetailsActivity.class); b.putInt("id", Integer.valueOf(marker.getSnippet())); intent.putExtras(b); TrafficMapActivity.this.startActivity(intent); } else if (markers.get(marker).equalsIgnoreCase("restarea")) { MyLogger.crashlyticsLog("Traffic", "Tap", "Rest Area", 1); intent = new Intent(this, RestAreaActivity.class); intent.putExtra("restarea_json", marker.getSnippet()); TrafficMapActivity.this.startActivity(intent); } return true; } @Override public boolean onClusterItemClick(CameraItem cameraItem) { // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); mTracker.setScreenName("/Traffic Map/Camera"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); Bundle b = new Bundle(); Intent intent = new Intent(this, CameraActivity.class); b.putInt("id", cameraItem.getCameraId()); intent.putExtras(b); TrafficMapActivity.this.startActivity(intent); return false; } @Override public boolean onClusterClick(Cluster<CameraItem> cluster) { mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); if (isCameraGroup(cluster)) { mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); mTracker.setScreenName("/Traffic Map/Camera Group"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); Bundle b = new Bundle(); Intent intent; intent = new Intent(this, CameraListActivity.class); // Load camera ids into array for bundle. int cameraIds[] = new int[cluster.getSize()]; String cameraUrls[] = new String[cluster.getSize()]; int index = 0; for (CameraItem camera : cluster.getItems()) { cameraIds[index] = camera.getCameraId(); cameraUrls[index] = camera.getImageUrl(); index++; } b.putStringArray("cameraUrls", cameraUrls); b.putIntArray("cameraIds", cameraIds); intent.putExtras(b); TrafficMapActivity.this.startActivity(intent); } else { mTracker.setScreenName("/Traffic Map/Camera Cluster"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); LatLngBounds.Builder builder = LatLngBounds.builder(); for (ClusterItem item : cluster.getItems()) { builder.include(item.getPosition()); } // Get the LatLngBounds final LatLngBounds bounds = builder.build(); // Animate camera to the bounds try { mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100)); } catch (Exception e) { e.printStackTrace(); } } return true; } /** * Set up the App bar menu * * Loop through all menu items, checking ID for set up. * We do it this way because item indices aren't set since the menu is dynamic. * (ex. travel charts may be added to the start) * * @param menu * @return */ @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); getMenuInflater().inflate(R.menu.traffic, menu); if (bestTimesAvailable) { menu.add(0, R.id.best_times_to_travel, 0, "Best Times to Travel").setIcon(R.drawable.ic_menu_chart) .setActionView(R.layout.action_bar_notification_icon) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); final MenuItem chartMenuItem = menu.findItem(R.id.best_times_to_travel); // Since we added an action view, need to hook up the onclick ourselves. chartMenuItem.getActionView() .setOnClickListener(v -> TrafficMapActivity.this.onMenuItemSelected(0, chartMenuItem)); menu_item_refresh = 2; } else { menu_item_refresh = 1; } for (int i = 0; i < menu.size(); i++) { switch (menu.getItem(i).getItemId()) { case R.id.toggle_cameras: if (showCameras) { menu.getItem(i).setTitle("Hide Cameras"); menu.getItem(i).setIcon(R.drawable.ic_menu_traffic_cam); } else { menu.getItem(i).setTitle("Show Cameras"); menu.getItem(i).setIcon(R.drawable.ic_menu_traffic_cam_off); } break; default: break; } } return super.onPrepareOptionsMenu(menu); } @Override public boolean onMenuOpened(int featureId, Menu menu) { closeFABMenu(); return super.onMenuOpened(featureId, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); switch (item.getItemId()) { case R.id.best_times_to_travel: Intent chartsIntent = new Intent(this, TravelChartsActivity.class); chartsIntent.putExtra("title", bestTimesTitle); startActivity(chartsIntent); break; case R.id.set_favorite: AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.WSDOT_popup); final EditText textEntryView = new EditText(this); textEntryView.setInputType(InputType.TYPE_CLASS_TEXT); builder.setView(textEntryView); builder.setMessage(R.string.add_location_dialog); builder.setNegativeButton(R.string.cancel, (dialog, whichButton) -> dialog.dismiss()); builder.setPositiveButton(R.string.submit, (dialog, whichButton) -> { String value = textEntryView.getText().toString(); dialog.dismiss(); MapLocationEntity location = new MapLocationEntity(); location.setTitle(value); location.setLatitude(mMap.getProjection().getVisibleRegion().latLngBounds.getCenter().latitude); location.setLongitude(mMap.getProjection().getVisibleRegion().latLngBounds.getCenter().longitude); location.setZoom((int) mMap.getCameraPosition().zoom); favoriteMapLocationViewModel.addMapLocation(location); }); AlertDialog alertDialog = builder.create(); alertDialog.show(); return true; case R.id.refresh: refreshOverlays(item); return true; case R.id.alerts_in_area: if (mMap != null) { LatLngBounds mBounds = mMap.getProjection().getVisibleRegion().latLngBounds; Intent alertsIntent = new Intent(this, HighwayAlertListActivity.class); alertsIntent.putExtra("nelat", mBounds.northeast.latitude); alertsIntent.putExtra("nelong", mBounds.northeast.longitude); alertsIntent.putExtra("swlat", mBounds.southwest.latitude); alertsIntent.putExtra("swlong", mBounds.southwest.longitude); startActivity(alertsIntent); } return true; case R.id.express_lanes: Intent expressIntent = new Intent(this, SeattleExpressLanesActivity.class); startActivity(expressIntent); return true; case android.R.id.home: finish(); return true; case R.id.social_media: Intent socialIntent = new Intent(this, SocialMediaTabActivity.class); startActivity(socialIntent); return true; case R.id.travel_times: Intent timesIntent = new Intent(this, TravelTimesActivity.class); startActivity(timesIntent); return true; case R.id.news: Intent newsIntent = new Intent(this, NewsActivity.class); startActivity(newsIntent); return true; case R.id.goto_bellingham: mTracker.setScreenName("/Traffic Map/Go To Location/Bellingham"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(48.756302, -122.46151, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_chehalis: mTracker.setScreenName("/Traffic Map/Go To Location/Chehalis"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(46.635529, -122.937698, 11); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_everett: mTracker.setScreenName("/Traffic Map/Go To Location/Everett"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.967976, -122.197627, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_hoodcanal: mTracker.setScreenName("/Traffic Map/Go To Location/Hood Canal"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.85268, -122.628365, 13); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_mtvernon: mTracker.setScreenName("/Traffic Map/Go To Location/Mt. Vernon"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(48.420657, -122.334824, 13); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_stanwood: mTracker.setScreenName("/Traffic Map/Go To Location/Standwood"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(48.22959, -122.34581, 13); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_monroe: mTracker.setScreenName("/Traffic Map/Go To Location/Monroe"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.859476, -121.972446, 14); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_sultan: mTracker.setScreenName("/Traffic Map/Go To Location/Sultan"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.86034, -121.812286, 13); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_olympia: mTracker.setScreenName("/Traffic Map/Go To Location/Olympia"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.021461, -122.899933, 13); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_seattle: mTracker.setScreenName("/Traffic Map/Go To Location/Seattle"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.5990, -122.3350, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_spokane: mTracker.setScreenName("/Traffic Map/Go To Location/Spokane"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.658566, -117.425995, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_tacoma: mTracker.setScreenName("/Traffic Map/Go To Location/Tacoma"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.206275, -122.46254, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_vancouver: mTracker.setScreenName("/Traffic Map/Go To Location/Vancouver"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(45.639968, -122.610512, 11); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_wenatchee: mTracker.setScreenName("/Traffic Map/Go To Location/Wenatchee"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.435867, -120.309563, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_snoqualmiepass: mTracker.setScreenName("/Traffic Map/Go To Location/Snoqualmie Pass"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(47.404481, -121.4232569, 12); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_tricities: mTracker.setScreenName("/Traffic Map/Go To Location/Tri-Cities"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(46.2503607, -119.2063781, 11); UIUtils.refreshActionBarMenu(this); return true; case R.id.goto_yakima: mTracker.setScreenName("/Traffic Map/Go To Location/Yakima"); mTracker.send(new HitBuilders.ScreenViewBuilder().build()); goToLocation(46.6063273, -120.4886952, 11); UIUtils.refreshActionBarMenu(this); return true; case R.id.map_legend: AlertDialog.Builder imageDialog = new AlertDialog.Builder(this, R.style.WSDOT_popup); LayoutInflater inflater = (LayoutInflater) this.getSystemService(LAYOUT_INFLATER_SERVICE); View layout = inflater.inflate(R.layout.map_legend_layout, null); imageDialog.setView(layout); imageDialog.setPositiveButton(R.string.submit, (dialog, whichButton) -> dialog.dismiss()); imageDialog.create(); imageDialog.show(); return true; } return super.onOptionsItemSelected(item); } public class AlertsTimerTask extends TimerTask { private Runnable runnable = new Runnable() { public void run() { mapHighwayAlertViewModel.refreshAlerts(); } }; public void run() { handler.post(runnable); } } private void refreshOverlays(final MenuItem item) { // define the animation for rotation Animation animation = new RotateAnimation(360.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(1000); animation.setRepeatCount(Animation.INFINITE); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mToolbar.getMenu().getItem(menu_item_refresh).setActionView(null); mToolbar.getMenu().getItem(menu_item_refresh).setIcon(R.drawable.ic_menu_refresh); } @Override public void onAnimationRepeat(Animation animation) { } }); ImageView imageView = new ImageView(this, null, android.R.style.Widget_Material_ActionButton); imageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_menu_refresh)); imageView.setPadding(31, imageView.getPaddingTop(), 32, imageView.getPaddingBottom()); imageView.startAnimation(animation); item.setActionView(imageView); if (mMap != null) { mapCameraViewModel.refreshCameras(); mapHighwayAlertViewModel.refreshAlerts(); } } /* * Adds or removes cameras from the cluster manager. * When clustering is turned off all items are removed from the cluster manager and * markers are plotted normally. */ private void toggleCluster(FloatingActionButton fab) { // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); if (clusterCameras) { clusterCameras = false; toggleFabOff(fab); if (cameras != null) { mClusterManager.clearItems(); mClusterManager.cluster(); addCameraMarkers(cameras); } mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Cameras") .setLabel("Clustering off").build()); } else { clusterCameras = true; toggleFabOn(fab); removeCameraMarkers(); if (cameras != null && showCameras) { mClusterManager.addItems(cameras); mClusterManager.cluster(); } mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Cameras") .setLabel("Clustering on").build()); } // Save camera display preference SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("KEY_CLUSTER_CAMERAS", clusterCameras); editor.apply(); } /** * Toggle camera visibility * checks clusterCameras to see the current state of the camera markers and hide * them accordingly. * * @param fab */ private void toggleCameras(FloatingActionButton fab) { // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); if (showCameras) { if (clusterCameras) { mClusterManager.clearItems(); mClusterManager.cluster(); } else { hideCameraMarkers(); } toggleFabOff(fab); showCameras = false; mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Cameras") .setLabel("Hide Cameras").build()); } else { if (clusterCameras) { if (cameras != null) { mClusterManager.addItems(cameras); mClusterManager.cluster(); } } else { showCameraMarkers(); } toggleFabOn(fab); showCameras = true; mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Cameras") .setLabel("Show Cameras").build()); } // Save camera display preference SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("KEY_SHOW_CAMERAS", showCameras); editor.apply(); } /** * Toggles rest area markers on/off * @param fab */ private void toggleRestAreas(FloatingActionButton fab) { // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); String label; if (showRestAreas) { toggleFabOff(fab); for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("restarea")) { key.setVisible(false); } } showRestAreas = false; label = "Hide Rest Areas"; } else { toggleFabOn(fab); for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("restarea")) { key.setVisible(true); } } showRestAreas = true; label = "Show Rest Areas"; } mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Rest Areas").setLabel(label) .build()); // Save rest areas display preference SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("KEY_SHOW_REST_AREAS", showRestAreas); editor.apply(); } /** * Toggles alert markers on/off * @param fab */ private void toggleAlerts(FloatingActionButton fab) { // GA tracker mTracker = ((WsdotApplication) getApplication()).getDefaultTracker(); String label; if (showAlerts) { toggleFabOff(fab); for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("alert")) { key.setVisible(false); } } showAlerts = false; label = "Hide Alerts"; } else { toggleFabOn(fab); for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("alert")) { key.setVisible(true); } } showAlerts = true; label = "Show Alerts"; } mTracker.send(new HitBuilders.EventBuilder().setCategory("Traffic").setAction("Highway Alerts") .setLabel(label).build()); // Save rest areas display preference SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("KEY_SHOW_ALERTS", showAlerts); editor.apply(); } public void goToLocation(double latitude, double longitude, int zoomLevel) { if (mMap != null) { LatLng latLng = new LatLng(latitude, longitude); mMap.moveCamera(CameraUpdateFactory.newLatLng(latLng)); mMap.animateCamera(CameraUpdateFactory.zoomTo(zoomLevel)); } } /** * Layers FAB menu logic */ private void showFABMenu() { isFABOpen = true; fabLayoutCameras.setVisibility(View.VISIBLE); fabLayoutClusters.setVisibility(View.VISIBLE); fabLayoutAlerts.setVisibility(View.VISIBLE); fabLayoutRestareas.setVisibility(View.VISIBLE); if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { fabLabelCameras.setPivotX(fabLabelCameras.getWidth()); fabLabelCameras.setPivotY(fabLabelCameras.getHeight()); fabLabelCameras.setRotation(40); fabLabelClusters.setPivotX(fabLabelClusters.getWidth()); fabLabelClusters.setPivotY(fabLabelClusters.getHeight()); fabLabelClusters.setRotation(40); fabLabelAlerts.setPivotX(fabLabelAlerts.getWidth()); fabLabelAlerts.setPivotY(fabLabelAlerts.getHeight()); fabLabelAlerts.setRotation(40); fabLabelRestareas.setPivotX(fabLabelRestareas.getWidth()); fabLabelRestareas.setPivotY(fabLabelRestareas.getHeight()); fabLabelRestareas.setRotation(40); fabLabelCameras.animate().translationY(-fabCameras.getHeight() / 2).setDuration(0); fabLabelClusters.animate().translationY(-fabClusters.getHeight() / 2).setDuration(0); fabLabelAlerts.animate().translationY(-fabAlerts.getHeight() / 2).setDuration(0); fabLabelRestareas.animate().translationY(-fabRestareas.getHeight() / 2).setDuration(0); fabLayoutCameras.animate().translationX(-getResources().getDimension(R.dimen.fab_1)).setDuration(270); fabLayoutClusters.animate().translationX(-getResources().getDimension(R.dimen.fab_2)).setDuration(270); fabLayoutAlerts.animate().translationX(-getResources().getDimension(R.dimen.fab_3)).setDuration(270); fabLayoutRestareas.animate().translationX(-getResources().getDimension(R.dimen.fab_4)).setDuration(270); } else { fabLayoutCameras.animate().translationY(-getResources().getDimension(R.dimen.fab_1)).setDuration(270); fabLayoutClusters.animate().translationY(-getResources().getDimension(R.dimen.fab_2)).setDuration(270); fabLayoutAlerts.animate().translationY(-getResources().getDimension(R.dimen.fab_3)).setDuration(270); fabLayoutRestareas.animate().translationY(-getResources().getDimension(R.dimen.fab_4)).setDuration(270); } } private void closeFABMenu() { if (isFABOpen) { isFABOpen = false; if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { fabLayoutCameras.animate().translationX(0); fabLayoutClusters.animate().translationX(0); fabLayoutAlerts.animate().translationX(0); fabLayoutRestareas.animate().translationX(0).setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { fabLayoutCameras.setVisibility(View.GONE); fabLayoutClusters.setVisibility(View.GONE); fabLayoutAlerts.setVisibility(View.GONE); fabLayoutRestareas.setVisibility(View.GONE); fabLayoutRestareas.animate().setListener(null); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); } else { fabLayoutCameras.animate().translationY(0); fabLayoutClusters.animate().translationY(0); fabLayoutAlerts.animate().translationY(0); fabLayoutRestareas.animate().translationY(0).setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { fabLayoutCameras.setVisibility(View.GONE); fabLayoutClusters.setVisibility(View.GONE); fabLayoutAlerts.setVisibility(View.GONE); fabLayoutRestareas.setVisibility(View.GONE); fabLayoutRestareas.animate().setListener(null); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); } } } private void toggleFabOn(FloatingActionButton fab) { TypedArray ta = this.getTheme().obtainStyledAttributes(R.styleable.ThemeStyles); fab.setBackgroundTintList(ColorStateList.valueOf(ta.getColor(R.styleable.ThemeStyles_fabButtonColor, getResources().getColor(R.color.primary_default)))); fab.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_on)); } private void toggleFabOff(FloatingActionButton fab) { fab.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.semi_white))); fab.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_off)); } /** * Build and draw rest areas on the map */ class RestAreasOverlayTask extends AsyncTask<Void, Void, Void> { @Override public void onPreExecute() { restAreasOverlay = null; } @Override public Void doInBackground(Void... unused) { restAreasOverlay = new RestAreasOverlay(getResources().openRawResource(R.raw.restareas)); return null; } @Override public void onPostExecute(Void result) { super.onPostExecute(result); Iterator<Entry<Marker, String>> iter = markers.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<Marker, String> entry = iter.next(); if (entry.getValue().equalsIgnoreCase("restArea")) { entry.getKey().remove(); iter.remove(); } } restAreas.clear(); restAreas = restAreasOverlay.getRestAreaItems(); try { for (int i = 0; i < restAreas.size(); i++) { LatLng latLng = new LatLng(restAreas.get(i).getLatitude(), restAreas.get(i).getLongitude()); Marker marker = mMap .addMarker(new MarkerOptions().position(latLng).title(restAreas.get(i).getLocation()) // Save the whole rest area object as the snippet .snippet(new Gson().toJson(restAreas.get(i))) .icon(BitmapDescriptorFactory.fromResource(restAreas.get(i).getIcon())) .visible(showRestAreas)); markers.put(marker, "restArea"); } } catch (NullPointerException e) { Crashlytics.logException(e); // Ignore for now. Simply don't draw the marker. } } } /** * Checks for posted travel charts. */ private class TravelChartsAvailableTask extends AsyncTask<Void, Void, Boolean> { @Override protected Boolean doInBackground(Void... params) { try { URL url = new URL(APIEndPoints.TRAVEL_CHARTS); URLConnection urlConn = url.openConnection(); BufferedInputStream bis = new BufferedInputStream(urlConn.getInputStream()); GZIPInputStream gzin = new GZIPInputStream(bis); InputStreamReader is = new InputStreamReader(gzin); BufferedReader in = new BufferedReader(is); String jsonFile = ""; String line; while ((line = in.readLine()) != null) jsonFile += line; in.close(); JSONObject obj = new JSONObject(jsonFile); TrafficMapActivity.this.bestTimesTitle = obj.getString("name"); return obj.getBoolean("available"); } catch (Exception e) { Crashlytics.logException(e); Log.e(TAG, "Error parsing travel chart JSON feed", e); return false; } } @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); TrafficMapActivity.this.bestTimesAvailable = result; TrafficMapActivity.this.invalidateOptionsMenu(); } } private LocationRequest createLocationRequest() { LocationRequest mLocationRequest = LocationRequest.create(); mLocationRequest.setInterval(10000); mLocationRequest.setFastestInterval(8000); mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); return mLocationRequest; } private void enableMyLocation() { if (checkPermission(true)) { if (mMap != null) { mMap.setMyLocationEnabled(true); } } } /** * Request location updates after checking permissions first. */ private void requestLocationUpdates() { Log.e(TAG, "requesting location updates"); if (checkPermission(false)) { if (mGoogleApiClient.isConnected()) { getFusedLocationProviderClient(this).requestLocationUpdates(createLocationRequest(), mLocationCallback, Looper.myLooper()); } } } public boolean onMyLocationButtonClick() { if (checkPermission(true)) { // Get last known recent location using new Google Play Services SDK (v11+) FusedLocationProviderClient locationClient = getFusedLocationProviderClient(this); locationClient.getLastLocation().addOnSuccessListener(location -> { requestLocationUpdates(); if (location != null) { moveToNewLocation(location); } }).addOnFailureListener(e -> { Log.d(TAG, "Error trying to get last GPS location"); e.printStackTrace(); }); } return true; } private boolean checkPermission(Boolean requestIfNeeded) { if (ContextCompat.checkSelfPermission(TrafficMapActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (requestIfNeeded) { requestPermissions(); } } else { return true; } return false; } private void requestPermissions() { LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() .addLocationRequest(createLocationRequest()); SettingsClient client = LocationServices.getSettingsClient(this); Task<LocationSettingsResponse> task = client.checkLocationSettings(builder.build()); task.addOnSuccessListener(this, locationSettingsResponse -> { ActivityCompat.requestPermissions(TrafficMapActivity.this, new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_ACCESS_FINE_LOCATION); }); task.addOnFailureListener(this, e -> { if (e instanceof ResolvableApiException) { // Location settings are not satisfied, but this can be fixed // by showing the user a dialog. if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { try { // Show the dialog by calling startResolutionForResult(), // and check the result in onActivityResult(). ResolvableApiException resolvable = (ResolvableApiException) e; resolvable.startResolutionForResult(TrafficMapActivity.this, REQUEST_CHECK_SETTINGS); } catch (IntentSender.SendIntentException sendEx) { // Ignore the error. } } } }); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case REQUEST_ACCESS_FINE_LOCATION: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { enableMyLocation(); requestLocationUpdates(); } } } } public void onNewLocation(Location location) { // check users speed SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); if (location.getSpeed() > 9 && !settings.getBoolean("KEY_SEEN_DRIVER_ALERT", false)) { SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("KEY_SEEN_DRIVER_ALERT", true); editor.apply(); AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.WSDOT_popup); builder.setCancelable(false); builder.setTitle("You're moving fast."); builder.setMessage("Please do not use the app while driving."); builder.setPositiveButton("I'm a Passenger", (dialog, id) -> { }); builder.create().show(); } } /** * * @param location - The new location returned from location updates */ private void moveToNewLocation(Location location) { Log.d(TAG, location.toString()); double currentLatitude = location.getLatitude(); double currentLongitude = location.getLongitude(); LatLng latLng = new LatLng(currentLatitude, currentLongitude); CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 12); mMap.animateCamera(cameraUpdate); } public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { if (connectionResult.hasResolution()) { try { // Start an Activity that tries to resolve the error connectionResult.startResolutionForResult(this, CONNECTION_FAILURE_RESOLUTION_REQUEST); } catch (IntentSender.SendIntentException e) { e.printStackTrace(); } } else { Log.i(TAG, "Location services connection failed with code " + connectionResult.getErrorCode()); } } public void onConnected(Bundle bundle) { Log.i(TAG, "Location services connected."); } public void onConnectionSuspended(int i) { Log.i(TAG, "Location services suspended. Please reconnect."); } /** * Based on custom renderer demo: * https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/clustering/view/DefaultClusterRenderer.java */ private class CameraRenderer extends DefaultClusterRenderer<CameraItem> { private final IconGenerator mClusterIconGenerator; private final float mDensity; private final Bitmap singleCameraIcon = BitmapFactory.decodeResource(getResources(), R.drawable.camera); private final Bitmap openCameraGroupIcon = BitmapFactory.decodeResource(getResources(), R.drawable.camera_cluster_open); private SparseArray<BitmapDescriptor> mIcons = new SparseArray<>(); private CameraRenderer() { super(getApplicationContext(), mMap, mClusterManager); Context context = getApplicationContext(); mDensity = context.getResources().getDisplayMetrics().density; mClusterIconGenerator = new IconGenerator(context); mClusterIconGenerator.setContentView(makeSquareTextView(context)); mClusterIconGenerator.setTextAppearance(R.style.amu_ClusterIcon_TextAppearance); } private SquareTextView makeSquareTextView(Context context) { SquareTextView squareTextView = new SquareTextView(context); ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); squareTextView.setLayoutParams(layoutParams); squareTextView.setId(R.id.amu_text); int twelveDpi = (int) (6 * mDensity); squareTextView.setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi); return squareTextView; } private LayerDrawable makeClusterBackground(Drawable backgroundImage) { ShapeDrawable outline = new ShapeDrawable(new OvalShape()); outline.getPaint().setColor(0x80ffffff); // Transparent white. LayerDrawable background = new LayerDrawable(new Drawable[] { outline, backgroundImage }); int strokeWidth = (int) (getApplication().getResources().getDisplayMetrics().density * 2); background.setLayerInset(1, strokeWidth, strokeWidth, strokeWidth, strokeWidth); return background; } private Drawable getBackgroundImage(int bucket) { if (bucket < 11) { return ResourcesCompat.getDrawable(getResources(), R.drawable.camera_cluster_1, null); } else if (bucket < 51) { return ResourcesCompat.getDrawable(getResources(), R.drawable.camera_cluster_2, null); } else if (bucket < 101) { return ResourcesCompat.getDrawable(getResources(), R.drawable.camera_cluster_3, null); } else if (bucket < 201) { return ResourcesCompat.getDrawable(getResources(), R.drawable.camera_cluster_4, null); } else { return ResourcesCompat.getDrawable(getResources(), R.drawable.camera_cluster_5, null); } } @Override protected void onBeforeClusterItemRendered(CameraItem camera, MarkerOptions markerOptions) { // Draw a single camera markerOptions.icon(BitmapDescriptorFactory.fromBitmap(singleCameraIcon)); } @Override protected void onBeforeClusterRendered(Cluster<CameraItem> cluster, MarkerOptions markerOptions) { // Draw multiple cameras // Loop through all cameras in cluster, check lat/long, if same make group? // How do we mark this special kind of cluster? With the blue icon. // How to we capture click events and know it's one of these groups? int bucket = getBucket(cluster); if (isCameraGroup(cluster)) { markerOptions.icon(BitmapDescriptorFactory.fromBitmap(openCameraGroupIcon)); } else { BitmapDescriptor descriptor = mIcons.get(bucket); if (descriptor == null) { String countText = getClusterText(bucket); mClusterIconGenerator.setBackground(makeClusterBackground(getBackgroundImage(bucket))); Bitmap icon = mClusterIconGenerator.makeIcon(countText); descriptor = BitmapDescriptorFactory.fromBitmap(icon); mIcons.put(bucket, descriptor); } markerOptions.icon(descriptor); } } @Override protected boolean shouldRenderAsCluster(Cluster cluster) { return cluster.getSize() > 1; } } // Camera marker and clustering Helper functions /** * Checks if this cluster can be opened to a list of cameras * Arbitrarily assumes cameras in same space will have no more than 20 images. This also helps performance. * * @param cluster * @return */ private boolean isCameraGroup(Cluster<CameraItem> cluster) { if (cluster.getSize() < 20) { CameraItem firstCamera = (CameraItem) cluster.getItems().toArray()[0]; for (CameraItem camera : cluster.getItems()) { if (!firstCamera.getLatitude().equals(camera.getLatitude()) || !firstCamera.getLongitude().equals(camera.getLongitude())) { return false; } } return true; } else { return false; } } /** * sets all camera marker visibility to false. * NOTE: Doesn't work for clusters */ private void hideCameraMarkers() { for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("camera")) { key.setVisible(false); } } } /** * sets all camera marker visibility to true. * NOTE: Doesn't work for clusters */ private void showCameraMarkers() { for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("camera")) { key.setVisible(true); } } } /** * Helper * Adds camera markers from the map. * NOTE: Doesn't work for clusters */ private void addCameraMarkers(List<CameraItem> cameras) { if (cameras.size() != 0) { for (int i = 0; i < cameras.size(); i++) { LatLng latLng = new LatLng(cameras.get(i).getLatitude(), cameras.get(i).getLongitude()); Marker marker = mMap.addMarker(new MarkerOptions().position(latLng).title(cameras.get(i).getTitle()) .snippet(cameras.get(i).getCameraId().toString()) .icon(BitmapDescriptorFactory.fromResource(cameras.get(i).getCameraIcon())) .visible(showCameras)); markers.put(marker, "camera"); } } } /** * Removes camera markers from the map. * Uses for switching clustering on/off. * NOTE: Doesn't work for clusters */ private void removeCameraMarkers() { for (Entry<Marker, String> entry : markers.entrySet()) { Marker key = entry.getKey(); String value = entry.getValue(); if (value.equalsIgnoreCase("camera")) { key.remove(); } } } protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("read_extras", extrasRead); } }