/*
* Copyright (C) 2008-2013 The Android Open Source Project,
* Sean J. Barbeau
*
* 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.android.gpstest;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
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.maps.android.SphericalUtil;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.GnssMeasurementsEvent;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RequiresApi;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class GpsMapFragment extends SupportMapFragment
implements GpsTestListener, View.OnClickListener, LocationSource,
GoogleMap.OnCameraChangeListener, GoogleMap.OnMapClickListener,
GoogleMap.OnMapLongClickListener,
GoogleMap.OnMyLocationButtonClickListener, OnMapReadyCallback {
// Constants used to control how the camera animates to a position
public static final float CAMERA_INITIAL_ZOOM = 18.0f;
public static final float CAMERA_INITIAL_BEARING = 0.0f;
public static final float CAMERA_INITIAL_TILT = 45.0f;
public static final float CAMERA_ANCHOR_ZOOM = 19.0f;
public static final float CAMERA_MIN_TILT = 0.0f;
public static final float CAMERA_MAX_TILT = 90.0f;
public static final double TARGET_OFFSET_METERS = 150;
// Amount of time the user must not touch the map for the automatic camera movements to kick in
public static final long MOVE_MAP_INTERACTION_THRESHOLD = 5 * 1000; // milliseconds
private static final String PREFERENCE_SHOWED_DIALOG = "showed_google_map_install_dialog";
private final static String TAG = "GpsMapFragment";
Bundle mSavedInstanceState;
private GoogleMap mMap;
private LatLng mLatLng;
private OnLocationChangedListener mListener; //Used to update the map with new location
// Camera control
private long mLastMapTouchTime = 0;
private CameraPosition mlastCameraPosition;
private boolean mGotFix;
// User preferences for map rotation and tilt based on sensors
private boolean mRotate;
private boolean mTilt;
/**
* Clamps a value between the given positive min and max. If abs(value) is less than
* min, then min is returned. If abs(value) is greater than max, then max is returned.
* If abs(value) is between min and max, then abs(value) is returned.
*
* @param min minimum allowed value
* @param value value to be evaluated
* @param max maximum allowed value
* @return clamped value between the min and max
*/
private static double clamp(double min, double value, double max) {
value = Math.abs(value);
if (value >= min && value <= max) {
return value;
} else {
return (value < min ? value : max);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
if (isGoogleMapsInstalled()) {
// Save the savedInstanceState
mSavedInstanceState = savedInstanceState;
// Register for an async callback when the map is ready
getMapAsync(this);
} else {
final SharedPreferences sp = Application.getPrefs();
if (!sp.getBoolean(PREFERENCE_SHOWED_DIALOG, false)) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(getString(R.string.please_install_google_maps));
builder.setPositiveButton(getString(R.string.install),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
sp.edit().putBoolean(PREFERENCE_SHOWED_DIALOG, true).commit();
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse(
"market://details?id=com.google.android.apps.maps"));
startActivity(intent);
}
}
);
builder.setNegativeButton(getString(R.string.no_thanks),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
sp.edit().putBoolean(PREFERENCE_SHOWED_DIALOG, true).commit();
}
}
);
AlertDialog dialog = builder.create();
dialog.show();
}
}
return v;
}
@Override
public void onResume() {
SharedPreferences settings = Application.getPrefs();
if (mMap != null) {
if (mMap.getMapType() != Integer.valueOf(
settings.getString(getString(R.string.pref_key_map_type),
String.valueOf(GoogleMap.MAP_TYPE_NORMAL))
)) {
mMap.setMapType(Integer.valueOf(
settings.getString(getString(R.string.pref_key_map_type),
String.valueOf(GoogleMap.MAP_TYPE_NORMAL))
));
}
}
mRotate = settings
.getBoolean(getString(R.string.pref_key_rotate_map_with_compass), true);
mTilt = settings.getBoolean(getString(R.string.pref_key_tilt_map_with_sensors), true);
super.onResume();
}
public void onClick(View v) {
}
public void gpsStart() {
mGotFix = false;
}
public void gpsStop() {
}
public void onLocationChanged(Location loc) {
//Update real-time location on map
if (mListener != null) {
mListener.onLocationChanged(loc);
}
mLatLng = new LatLng(loc.getLatitude(), loc.getLongitude());
if (mMap != null) {
//Get bounds for detection of real-time location within bounds
LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
if (!mGotFix &&
(!bounds.contains(mLatLng) ||
mMap.getCameraPosition().zoom < (mMap.getMaxZoomLevel() / 2))) {
CameraPosition cameraPosition = new CameraPosition.Builder()
.target(mLatLng)
.zoom(CAMERA_INITIAL_ZOOM)
.bearing(CAMERA_INITIAL_BEARING)
.tilt(CAMERA_INITIAL_TILT)
.build();
mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
mGotFix = true;
}
}
public void onStatusChanged(String provider, int status, Bundle extras) {
}
public void onProviderEnabled(String provider) {
}
public void onProviderDisabled(String provider) {
}
@Deprecated
public void onGpsStatusChanged(int event, GpsStatus status) {
}
@Override
public void onGnssFirstFix(int ttffMillis) {
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSatelliteStatusChanged(GnssStatus status) {
}
@Override
public void onGnssStarted() {
}
@Override
public void onGnssStopped() {
}
@Override
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
}
@Override
public void onNmeaMessage(String message, long timestamp) {
}
@Override
public void onOrientationChanged(double orientation, double tilt) {
// For performance reasons, only proceed if this fragment is visible
if (!getUserVisibleHint()) {
return;
}
// Only proceed if map is not null
if (mMap == null) {
return;
}
/*
If we have a location fix, and we have a preference to rotate the map based on sensors,
and the user hasn't touched the map lately, then do the map camera reposition
*/
if (mLatLng != null && mRotate
&& System.currentTimeMillis() - mLastMapTouchTime
> MOVE_MAP_INTERACTION_THRESHOLD) {
if (!mTilt || Double.isNaN(tilt)) {
tilt = mlastCameraPosition != null ? mlastCameraPosition.tilt : 0;
}
float clampedTilt = (float) clamp(CAMERA_MIN_TILT, tilt, CAMERA_MAX_TILT);
double offset = TARGET_OFFSET_METERS * (clampedTilt / CAMERA_MAX_TILT);
CameraPosition cameraPosition = CameraPosition.builder().
tilt(clampedTilt).
bearing((float) orientation).
zoom((float) (CAMERA_ANCHOR_ZOOM + (tilt / CAMERA_MAX_TILT))).
target(mTilt ? SphericalUtil.computeOffset(mLatLng, offset, orientation)
: mLatLng).
build();
mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
}
/**
* Maps V2 Location updates
*/
@Override
public void activate(OnLocationChangedListener listener) {
mListener = listener;
}
/**
* Maps V2 Location updates
*/
@Override
public void deactivate() {
mListener = null;
}
/**
* Returns true if Google Maps is installed, false if it is not
*/
@SuppressWarnings("unused")
public boolean isGoogleMapsInstalled() {
try {
ApplicationInfo info = getActivity().getPackageManager()
.getApplicationInfo("com.google.android.apps.maps", 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
@Override
public void onCameraChange(CameraPosition cameraPosition) {
if (System.currentTimeMillis() - mLastMapTouchTime < MOVE_MAP_INTERACTION_THRESHOLD) {
/*
If the user recently interacted with the map (causing a camera change), extend the
touch time before automatic map movements based on sensors will kick in
*/
mLastMapTouchTime = System.currentTimeMillis();
}
mlastCameraPosition = cameraPosition;
}
@Override
public void onMapClick(LatLng latLng) {
mLastMapTouchTime = System.currentTimeMillis();
}
@Override
public void onMapLongClick(LatLng latLng) {
mLastMapTouchTime = System.currentTimeMillis();
}
@Override
public boolean onMyLocationButtonClick() {
mLastMapTouchTime = System.currentTimeMillis();
// Return false, so button still functions as normal
return false;
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
//Show the location on the map
mMap.setMyLocationEnabled(true);
//Set location source
mMap.setLocationSource(this);
// Listener for camera changes
mMap.setOnCameraChangeListener(this);
// Listener for map / My Location button clicks, to disengage map camera control
mMap.setOnMapClickListener(this);
mMap.setOnMapLongClickListener(this);
mMap.setOnMyLocationButtonClickListener(this);
GpsTestActivity.getInstance().addListener(this);
}
}