/*
* Copyright (C) 2016 Google Inc. All Rights Reserved.
*
* 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.google.android.apps.santatracker.map;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.DrawableRes;
import android.support.graphics.drawable.VectorDrawableCompat;
import com.google.android.apps.santatracker.R;
import com.google.android.apps.santatracker.data.Destination;
import com.google.android.apps.santatracker.data.DestinationDbHelper;
import com.google.android.apps.santatracker.data.SantaPreferences;
import com.google.android.apps.santatracker.map.SantaMarker.SantaMarkerInterface;
import com.google.android.apps.santatracker.map.cameraAnimations.AtLocation;
import com.google.android.apps.santatracker.map.cameraAnimations.SantaCamAnimator;
import com.google.android.apps.santatracker.util.AnalyticsManager;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.CancelableCallback;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener;
import com.google.android.gms.maps.GoogleMap.OnMapClickListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
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.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.firebase.analytics.FirebaseAnalytics;
/**
* A specialised {@link MapFragment} that displays Santa's destinations and
* holds a {@link SantaMarker}. The attaching activity MUST implement {@link SantaMapInterface}.
*
*/
public class SantaMapFragment extends SupportMapFragment implements SantaMarkerInterface {
// The map
private GoogleMap mMap = null;
// Interface
private SantaMapInterface mCallback;
// visited location
private BitmapDescriptor mMarkerIconVisited;
// Identify different types of markers for info window
public static final String MARKER_PAST = "MARKER_PAST";
public static final String MARKER_NEXT = "MARKER_NEXT";
public static final String MARKER_ACTIVE = "MARKER_ACTIVE";
// Next location marker
private Marker mNextMarker = null;
// info window for marker pop-up bubbles
private DestinationInfoWindowAdapter mInfoWindowAdapter;
// Marker used for active marker
private Marker mActiveMarker = null;
private Marker mCurrentInfoMarker = null;
private Marker mPendingInfoMarker = null;
protected static final LatLng BOGUS_LOCATION = new LatLng(0f, 0f);
// Santa
private SantaMarker mSantaMarker = null;
// duration of camera animation to santa when SC is enabled
public static final int SANTACAM_MOVE_TO_SANTA_DURATION = 2000;
// duration of camera animation to user destination
public static final int SANTACAM_MOVE_TO_DEST_DURATION = 2000;
// zoom level of MOVE TO DEST destination animation
public static final float SANTACAM_MOVE_TO_DEST_ZOOM = 12.f;
// is SantaCam enabled?
private boolean mSantaCam = false;
// Manages audio playback
private TrackerSoundPlayer mTrackerSoundPlayer;
private SantaCamAnimator mSantaCamAnimator;
private Handler mHandler = new Handler();
private FirebaseAnalytics mMeasurement;
// Padding
private int mPaddingCamLeft, mPaddingCamTop, mPaddingCamRight, mPaddingCamBottom;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mMap = null;
mMeasurement = FirebaseAnalytics.getInstance(this.getContext());
}
@SuppressLint("NewApi")
@Override
public void onResume() {
super.onResume();
getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(GoogleMap map) {
setupMap(map);
mCallback.onMapInitialised();
}
});
}
@Override
public void onStop() {
super.onStop();
// Stop audio and release audio player
stopAudio();
}
public void resumeAudio() {
mTrackerSoundPlayer.resume();
mTrackerSoundPlayer.unmute();
}
public void pauseAudio() {
mTrackerSoundPlayer.pause();
mTrackerSoundPlayer.mute();
}
public void stopAudio() {
mTrackerSoundPlayer.release();
}
/**
* Add the santa marker to the map.
*/
private void addSanta() {
// create Santa marker
mSantaMarker = new SantaMarker(this);
}
/**
* Animate the santa marker to destination to arrive at its arrival time.
*/
public void setSantaTravelling(Destination origin,
Destination destination, boolean moveCameraToSanta) {
mTrackerSoundPlayer.sayHoHoHo();
mTrackerSoundPlayer.startSleighBells();
// display next destination marker
mNextMarker.setSnippet(Integer.toString(destination.id));
mNextMarker.setPosition(destination.position);
mNextMarker.setVisible(true);
if (moveCameraToSanta) {
mSantaCamAnimator.reset();
}
mSantaMarker.animateTo(origin.position, destination.position,
origin.departure, destination.arrival);
}
/**
* Sets santa as visiting the given location.
*/
public void setSantaVisiting(Destination destination, boolean playSound) {
// move santa to this location
mSantaMarker.setVisiting(destination.position);
// stop bells and play 'hohoho'
mTrackerSoundPlayer.stopSleighBells();
if (playSound) {
mTrackerSoundPlayer.sayHoHoHo();
}
// hide the next marker from this position, move it off-screen to
// prevent touch events
mNextMarker.setVisible(false);
mNextMarker.setPosition(BOGUS_LOCATION);
// if the info window for this position is open, dismiss it
if (mActiveMarker != null && mActiveMarker.isVisible()
&& mActiveMarker.getSnippet().equals(Integer.toString(destination.id))) {
hideInfoWindow();
}
}
public boolean isInSantaCam() {
return mSantaCam;
}
public void jumpToDestination(final LatLng position) {
disableSantaCam();
if (mSantaMarker != null) {
// present drawing will be resumed when SantaCam is enabled again.
mSantaMarker.pausePresentsDrawing();
}
if (mMap != null) {
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position,
SANTACAM_MOVE_TO_DEST_ZOOM),
SANTACAM_MOVE_TO_DEST_DURATION, null);
}
}
/**
* Enables the SantaCam and (if set) animates the camera to santa.
*/
public void enableSantaCam(boolean animateToSanta) {
if (mMap != null) {
mMap.setPadding(mPaddingCamLeft, mPaddingCamTop, mPaddingCamRight, mPaddingCamBottom);
}
mSantaCam = true;
mCallback.onSantacamStateChange(true);
// hide current info window
hideInfoWindow();
mSantaCamAnimator.reset();
if (animateToSanta) {
// santa is already enroute, start animation to Santa and pause animator to speed up
// camera animation
if (!mSantaMarker.isVisiting()) {
mSantaCamAnimator.pause();
LatLng futurePosition = mSantaMarker.getFuturePosition(
SantaPreferences.getCurrentTime() + SANTACAM_MOVE_TO_SANTA_DURATION);
if (futurePosition != null &&
!(futurePosition.latitude == 0 && futurePosition.longitude == 0)) {
mMap.animateCamera(CameraUpdateFactory.newLatLng(futurePosition),
SANTACAM_MOVE_TO_SANTA_DURATION, mMovingCatchupCallback);
} else {
mSantaCamAnimator.resume();
mSantaMarker.resumePresentsDrawing();
}
} else {
// Santa is at a location
onSantaReachedDestination(mSantaMarker.getPosition());
}
} else {
mSantaMarker.resumePresentsDrawing();
}
}
public void disableSantaCam() {
if (mSantaCam) {
mSantaCam = false;
mCallback.onSantacamStateChange(false);
mSantaCamAnimator.cancel();
}
}
/**
* Called when Santa has reached the given destination.
*/
public void onSantaReachedDestination(final LatLng destination) {
// hide the next marker from this position
mNextMarker.setVisible(false);
// Santa has reached destination - update camera
// center on Santa's current position at lower zoom level
if (mSantaCam) {
// Post camera update through Handler to allow for subsequent camera animation in
// CancellableCallback
mHandler.post(mReachedDestinationRunnable);
}
mSantaCamAnimator.reset();
}
/**
* Santa is currently moving, called with a progress update. If in SantaCam,
* the camera is repositioned to capture santa.
*/
public void onSantaIsMovingProgress(LatLng position, long remainingTime,
long elapsedTime) {
if (mSantaCam && mMap != null && mSantaMarker != null && position != null
&& mSantaMarker.getPosition() != null) {
// use animator to update camera if in santa cam mode
mSantaCamAnimator.animate(position, remainingTime, elapsedTime);
}
}
/*
* On map click - disable info window if it is displayed, otherwise disable
* santa cam or do nothing
*/
private OnMapClickListener mMapClickListener = new OnMapClickListener() {
public void onMapClick(LatLng arg0) {
if (mCallback == null) {
// This can happen on orientation change
return;
}
if (mCurrentInfoMarker != null) {
// info window is displayed, hide it
restoreClickedMarker();
mCallback.onClearDestination();
} else {
mCallback.mapClickAction();
}
}
};
private OnCameraChangeListener mCameraChangeListener = new OnCameraChangeListener() {
private float mPreviousBearing = Float.MIN_VALUE;
public void onCameraChange(CameraPosition camera) {
// Notify santa marker if new bearing
if (mPreviousBearing != camera.bearing) {
mSantaMarker.setCameraOrientation(camera.bearing);
mPreviousBearing = camera.bearing;
}
}
};
/**
* Marker click listener. Handles clicks on markers. When a destination
* marker is clicked, the active marker is set to this position and the
* corresponding info window is displayed. If santa cam is enabled, it is
* disabled.
*/
private OnMarkerClickListener mMarkerClickListener = new OnMarkerClickListener() {
public boolean onMarkerClick(Marker marker) {
// unsupported marker
if (marker.getTitle() == null) {
return false;
}
// Santa Marker
else if (marker.getTitle().equals(SantaMarker.TITLE)) {
mTrackerSoundPlayer.sayHoHoHo();
hideInfoWindow();
// App Measurement
MeasurementManager.recordCustomEvent(mMeasurement,
getString(R.string.analytics_event_category_tracker),
getString(R.string.analytics_tracker_action_clicksanta), null);
// [ANALYTICS EVENT]: SantaClicked
AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
R.string.analytics_tracker_action_clicksanta);
// spin camera around if santa is not moving in SC
// or at any other time if not in SC
if (!mSantaCam || mSantaMarker.isVisiting()) {
// spin camera around to opposite side
CameraPosition oldCamera = mMap.getCameraPosition();
float bearing = oldCamera.bearing;
// calculate bearing, +1 so that the camera always moves in
// the
// same direction
bearing = (bearing + 181f) % 360f;
CameraPosition camera = CameraPosition.builder(oldCamera)
.bearing(bearing).build();
mMap.animateCamera(
CameraUpdateFactory.newCameraPosition(camera),
SANTACAM_MOVE_TO_SANTA_DURATION,
new CancelableCallback() {
public void onFinish() {
// animate another 181 degrees around
CameraPosition oldCamera = mMap
.getCameraPosition();
float bearing = oldCamera.bearing;
bearing = (bearing + 181f) % 360f;
CameraPosition camera = CameraPosition
.builder(mMap.getCameraPosition())
.bearing(bearing).build();
mMap.animateCamera(CameraUpdateFactory
.newCameraPosition(camera),
SANTACAM_MOVE_TO_SANTA_DURATION, null);
}
public void onCancel() {
}
});
}
return true;
// Present Marker
} else if (marker.getTitle().equals(PresentMarker.MARKER_TITLE)) {
return true;
// Pin marker (location)
} else if (marker.getTitle().equals(MARKER_NEXT)
|| marker.getTitle().equals(MARKER_PAST)) {
showInfoWindow(marker);
return true;
// Active marker
} else if (marker.getTitle().equals(MARKER_ACTIVE)) {
hideInfoWindow();
return true;
} else {
return false;
}
}
};
/**
* Activity is attaching to this fragment, ensure it is implementing
* {@link SantaMapInterface}.
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mTrackerSoundPlayer = new TrackerSoundPlayer(activity);
// ensure that attaching activity implements the required interface
try {
mCallback = (SantaMapInterface) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement SantaMapInterface");
}
}
@Override
public void onDetach() {
super.onDetach();
mCallback = null;
mTrackerSoundPlayer.release();
}
@Override
public void onPause() {
super.onPause();
if (this.mMap != null) {
this.mMap.clear();
}
// reset map to trigger new setup
this.mMap = null;
// stop santa's animation thread
if (mSantaMarker != null) {
mSantaMarker.stopAnimations();
}
if (this.mSantaCamAnimator != null) {
this.mSantaCamAnimator.cancel();
}
pauseAudio();
}
/**
* Sets up the map and member variables. This method should be called once
* the map has been initialised.
*/
private void setupMap(GoogleMap map) {
mMap = map;
mInfoWindowAdapter = new DestinationInfoWindowAdapter(getLayoutInflater(null),
getActivity().getApplicationContext());
// clear map in case it was restored
mMap.clear();
// Set map theme
mMap.setMapStyle(MapStyleOptions.loadRawResourceStyle(getActivity(),
R.raw.map_style));
// setup map UI - disable zoom controls
UiSettings ui = this.mMap.getUiSettings();
ui.setZoomControlsEnabled(false);
ui.setCompassEnabled(false);
mMap.setInfoWindowAdapter(mInfoWindowAdapter);
mMap.setOnInfoWindowClickListener(mInfoWindowClickListener);
mMap.setOnMapClickListener(mMapClickListener);
mMap.setOnCameraChangeListener(mCameraChangeListener);
mMap.setOnMarkerClickListener(mMarkerClickListener);
// setup marker icons
mMarkerIconVisited = createMarker(R.drawable.marker_pin);
// add active marker
mActiveMarker = mMap.addMarker(new MarkerOptions()
.position(BOGUS_LOCATION)
.icon(createMarker(R.drawable.marker_pin_active))
.title(MARKER_ACTIVE)
.visible(false)
.snippet("0")
.anchor(0.5f, 1f));
mActiveMarker.setVisible(false); // required, visible in MarkerOptions
// does not work
// add next marker
mNextMarker = mMap.addMarker(new MarkerOptions()
.position(BOGUS_LOCATION)
.icon(createMarker(R.drawable.marker_pin))
.alpha(0.6f)
.visible(false)
.snippet("0")
.title(MARKER_NEXT)
.anchor(0.5f, 1f));
mNextMarker.setVisible(false);
addSanta();
mSantaCamAnimator = new SantaCamAnimator(mMap, mSantaMarker);
}
private BitmapDescriptor createMarker(@DrawableRes int id) {
final VectorDrawableCompat drawable =
VectorDrawableCompat.create(getResources(), id, getActivity().getTheme());
if (drawable == null) {
return null;
}
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
drawable.setBounds(0, 0, width, height);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.draw(canvas);
return BitmapDescriptorFactory.fromBitmap(bitmap);
}
public boolean isInitialised() {
return this.mMap != null;
}
public GoogleMap getMap() {
return mMap;
}
/**
* Add a marker for a previous location.
*/
public void addLocation(Destination destination) {
mMap.addMarker(new MarkerOptions().position(destination.position)
.icon(mMarkerIconVisited).anchor(0.5f, 1f).title(MARKER_PAST)
.snippet(Integer.toString(destination.id)));
}
/**
* If the active marker is set, hide its info window and restore the original
* marker.
*/
private void restoreClickedMarker() {
if (this.mCurrentInfoMarker != null) {
mActiveMarker.hideInfoWindow();
mActiveMarker.setVisible(false);
mCurrentInfoMarker.setPosition(mActiveMarker.getPosition());
mActiveMarker.setPosition(BOGUS_LOCATION);
mCurrentInfoMarker.setVisible(true);
mCurrentInfoMarker = null;
}
}
/**
* Hides the current info window if it is displayed
*/
public void hideInfoWindow() {
if (this.mCurrentInfoMarker != null) {
restoreClickedMarker();
mCallback.onClearDestination();
}
}
/**
* Display the info window for a marker. The database is queried using a DestinationTask to
* retrieve a Destination object.
*/
private void showInfoWindow(Marker marker) {
// disable santa cam mode
if (mSantaCam) {
disableSantaCam();
}
// store tapped marker as pending
mPendingInfoMarker = marker;
// hide window if it is currently displayed.
hideInfoWindow();
new DestinationTask().execute(Integer.parseInt(marker
.getSnippet()));
// App Measurement
MeasurementManager.recordCustomEvent(mMeasurement,
getString(R.string.analytics_event_category_tracker),
getString(R.string.analytics_tracker_action_location),
marker.getSnippet());
// [ANALYTICS EVENT]: LocationSelected
AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
R.string.analytics_tracker_action_location,
marker.getSnippet());
}
private void showInfoWindow(Destination destination) {
// ensure that destination data belongs to the pending info marker, ignore otherwise
if (mPendingInfoMarker != null && destination != null &&
mPendingInfoMarker.getSnippet() != null &&
destination.id == Integer.parseInt(mPendingInfoMarker.getSnippet())) {
// store selected marker
mCurrentInfoMarker = mPendingInfoMarker;
mPendingInfoMarker.setVisible(false);
updateActiveDestination(destination, mCurrentInfoMarker);
mInfoWindowAdapter.setData(destination);
mActiveMarker.showInfoWindow();
mPendingInfoMarker = null;
mCallback.onShowDestination(destination);
}
}
/**
* Adds the Marker to the map.
*/
public Marker addMarker(MarkerOptions m) {
return this.mMap.addMarker(m);
}
/**
* Sets the active marker to the given destination and makes it visible.
*/
public void updateActiveDestination(Destination destination,
Marker clickedMarker) {
mActiveMarker.setPosition(destination.position);
clickedMarker.setPosition(BOGUS_LOCATION);
mActiveMarker.setVisible(true);
mActiveMarker.setSnippet("" + destination.id);
}
/**
* Info Window Click listener. When an info window is clicked, the displayed
* info window is dismissed.
*/
private OnInfoWindowClickListener mInfoWindowClickListener = new OnInfoWindowClickListener() {
public void onInfoWindowClick(Marker arg0) {
// dismiss the info window and restore original marker
hideInfoWindow();
}
};
public void setCamPadding(int left, int top, int right, int bottom) {
mPaddingCamLeft = left;
mPaddingCamTop = top;
mPaddingCamRight = right;
mPaddingCamBottom = bottom;
getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(GoogleMap googleMap) {
googleMap.setPadding(mPaddingCamLeft, mPaddingCamTop,
mPaddingCamRight, mPaddingCamBottom);
}
});
if (mSantaCam && mSantaMarker != null) {
mSantaCamAnimator.triggerPaddingAnimation();
}
}
private CancelableCallback mMovingCatchupCallback = new CancelableCallback() {
@Override
public void onFinish() {
mSantaCamAnimator.resume();
mSantaMarker.resumePresentsDrawing();
}
@Override
public void onCancel() {
mSantaCamAnimator.resume();
}
};
private CancelableCallback mReachedAnimationCallback = new CancelableCallback() {
@Override
public void onFinish() {
mHandler.post(mMoveToSantaRunnable);
}
@Override
public void onCancel() {
// ignore
}
};
/**
* Move camera: Reached destination in santa cam mode
*/
Runnable mReachedDestinationRunnable = new Runnable() {
@Override
public void run() {
if (mMap != null) {
mMap.animateCamera(AtLocation
.GetCameraUpdate(mSantaMarker.getPosition(),
mMap.getCameraPosition().bearing),
SANTACAM_MOVE_TO_SANTA_DURATION, mReachedAnimationCallback);
}
}
};
/**
* Move camera to center on Santa
*/
public Runnable mMoveToSantaRunnable = new Runnable() {
@Override
public void run() {
if (mMap != null && mSantaMarker != null && mSantaMarker.getDestination() != null) {
mMap.animateCamera(CameraUpdateFactory.newLatLng(mSantaMarker.getPosition()),
SANTACAM_MOVE_TO_SANTA_DURATION, null);
}
}
};
/**
* AsyncTask that queries the database for a destination.
*/
private class DestinationTask extends AsyncTask<Integer, Void, Destination> {
@Override
protected Destination doInBackground(Integer... params) {
DestinationDbHelper dbHelper = DestinationDbHelper
.getInstance(getActivity().getApplicationContext());
return dbHelper.getDestination(params[0]);
}
@Override
protected void onPostExecute(Destination destination) {
showInfoWindow(destination);
}
}
/**
* Interface for callbacks from this Fragment.
*
*/
public interface SantaMapInterface {
/**
* Called when the map has been initialised and is ready to be used.
*/
void onMapInitialised();
void mapClickAction();
/**
* Called when the santacam is enabled or disabled.
*/
void onSantacamStateChange(boolean santacamEnabled);
void onShowDestination(Destination destination);
void onClearDestination();
}
}