/*
Montréal Just in Case
Copyright (C) 2011 Mudar Noufal <mn@mudar.ca>
Geographic locations of public safety services. A Montréal Open Data
project.
This file is part of Montréal Just in Case.
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 ca.mudar.mtlaucasou.util;
import android.content.Context;
import android.location.Location;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
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.model.BitmapDescriptor;
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.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import java.util.ArrayList;
import java.util.List;
import ca.mudar.mtlaucasou.Const;
import ca.mudar.mtlaucasou.Const.LayerTypes;
import ca.mudar.mtlaucasou.Const.MapTypes;
import ca.mudar.mtlaucasou.R;
import ca.mudar.mtlaucasou.model.LayerType;
import ca.mudar.mtlaucasou.model.MapType;
import ca.mudar.mtlaucasou.model.Placemark;
import static ca.mudar.mtlaucasou.util.LogUtils.makeLogTag;
public class MapUtils {
private static final String TAG = makeLogTag("MapUtils");
private static final int MY_LOCATION_ANIM_DURATION = 300;
public static void enableMyLocation(AppCompatActivity activity, GoogleMap map) {
if (map != null && PermissionUtils.checkLocationPermission(activity)) {
map.setMyLocationEnabled(true);
}
}
/**
* Get the map marker icon (round buttons)
*
* @param type Selected map type {fire_halls|spvm_stations|water_supplies|emergency_hostels|health}
* @return bitmap for MarkerOptions
*/
public static BitmapDescriptor getMarkerIcon(@MapType String type) {
switch (type) {
case MapTypes.FIRE_HALLS:
return BitmapDescriptorFactory.fromResource(R.drawable.ic_maps_fire_halls);
case MapTypes.SPVM_STATIONS:
return BitmapDescriptorFactory.fromResource(R.drawable.ic_maps_spvm);
case MapTypes.HEAT_WAVE:
return BitmapDescriptorFactory.fromResource(R.drawable.ic_maps_water_supplies);
case MapTypes.EMERGENCY_HOSTELS:
return BitmapDescriptorFactory.fromResource(R.drawable.ic_maps_emergency_hostels);
case MapTypes.HEALTH:
return BitmapDescriptorFactory.fromResource(R.drawable.ic_maps_hospitals);
}
return null;
}
/**
* Get the app's colors for each section. Used for ProgressBar
*
* @param context Context to resolve resources
* @param type Selected map type {fire_halls|spvm_stations|water_supplies|emergency_hostels|health}
* @return the section's color
*/
@ColorInt
public static int getMapTypeColor(Context context, @MapType String type) {
@ColorRes int color;
switch (type) {
case MapTypes.FIRE_HALLS:
color = R.color.color_fire_halls;
break;
case MapTypes.SPVM_STATIONS:
color = R.color.color_spvm;
break;
case MapTypes.HEAT_WAVE:
color = R.color.color_heat_wave;
break;
case MapTypes.EMERGENCY_HOSTELS:
color = R.color.color_emergency_hostels;
break;
case MapTypes.HEALTH:
color = R.color.color_health;
break;
default:
color = R.color.color_primary;
}
return ContextCompat.getColor(context, color);
}
@LayerType
public static String getFilterItemLayerType(@IdRes int id) {
if (id == R.id.fab_air_conditioning) {
return LayerTypes.AIR_CONDITIONING;
} else if (id == R.id.fab_pools) {
return LayerTypes.POOLS;
} else if (id == R.id.fab_wading_pools) {
return LayerTypes.WADING_POOLS;
} else if (id == R.id.fab_play_fountains) {
return LayerTypes.PLAY_FOUNTAINS;
} else if (id == R.id.fab_hospitals) {
return LayerTypes.HOSPITALS;
} else if (id == R.id.fab_clsc) {
return LayerTypes.CLSC;
}
return null;
}
public static boolean isMultiLayerMapType(@MapType String mapType) {
return MapTypes.HEAT_WAVE.equals(mapType) || MapTypes.HEALTH.equals(mapType);
}
/**
* Clean the HTML description provided by the city's data. Also removes duplicate title.
*
* @param descHtml placemark properties description
* @param name the placemark, to remove it from the description
* @return Displayable description
*/
public static String getCleanDescription(@NonNull String descHtml, @NonNull String name) {
if (TextUtils.isEmpty(descHtml)) {
return null;
}
return CompatUtils.fromHtml(descHtml.replace(name, ""))
.toString()
.trim();
}
/**
* Add the Realm Placemarks to the map
*
* @param map the Map object
* @param placemarks list of Placemarks
* @return List of markers added to the map
*/
public static List<Marker> addPlacemarksToMap(GoogleMap map, List<? extends Placemark> placemarks) {
final long startTime = System.currentTimeMillis();
if (map == null || placemarks == null) {
return null;
}
final List<MarkerOptions> markerOptionsList = new ArrayList<>();
for (Placemark placemark : placemarks) {
final LatLng position = placemark.getLatLng();
final String title = placemark.getName();
final String desc = MapUtils.getCleanDescription(placemark.getDescription(), title);
if (position != null && !TextUtils.isEmpty(title)) {
final MarkerOptions markerOptions = new MarkerOptions()
.position(position)
.icon(MapUtils.getMarkerIcon(placemark.getMapType()))
.title(title);
if (!TextUtils.isEmpty(desc)) {
markerOptions.snippet(desc);
}
markerOptionsList.add(markerOptions);
}
}
// Add markers once all are ready
final List<Marker> results = new ArrayList<>();
for (MarkerOptions markerOptions : markerOptionsList) {
results.add(map.addMarker(markerOptions));
}
Log.v(TAG, String.format("Added %1$d markers. Duration: %2$dms",
markerOptionsList.size(),
System.currentTimeMillis() - startTime));
return results;
}
/**
* Get the marker nearest to map center
*
* @param map The GoogleMap
* @param markers List of markers contained by the map
* @param snackbarView Anchor view used to show the snackbar message
*/
public static void findAndShowNearestMarker(final GoogleMap map, List<Marker> markers, @Nullable View snackbarView) {
if (map == null || markers == null) {
return;
}
final LatLng mapCenter = map.getCameraPosition().target;
final LatLngBounds visibleRegion = map.getProjection().getVisibleRegion().latLngBounds;
// Find the nearest marker
boolean hasVisibleMarker = false;
Marker nearestMarker = null;
float shortestDistance = 0f; // Compute distance of each marker once only
for (Marker marker : markers) {
final float distance = GeoUtils.distanceBetween(marker.getPosition(), mapCenter);
if ((nearestMarker == null) || (Float.compare(distance, shortestDistance) < 0)) {
nearestMarker = marker;
shortestDistance = distance;
}
if (!hasVisibleMarker) {
hasVisibleMarker = visibleRegion.contains(marker.getPosition());
}
}
assert nearestMarker != null;
if (hasVisibleMarker) {
// Show the infoWindow of the nearest maker
nearestMarker.showInfoWindow();
} else if (snackbarView != null) {
// Check if the map shows any markers, Otherwise, show a zoom-out message to show the nearest
final LatLngBounds newVisibleRegion = visibleRegion.including(nearestMarker.getPosition());
Snackbar.make(snackbarView, R.string.snackbar_empty_map_visible_region, Snackbar.LENGTH_LONG)
.setAction(R.string.btn_zoom_out, new View.OnClickListener() {
@Override
public void onClick(View view) {
final int padding = view.getResources()
.getDimensionPixelSize(R.dimen.map_new_bounds_padding);
final CameraUpdate camera = CameraUpdateFactory
.newLatLngBounds(newVisibleRegion, padding);
map.animateCamera(camera);
}
})
.show();
}
}
/**
* Get Montreal's LatLngBounds, to limit the mapview
*
* @return Bounds to limits the camera movement
*/
public static LatLngBounds getDefaultBounds() {
return new LatLngBounds(
new LatLng(Const.MAPS_GEOCODER_LIMITS[0], Const.MAPS_GEOCODER_LIMITS[1]), // LowerLeft
new LatLng(Const.MAPS_GEOCODER_LIMITS[2], Const.MAPS_GEOCODER_LIMITS[3]) // UpperRight
);
}
/**
* When first showing the map, center the camera (wide zoom) on Montreal coordinates
* and setup the bounding box.
*
* @param map The GoogleMap
*/
public static void setupMapLocation(GoogleMap map) {
map.moveCamera(CameraUpdateFactory.newCameraPosition(
new CameraPosition.Builder()
.target(Const.MONTREAL_GEO_LAT_LNG)
.bearing(Const.MONTREAL_NATURAL_NORTH_ROTATION)
.zoom(Const.ZOOM_OUT)
.build()
)
);
map.setLatLngBoundsForCameraTarget(MapUtils.getDefaultBounds());
}
/**
* Move the camera to the user's location when found, if user hasn't touched the map yet.
* Default zoom, with an animated camera.
*
* @param map The GoogleMap
* @param location The location found for the user
*/
public static void moveCameraToMyLocation(GoogleMap map, Location location) {
map.animateCamera(CameraUpdateFactory.newCameraPosition(
new CameraPosition.Builder()
.target(GeoUtils.locationToLatLng(location))
.bearing(Const.MONTREAL_NATURAL_NORTH_ROTATION)
.zoom(Const.ZOOM_DEFAULT)
.build()),
MY_LOCATION_ANIM_DURATION, // short duration
null // ignore callback
);
}
/**
* Move the camera to a Location obtained from the GeoLocator for a user search query,
* or to user's location when tapping mMyLocationFAB. Near zoom.
* Also adds a default Marker (pin) at the requested location.
*
* @param map The GoogleMap
* @param location The selection location to show at center
* @param animate With/out animation
*/
public static void moveCameraToLocation(GoogleMap map, Location location, boolean animate) {
if (map == null || location == null) {
return;
}
final LatLng position = GeoUtils.locationToLatLng(location);
if (location.getExtras() != null) {
// Add a marker only if the location has a title
final MarkerOptions markerOptions = new MarkerOptions()
.title(location.getExtras().getString(Const.BundleKeys.NAME))
.position(position);
map.addMarker(markerOptions);
}
moveCameraToTarget(map, position, animate, null);
}
/**
* Move the camera to a Placemark. Mainly for SuggestionsPlacemarks selected by the user
* from search auto-complete
*
* @param map The GoogleMap
* @param placemark The placemark to show at center
* @param animate With/out animation
* @param cameraIdleListener Listener to be called after moving the camera,
* useful to switch tabs AFTER the anim (to avoid hiccups)
*/
public static void moveCameraToPlacemark(GoogleMap map, final Placemark placemark, boolean animate, final GoogleMap.OnCameraIdleListener cameraIdleListener) {
if (map == null || placemark == null) {
return;
}
moveCameraToTarget(map, placemark.getLatLng(), animate, cameraIdleListener);
}
/**
* Move camera to LatLng.
*
* @param map The GoogleMap
* @param target The coordinates of the new center
* @param animate With/out animation
* @param cameraIdleListener Listener to be called after moving the camera,
* useful to switch tabs AFTER the anim (to avoid hiccups)
*/
private static void moveCameraToTarget(@NonNull GoogleMap map, @NonNull LatLng target, boolean animate, final GoogleMap.OnCameraIdleListener cameraIdleListener) {
final CameraUpdate camera = CameraUpdateFactory.newCameraPosition(
new CameraPosition.Builder()
.target(target)
.zoom(Const.ZOOM_IN)
.bearing(map.getCameraPosition().bearing)
.build());
if (animate) {
if (cameraIdleListener != null) {
map.animateCamera(camera, new GoogleMap.CancelableCallback() {
@Override
public void onFinish() {
cameraIdleListener.onCameraIdle();
}
@Override
public void onCancel() {
cameraIdleListener.onCameraIdle();
}
});
} else {
map.animateCamera(camera);
}
} else {
map.moveCamera(camera);
if (cameraIdleListener != null) {
cameraIdleListener.onCameraIdle();
}
}
}
/**
* Clear previous markers, restoring the user's search Markers
*
* @param map The GoogleMap
*/
public static void clearMap(GoogleMap map) {
map.clear();
}
}