/* * 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.graphics.Color; import android.os.Handler; import android.os.SystemClock; import android.view.View; import com.google.android.apps.santatracker.R; import com.google.android.apps.santatracker.data.SantaPreferences; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; import com.google.maps.android.SphericalUtil; import java.lang.ref.WeakReference; import java.util.ArrayList; /** * Manages the Santa Marker on a {@link SantaMapFragment}. * */ public class SantaMarker { // private static final String TAG = "SantaMarker"; /** * Snippet used by all markers that make up a santa marker (including all * animation frame markers). */ static final String TITLE = "santa-marker"; private static final Object TOKEN = new Object(); // The santa marker private Marker[] mMovementMarkers; // The map to which this marker is attached private final SantaMapFragment mMap; // The movement thread private SantaMarkerMovementThread mMovementThread = null; // The animation thread (marker icon) private SantaMarkerAnimationThread mAnimationThread = null; private Marker[] mAnimationMarkers; // line colour private static final int mLineColour = Color.parseColor("#AA109f65"); // Santa's path private Polyline mPath = null; // 2D array: for each present type, 4 types of presents, 0=100 private static final int[][] PRESENTS = { {R.drawable.blue_100, R.drawable.blue_75, R.drawable.blue_50, R.drawable.blue_25}, {R.drawable.purple_100, R.drawable.purple_75, R.drawable.purple_50, R.drawable.purple_25}, {R.drawable.yellow_100, R.drawable.yellow_75, R.drawable.yellow_50, R.drawable.yellow_25}, {R.drawable.red_100, R.drawable.red_75, R.drawable.red_50, R.drawable.red_25}, {R.drawable.green_100, R.drawable.green_75, R.drawable.green_50, R.drawable.green_25}}; /** * Markers for santa movement */ private static final int[] MOVEMENT_MARKERS = new int[]{ R.drawable.santa_n, R.drawable.santa_ne, R.drawable.santa_e, R.drawable.santa_se, R.drawable.santa_s, R.drawable.santa_sw, R.drawable.santa_w, R.drawable.santa_nw, R.drawable.santa_n,}; // orientation of camera private double mCameraOrientation; // Santa's heading when moving private double mHeading = -1; // current movement marker private int mMovingMarker = 0; private PresentMarker[] mPresentMarkers; // State of Santa Marker - visiting or travelling private boolean mVisiting = false; // Flag to indicate whether draw presents or not. private boolean mPresentsDrawingPaused = false; /** * Santa's position. */ private LatLng mPosition = new LatLng(0, 0); SantaMarker(SantaMapFragment map) { super(); mMap = map; LatLng tempLocation = new LatLng(0, 0); // setup array of Santa animation markers and make them invisible final int[] animationIcons = new int[]{ R.drawable.marker_santa_presents1, R.drawable.marker_santa_presents2, R.drawable.marker_santa_presents3, R.drawable.marker_santa_presents4, R.drawable.marker_santa_presents5, R.drawable.marker_santa_presents6, R.drawable.marker_santa_presents7, R.drawable.marker_santa_presents8}; mAnimationMarkers = new Marker[animationIcons.length]; for (int i = 0; i < animationIcons.length; i++) { Marker m = addSantaMarker(animationIcons[i], 0.5f, 1f, tempLocation); m.setVisible(false); mAnimationMarkers[i] = m; } // Present marker View v = map.getView(); if (v != null) { mPresentMarkers = new PresentMarker[PRESENTS.length]; for (int i = 0; i < mPresentMarkers.length; i++) { mPresentMarkers[i] = new PresentMarker(map.getMap(), this, new Handler(), PRESENTS[i], v.getWidth(), v.getHeight()); } } // Movement markers mMovementMarkers = new Marker[MOVEMENT_MARKERS.length]; for (int i = 0; i < MOVEMENT_MARKERS.length; i++) { mMovementMarkers[i] = addSantaMarker(MOVEMENT_MARKERS[i], 0.5f, 0.5f, tempLocation); mMovementMarkers[i].setVisible(false); } mMovingMarker = 0; } /** * Move all Markers used for the present animation to the given position */ private void moveAnimationMarkers(LatLng position) { for (Marker m : mAnimationMarkers) { m.setPosition(position); } } /** * Adds a new marker at the given position. u, describes the anchor * position. */ private Marker addSantaMarker(int iconDrawable, float u, float v, LatLng position) { return mMap.addMarker(new MarkerOptions().position(position) .anchor(u, v) // anchor in center .title(TITLE) .icon(BitmapDescriptorFactory.fromResource(iconDrawable))); } /** * Sets the camera orientation and update the marker if moving. */ public void setCameraOrientation(float bearing) { mCameraOrientation = (bearing + 360.0f) % 360.0f; if (mMovementThread != null && mMovementThread.isMoving()) { setMovingIcon(); } } /** * Update the movement marker. */ private void setMovingIcon() { double angle = ((mHeading - mCameraOrientation + 360.0)) % 360.0; int index = ((int) (Math.round((Math.abs(angle) / 360f) * (mMovementMarkers.length - 1)))) % mMovementMarkers.length; setMovingMarker(index); // Log.d("SantaMarker", "Moving icon = camera:" + mCameraOrientation // + ", heading:" + mHeading + ",angle=" + angle + ", index="+index); } /** * Hides the previous marker, moves the new marker and makes it visible. */ private void setMovingMarker(int i) { if (mMovingMarker != i) { LatLng pos = mMovementMarkers[mMovingMarker].getPosition(); mMovementMarkers[i].setPosition(pos); mMovementMarkers[i].setVisible(true); mMovementMarkers[mMovingMarker].setVisible(false); mMovingMarker = i; } } /** * Sets the position of the current movement marker. */ private void setMovingPosition(LatLng pos) { setCachedPosition(pos); mMovementMarkers[mMovingMarker].setPosition(pos); } /** * Hides the current movement marker. */ private void hideMovingMarker() { mMovementMarkers[mMovingMarker].setVisible(false); } /** * Santa is visiting this location, display animation. */ public void setVisiting(LatLng pos) { mVisiting = true; setCachedPosition(pos); // stopAnimations(); removePath(); mAnimationThread = new SantaMarkerAnimationThread(this, mAnimationMarkers); mAnimationThread.startAnimation(pos); hideMovingMarker(); // reset heading mHeading = -1; } public boolean isVisiting() { return mVisiting; } /** * Returns the current position of this marker. */ public synchronized LatLng getPosition() { return mPosition; } /** * Saves a location as Santa's current location. This makes it available to other classes in * {@link #getPosition()}. * * @param position The location to be saved. */ private synchronized void setCachedPosition(LatLng position) { mPosition = position; } /** * Returns the destination position if the marker is moving, null otherwise. */ public LatLng getDestination() { if (mMovementThread != null) { return mMovementThread.getDestination(); } else { return null; } } /** * Animate this marker to the given position for the timestamps. */ void animateTo(LatLng originLocation, LatLng destinationLocation, long departure, long arrival) { mVisiting = false; setMovingIcon(); // create new animation runnable and post to handler mMovementThread = new SantaMarkerMovementThread(this, departure, arrival, destinationLocation, originLocation, true); mMovementThread.startAnimation(); if (mAnimationThread != null && mAnimationThread.isAlive()) { mAnimationThread.stopAnimation(); } } /** * Remove the path. */ private void removePath() { if (mPath != null) { mPath.remove(); mPath = null; } } /** * Stops all marker animations. Should be called by attached Activity in * lifecycle methods. */ void stopAnimations() { if (mMovementThread != null) { mMovementThread.stopAnimation(); } if (mAnimationThread != null) { mAnimationThread.stopAnimation(); } } /** * If this marker is currently moving, calculate its future position at the * given timestamp. If this marker is not moving, return its current * position */ public LatLng getFuturePosition(long timestamp) { if (mMovementThread != null && mMovementThread.isMoving()) { return mMovementThread.calculatePosition(timestamp); } else { return getPosition(); } } void pausePresentsDrawing() { mPresentsDrawingPaused = true; } void resumePresentsDrawing() { mPresentsDrawingPaused = false; } /** * Thread that toggles visibility of the markers, making one marker at a * time visible. * */ private static class SantaMarkerAnimationThread extends Thread { /* * The refresh rate is identical to the marker movement thread to * animate the presents */ static final int REFRESH_RATE = SantaMarkerMovementThread.REFRESH_RATE; static final int ANIMATION_DELAY = 6; // should be equivalent to a post delay of 150ms private Marker[] mToggleMarkers; private int mCurrent = 0; private int mFrame = 0; private boolean mStopThread = false; private SwapMarkersRunnable mSwapRunnable; private final LatLng TEMP_POSITION = new LatLng(0f, 0f); private final WeakReference<SantaMarker> mSantaMarkerRef; SantaMarkerAnimationThread(SantaMarker santaMarker, Marker[] markers) { super(); mToggleMarkers = markers; mSwapRunnable = new SwapMarkersRunnable(); mSantaMarkerRef = new WeakReference<>(santaMarker); } public void run() { while (!this.mStopThread) { SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { mStopThread = true; break; } if (mFrame == 0) { final int currentMarker = mCurrent; final int nextMarker = (++mCurrent % mToggleMarkers.length); mCurrent = nextMarker; mSwapRunnable.currentMarker = currentMarker; mSwapRunnable.nextMarker = nextMarker; View view = marker.mMap.getView(); if (view != null) { view.getHandler().postAtTime(mSwapRunnable, TOKEN, SystemClock.uptimeMillis()); } } mFrame = (mFrame + 1) % ANIMATION_DELAY; for (PresentMarker m : marker.mPresentMarkers) { m.draw(); } try { Thread.sleep(REFRESH_RATE); } catch (InterruptedException e) { // if interrupted, cancel this.mStopThread = true; } } } /** * Hide and move markers, need to restart thread to make visible again. */ void hideAll() { for (Marker m : mToggleMarkers) { m.setVisible(false); m.setPosition(TEMP_POSITION); } } public void cancel() { this.mStopThread = true; } /** * Start this thread. All animated markers (and the normal santa marker) * are hidden. */ void startAnimation(LatLng position) { mStopThread = false; hideAll(); SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { return; } marker.setCachedPosition(position); for (PresentMarker m : marker.mPresentMarkers) { m.reset(); } marker.moveAnimationMarkers(position); start(); } /** * Stop this thread. All animated markers are hidden and the original * santa marker is made visible. */ void stopAnimation() { // stop execution by removing all callbacks mStopThread = true; SantaMarker marker = mSantaMarkerRef.get(); if (marker != null) { View view = marker.mMap.getView(); if (view != null) { view.getHandler().removeCallbacksAndMessages(TOKEN); } } hideAll(); } class SwapMarkersRunnable implements Runnable { int currentMarker, nextMarker; public void run() { SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { return; } SantaMapFragment map = marker.mMap; if (map != null && map.getMap() != null) { mToggleMarkers[currentMarker].setVisible(false); mToggleMarkers[nextMarker].setVisible(true); float zoom = map.getMap().getCameraPosition().zoom; PresentMarker.setViewParameters(zoom, map.isInSantaCam()); } } } } /** * Animation Thread for a Santa Marker. Animates the marker between two * locations. * */ private static class SantaMarkerMovementThread extends Thread { /** * Refresh rate of this thread (it is called again every X ms.) */ static final int REFRESH_RATE = 17; private boolean mStopThread = false; private long mStart, mArrival; private double mDuration; private LatLng mDestinationLocation, mStartLocation; private boolean mIsAnimated = false; private ArrayList<LatLng> mPathPoints; private final WeakReference<SantaMarker> mSantaMarkerRef; // Threads private MovementRunnable mMovementRunnable; SantaMarkerMovementThread(SantaMarker marker, long departureTime, long arrivalTime, LatLng destinationLocation, LatLng startLocation, boolean drawPath) { mStart = departureTime; mArrival = arrivalTime; mDestinationLocation = destinationLocation; mStartLocation = startLocation; mDuration = arrivalTime - departureTime; mSantaMarkerRef = new WeakReference<>(marker); marker.removePath(); if (drawPath) { // set up path PolylineOptions line = new PolylineOptions().add(startLocation) .add(startLocation).color(mLineColour); marker.mPath = marker.mMap.getMap().addPolyline(line); marker.mPath.setGeodesic(true); mPathPoints = new ArrayList<>(2); mPathPoints.add(startLocation); // origin mPathPoints.add(startLocation); // destination - updated in loop } else { marker.mPath = null; // already removed } mMovementRunnable = new MovementRunnable(); } private void stopAnimation() { this.mStopThread = true; SantaMarker marker = mSantaMarkerRef.get(); if (marker != null) { View view = marker.mMap.getView(); if (view != null) { view.getHandler().removeCallbacksAndMessages(TOKEN); } } } void startAnimation() { this.mStopThread = false; start(); } private Runnable mSetIconRunnable = new Runnable() { public void run() { SantaMarker marker = mSantaMarkerRef.get(); if (marker != null) { marker.setMovingIcon(); } } }; private Runnable mReachedDestinationRunnable = new Runnable() { public void run() { SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { return; } marker.removePath(); // notify callback marker.mMap.onSantaReachedDestination(mDestinationLocation); } }; public void run() { while (!mStopThread) { SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { mStopThread = true; break; } // need to initialise, marker not set as animated yet final View view = marker.mMap.getView(); if (!mIsAnimated) { mIsAnimated = true; // calculate heading and update icon marker.mHeading = SphericalUtil.computeHeading(mStartLocation, mDestinationLocation); marker.mHeading = (marker.mHeading + 360f) % 360f; if (view != null) { view.getHandler().postAtTime(mSetIconRunnable, TOKEN, SystemClock.uptimeMillis()); } // Log.d(TAG, "Starting animation thread: from: " // + startLocation + " --to: " + destinationLocation // + ", start=" + start + ", dep=" + arrival // + ", duration=" + duration + ", head=" + mHeading); } double t = calculateProgress(SantaPreferences .getCurrentTime()); //SantaLog.d(TAG,"inSantaAnimateThread: t="+t+", position="+calculatePositionProgress(t)+", stopThread="+mStopThread); // Don't go backwards, but it could be negative if this thread is started too early t = Math.max(t, 0.0); // loop until finished or thread was notified to be stopped if (t < 1.0 && !mStopThread) { mMovementRunnable.position = calculatePositionProgress(t); // move marker and update path if (view != null) { view.getHandler().postAtTime(mMovementRunnable, TOKEN, SystemClock.uptimeMillis()); } if (!marker.mPresentsDrawingPaused) { for (PresentMarker p : marker.mPresentMarkers) { p.draw(); } } try { Thread.sleep(REFRESH_RATE); } catch (InterruptedException e) { this.mStopThread = true; } } else { // reached final destination,stop moving mIsAnimated = false; mStopThread = true; marker.setCachedPosition(mDestinationLocation); if (view != null) { view.getHandler().postAtTime(mReachedDestinationRunnable, TOKEN, SystemClock.uptimeMillis()); } } } } /** * Calculate the position for the given future timestamp. If the * destination is reached before this timestamp, its destination is * returned. */ LatLng calculatePosition(long timestamp) { double progress = calculateProgress(timestamp); return calculatePositionProgress(progress); } /** * Calculates the progress through the animation for the given timestamp */ private double calculateProgress(long currentTimestamp) { return (currentTimestamp - mStart) / mDuration; // linear progress } /** * Calculate the position for the given progress (start at 0, finished * at 1). */ private LatLng calculatePositionProgress(double progress) { return SphericalUtil.interpolate(mStartLocation, mDestinationLocation, progress); } private LatLng getDestination() { return mDestinationLocation; } private boolean isMoving() { return mIsAnimated; } class MovementRunnable implements Runnable { LatLng position; public void run() { SantaMarker marker = mSantaMarkerRef.get(); if (marker == null) { return; } marker.setMovingPosition(position); // update path if it is enabled if (marker.mPath != null) { mPathPoints.set(1, position); marker.mPath.setPoints(mPathPoints); } if (marker.mMap.getMap() != null) { float zoom = marker.mMap.getMap().getCameraPosition().zoom; PresentMarker.setViewParameters(zoom, marker.mMap.isInSantaCam()); } long time = SantaPreferences.getCurrentTime(); marker.mMap.onSantaIsMovingProgress(position, mArrival - time, time - mStart); } } } /** * Interface for callbacks from a {@link SantaMarker}. * */ interface SantaMarkerInterface { void onSantaReachedDestination(LatLng location); } }