com.google.appinventor.components.runtime.GoogleMap.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.components.runtime.GoogleMap.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt

package com.google.appinventor.components.runtime;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.View;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
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.OnMapLongClickListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.*;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.OnInitializeListener;
import com.google.appinventor.components.runtime.util.YailList;
import com.google.gson.*;
import gnu.math.DFloNum;
import gnu.math.IntNum;

/* Component for displaying information on Google Map
 * This component makes use of Android MapView (v2) to location specific information.
 * App Inventor user could use this component to do things like those demo apps
 * for Google Mapview in the android sdk
 *
 * @author fuming@mit.mit (Fuming Shih)
 * @author wli17@mit.edu (Weihua Li)
 */
@DesignerComponent(version = YaVersion.GOOGLE_MAP_COMPONENT_VERSION, description = "Visible component that show information on Google map.", category = ComponentCategory.MAPVIZ)
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.WRITE_EXTERNAL_STORAGE, "
        + "android.permission.ACCESS_NETWORK_STATE, " + "android.permission.INTERNET, "
        + "android.permission.ACCESS_COARSE_LOCATION, " + "android.permission.ACCESS_FINE_LOCATION, "
        + "com.google.android.providers.gsf.permission.READ_GSERVICES, "
        + "android.permission.WRITE_EXTERNAL_STORAGE")
@UsesLibraries(libraries = "google-play-services.jar, gson-2.1.jar") // we have to include funf.jar because we use gson.JsonParser
public class GoogleMap extends AndroidViewComponent
        implements OnResumeListener, OnInitializeListener, OnPauseListener, OnMarkerClickListener,
        OnInfoWindowClickListener, OnMarkerDragListener, OnMapClickListener, OnMapLongClickListener,
        OnCameraChangeListener, ConnectionCallbacks, OnConnectionFailedListener, LocationListener {

    private final Activity context;
    private final Form form;
    private static final String TAG = "GoogleMap";
    // Layout
    // We create thie LinerLayout and add our mapFragment in it.
    // private final com.google.appinventor.components.runtime.LinearLayout viewLayout;
    // private final FrameLayout viewLayout;
    // private final android.widget.LinearLayout viewLayout;
    // private LinearLayout viewLayout;
    private android.widget.LinearLayout viewLayout;

    // translates App Inventor alignment codes to Android gravity
    // private final AlignmentUtil alignmentSetter;
    private final String MAP_FRAGMENT_TAG;
    private com.google.android.gms.maps.GoogleMap mMap;
    private SupportMapFragment mMapFragment;
    private Bundle savedInstanceState;
    private HashMap<Marker, Integer> markers = new HashMap<Marker, Integer>();

    // basic configurations of a map
    private int mapType = com.google.android.gms.maps.GoogleMap.MAP_TYPE_NORMAL;
    private boolean myLocationEnabled = false;
    private boolean compassEnabled = false;
    private boolean rotateEnabled = true;
    private boolean scrollEnabled = true;
    private boolean zoomControlEnabled = false;
    private boolean zoomGesturesEnabled = true;

    // default settings for marker
    private int mMarkerColor = Component.COLOR_BLUE;
    private boolean mMarkerDraggable = false;

    // settings for map event listener
    private boolean enableMapClickListener = false;
    private boolean enableMapLongClickListener = false;
    private boolean enableCameraChangeListener = false;

    // setting up for circle overlay

    private static final double DEFAULT_RADIUS = 1000000;
    public static final double RADIUS_OF_EARTH_METERS = 6371009;
    private static final AtomicInteger snextCircleId = new AtomicInteger(1);
    private HashMap<Object, Integer> circles = new HashMap<Object, Integer>(); //we are storing references for both circle and draggable circle
    private List<DraggableCircle> mCircles = new ArrayList<DraggableCircle>(1);

    //defaults for circle overlay
    private float mStrokeWidth = 2; // in pixel, 0 means no outline will be drawn
    private int mStrokeColor = Color.BLACK; // perimeter default color is black
    private int mColorHue = 0; // value ranges from [0, 360]
    private int mAlpha = 20; //min 0, default 127, max 255
    private int mFillColor = Color.HSVToColor(mAlpha, new float[] { mColorHue, 1, 1 });//default to red, medium level hue color

    private UiSettings mUiSettings;

    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
    private static final AtomicInteger snextMarkerId = new AtomicInteger(1);
    private final Handler androidUIHandler = new Handler();

    private YailList markersList;

    // Setting for LocationClient
    // These settings are the same as the settings for the map. They will in fact give you updates at
    // the maximal rates currently possible.
    private LocationClient mLocationClient;
    private static final LocationRequest REQUEST = LocationRequest.create().setInterval(5000) // 5 seconds
            .setFastestInterval(16) // 16ms = 60fps
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    public GoogleMap(ComponentContainer container) throws IOException {
        super(container);
        Log.i(TAG, "In the constructor of GoogleMap");
        context = container.$context();
        form = container.$form();
        savedInstanceState = form.getOnCreateBundle();
        Log.i(TAG, "savedInstanceState in GM: " + savedInstanceState);

        // try raw mapView with in the fragmment
        viewLayout = new android.widget.LinearLayout(context);
        viewLayout.setId(generateViewId());

        MAP_FRAGMENT_TAG = "map_" + System.currentTimeMillis();
        Log.i(TAG, "map_tag:" + MAP_FRAGMENT_TAG);
        //add check if the phone has installed Google Map and Google Play Service sdk

        checkGooglePlayServiceSDK();
        checkGoogleMapInstalled();

        // TODO: need to add code to check Form (activity) whether savedInstanceState ==null
        mMapFragment = (SupportMapFragment) form.getSupportFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG);

        // We only create a fragment if it doesn't already exist.
        if (mMapFragment == null) {

            Log.i(TAG, "mMapFragment is null.");
            // To programmatically add the map, we first create a SupportMapFragment.
            mMapFragment = SupportMapFragment.newInstance();

            //mMapFragment = new SomeFragment();
            FragmentTransaction fragmentTransaction = form.getSupportFragmentManager().beginTransaction();
            Log.i(TAG, "here before adding fragment");
            // try to use replace to see if we solve the issue
            fragmentTransaction.replace(viewLayout.getId(), mMapFragment, MAP_FRAGMENT_TAG);

            fragmentTransaction.commit();
        }

        setUpMapIfNeeded();

        container.$add(this);

        Width(LENGTH_FILL_PARENT);
        Height(LENGTH_FILL_PARENT);
        form.registerForOnInitialize(this);
        form.registerForOnResume(this);
        form.registerForOnResume(this);
        form.registerForOnPause(this);

    }

    /**
     * Generate a value suitable for use in .
     * This value will not collide with ID values generated at build time by aapt for R.id.
     *
     * @return a generated ID value
     */
    private static int generateViewId() {
        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF)
                newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }

    //  Currently this doesn't work
    @Override
    @SimpleProperty()
    public void Width(int width) {
        if (width == LENGTH_PREFERRED) {
            width = LENGTH_FILL_PARENT;
        }
        super.Width(width);
    }

    @Override
    @SimpleProperty()
    public void Height(int height) {
        if (height == LENGTH_PREFERRED) {
            height = LENGTH_FILL_PARENT;
        }
        super.Height(height);
    }

    private void setUpMapIfNeeded() {
        // Do a null check to confirm that we have not already instantiated the map.
        if (mMap == null) {
            // Try to obtain the map from the SupportMapFragment.
            mMap = mMapFragment.getMap();
            // Check if we were successful in obtaining the map.
            if (mMap != null) {
                Log.i(TAG, "Yes, we have a google map...");
                setUpMap();
            } else {
                // means that Google Service is not available
                form.dispatchErrorOccurredEvent(this, "setUpMapIfNeeded",
                        ErrorMessages.ERROR_GOOGLE_PLAY_NOT_INSTALLED);
            }

        }
    }

    private void setUpLocationClientIfNeeded() {
        if (mLocationClient == null) {
            mLocationClient = new LocationClient(context, this, // ConnectionCallbacks
                    this); // OnConnectionFailedListener
        }
    }

    private void setUpMap() {
        // could be the boilerplate for initiating everything
        // including all the configurations and markers

        // (testing: add an marker)
        Log.i(TAG, "in setUpMap()");
        // Set listeners for marker events.  See the bottom of this class for their behavior.
        mMap.setOnMarkerClickListener(this);
        mMap.setOnInfoWindowClickListener(this);
        mMap.setOnMarkerDragListener(this);

        //just for testing
        //    int uniqueId = generateMarkerId();
        //    Marker marker = mMap.addMarker(new MarkerOptions().position(new LatLng(0, 0)).title("Marker"));
        //    markers.put(marker, uniqueId);
        //////
        // create UiSetting instance and default ui settings of the map
        mUiSettings = mMap.getUiSettings();
        mUiSettings.setCompassEnabled(this.compassEnabled);
        mUiSettings.setRotateGesturesEnabled(this.rotateEnabled);
        mUiSettings.setScrollGesturesEnabled(this.scrollEnabled);
        mUiSettings.setZoomControlsEnabled(this.zoomControlEnabled);
        mUiSettings.setZoomGesturesEnabled(this.zoomGesturesEnabled);

        // after this method is called, user can add markers and change other settings.
        //TODO: Actions(Functions) that are called within MapIsReady() are not working
        MapIsReady();

    }

    @SimpleFunction(description = "Enables/disables the compass widget on the map's ui. Call this only after "
            + "event \"MapIsReady\" is received")
    public void EnableCompass(boolean enable) {
        this.compassEnabled = enable;
        mUiSettings.setCompassEnabled(enable);
    }

    @SimpleProperty(description = "Indicates whether the compass widget is currently enabled in the map ui")
    public boolean CompassEnabled() {
        return mUiSettings.isCompassEnabled();
    }

    @SimpleFunction(description = "Enables/disables the capability to rotate a map on the ui. Call this only after "
            + "the event \"MapIsReady\" is received.")
    public void EnableRotate(boolean enable) {
        this.rotateEnabled = enable;
        mUiSettings.setRotateGesturesEnabled(enable);
    }

    @SimpleProperty(description = "Indicates whether the capability to rotate a map on the ui is currently enabled")
    public boolean RotateEnabled() {
        return mUiSettings.isRotateGesturesEnabled();
    }

    @SimpleFunction(description = "Enables/disables the capability to scroll a map on the ui. Call this only after the "
            + "event \"MapIsReady\" is received")
    public void EnableScroll(boolean enable) {
        this.scrollEnabled = enable;
        mUiSettings.setScrollGesturesEnabled(enable);

    }

    @SimpleProperty(description = "Indicates whether the capability to scroll a map on the ui is currently enabled")
    public boolean ScrollEnabled() {
        return mUiSettings.isScrollGesturesEnabled();
    }

    @SimpleFunction(description = "Enables/disables the zoom widget on the map's ui. Call this only after the event"
            + " \"MapIsReady\" is received")
    public void EnableZoomControl(boolean enable) {
        this.zoomControlEnabled = enable;
        mUiSettings.setZoomControlsEnabled(enable);

    }

    @SimpleProperty(description = "Indicates whether the zoom widget on the map ui is currently enabled")
    public boolean ZoomControlEnabled() {
        return mUiSettings.isZoomControlsEnabled();
    }

    @SimpleFunction(description = "Enables/disables zoom gesture on the map ui. Call this only after the event "
            + " \"MapIsReady\" is received. ")
    public void EnableZoomGesture(boolean enable) {
        this.zoomGesturesEnabled = enable;
        mUiSettings.setZoomGesturesEnabled(enable);

    }

    @SimpleProperty(description = "Indicates whether the zoom gesture is currently enabled")
    public boolean ZoomGestureEnabled() {
        return mUiSettings.isZoomGesturesEnabled();
    }

    @SimpleEvent(description = "Indicates that the map has been rendered and ready for adding markers "
            + "or changing other settings. Please add or updating markers within this event")
    public void MapIsReady() {
        Log.i(TAG, "Map is ready for adding markers and other setting");
        EventDispatcher.dispatchEvent(GoogleMap.this, "MapIsReady");
    }

    //TODO: Move this to Util
    private void checkGooglePlayServiceSDK() {
        //To change body of created methods use File | Settings | File Templates.
        final int googlePlayServicesAvailable = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
        Log.i(TAG, "googlePlayServicesAvailable:" + googlePlayServicesAvailable);

        switch (googlePlayServicesAvailable) {
        case ConnectionResult.SERVICE_MISSING:
            form.dispatchErrorOccurredEvent(this, "checkGooglePlayServiceSDK",
                    ErrorMessages.ERROR_GOOGLE_PLAY_NOT_INSTALLED);
            break;
        case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
            form.dispatchErrorOccurredEvent(this, "checkGooglePlayServiceSDK",
                    ErrorMessages.ERROR_GOOGLE_PLAY_SERVICE_UPDATE_REQUIRED);
            break;
        case ConnectionResult.SERVICE_DISABLED:
            form.dispatchErrorOccurredEvent(this, "checkGooglePlayServiceSDK",
                    ErrorMessages.ERROR_GOOGLE_PLAY_DISABLED);
            break;
        case ConnectionResult.SERVICE_INVALID:
            form.dispatchErrorOccurredEvent(this, "checkGooglePlayServiceSDK",
                    ErrorMessages.ERROR_GOOGLE_PLAY_INVALID);
            break;
        }
    }

    private void checkGoogleMapInstalled() {
        try {
            ApplicationInfo info = context.getPackageManager().getApplicationInfo("com.google.android.apps.maps",
                    0);

        } catch (PackageManager.NameNotFoundException e) {
            form.dispatchErrorOccurredEvent(this, "checkGoogleMapInstalled",
                    ErrorMessages.ERROR_GOOGLE_MAP_NOT_INSTALLED);
        }
    }

    /**
     *
     * @param lat Latitude of the center of the circle
     * @param lng Longitude of the center of the circle
     * @param radius Radius of the circle
     * @param alpha Alpha value of the color of the circle overlay
     * @param hue Hue value of the color of the circle overaly
     * @param strokeWidth Width of the perimeter
     * @param strokeColor Color of the perimeter
     */
    @SimpleFunction(description = "Create a circle overlay on the map UI with specified latitude and longitude for center. "
            + "\"hue\" (min 0, max 360) and \"alpha\" (min 0, max 255) are used to set color and transparency level of the circle, "
            + "\"strokeWidth\" and \"strokeColor\" are for the perimeter of the circle. "
            + "Returning a unique id of the circle for future reference to events raised by moving this circle. If the circle is"
            + "set to be draggable, two default markers will appear on the map: one in the center of the circle, another on the perimeter.")
    public int AddCircle(double lat, double lng, double radius, int alpha, float hue, float strokeWidth,
            int strokeColor, boolean draggable) {
        int uid = generateCircleId();
        int fillColor = Color.HSVToColor(alpha, new float[] { hue, 1, 1 });

        if (draggable) {
            //create a draggableCircle
            DraggableCircle circle = new DraggableCircle(new LatLng(lat, lng), radius, strokeWidth, strokeColor,
                    fillColor);

            mCircles.add(circle);
            circles.put(circle, uid);

        } else {
            Circle plainCircle = mMap.addCircle(new CircleOptions().center(new LatLng(lat, lng)).radius(radius)
                    .strokeWidth(strokeWidth).strokeColor(strokeColor).fillColor(fillColor));
            circles.put(plainCircle, uid);
        }

        return uid;

    }

    private Object getCircleIfExisted(int circleId) {
        Object circle = getKeyByValue(circles, circleId);

        if (circle == null) {
            form.dispatchErrorOccurredEvent(this, "getCircleIfExisted",
                    ErrorMessages.ERROR_GOOGLE_MAP_CIRCLE_NOT_EXIST, Integer.toString(circleId));
            return null;
        }
        return circle;
    }

    @SimpleFunction(description = "Remove a circle for the map. Returns true if successfully removed, false if the circle "
            + "does not exist with the specified id")
    public boolean RemoveCircle(int circleId) {

        Object circle = getKeyByValue(circles, circleId);
        boolean isRemoved = false;

        if (circle == null) {
            //TODO: do we need another error Message?
            return isRemoved;
        } else {
            if (circle instanceof DraggableCircle) {// if it's a draggable circle
                ((DraggableCircle) circle).removeFromMap(); // remove all it's inner objects from the map
                mCircles.remove(circle); //need to remove it from the mCircles arraylist

            }
            if (circle instanceof Circle) { // it's a plain circle, just remove it from the hashmap and the map
                ((Circle) circle).remove();

            }
            circles.remove(circle);
            isRemoved = true;
        }

        return isRemoved;
    }

    @SimpleFunction(description = "Set the property of an existing circle. Properties include: "
            + "\"alpha\"(number, value ranging from 0~255), \"color\" (nimber, hue value ranging 0~360), "
            + "\"radius\"(number in meters)")
    public void UpdateCircle(int circleId, String propertyName, Object value) {
        Log.i(TAG, "inputs: " + circleId + "," + propertyName + ", " + value);
        float[] hsv = new float[3];
        Object circle = getCircleIfExisted(circleId); // if it's null, getCircleIfExisted will show error msg
        Circle updateCircle = null; // the real circle content that gets updated

        if (circle != null) {
            if (circle instanceof DraggableCircle) {
                updateCircle = ((DraggableCircle) circle).getCircle();

            }
            if (circle instanceof Circle) {
                updateCircle = (Circle) circle;

            }
            try {

                Float val = Float.parseFloat(value.toString());
                if (propertyName.equals("alpha")) {

                    int color = updateCircle.getFillColor();
                    Color.colorToHSV(color, hsv);
                    Integer alphaVal = val.intValue();
                    //Color.HSVToColor(mAlpha, new float[] {mColorHue, 1, 1});//default to red, medium level hue color
                    int newColor = Color.HSVToColor(alphaVal, hsv);
                    updateCircle.setFillColor(newColor);
                }

                if (propertyName.equals("color")) {
                    int alpha = Color.alpha(updateCircle.getFillColor());

                    int newColor = Color.HSVToColor(alpha, new float[] { val, 1, 1 });
                    updateCircle.setFillColor(newColor);
                }

                if (propertyName.equals("radius")) {
                    // need to cast value to float
                    Float radius = val;
                    updateCircle.setRadius(radius);
                    // if it's a draggableCircle, then we need to remove the previous marker and get new radius marker
                    if (circle instanceof DraggableCircle) {
                        // remove previous radius marker
                        Marker centerMarker = ((DraggableCircle) circle).getCenterMarker();
                        Marker oldMarker = ((DraggableCircle) circle).getRadiusMarker();
                        oldMarker.remove();
                        Marker newMarker = mMap.addMarker(new MarkerOptions()
                                .position(toRadiusLatLng(centerMarker.getPosition(), radius)).draggable(true)
                                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));

                        ((DraggableCircle) circle).setRadiusMarker(newMarker);
                        // create a new draggabble circle

                    }

                }

            } catch (NumberFormatException e) { //can't parse the string
                form.dispatchErrorOccurredEvent(this, "UpdateCircle", ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                        value.toString());
            }

        } else {
            // the circle doesn't exist
            form.dispatchErrorOccurredEvent(this, "UpdateCircle", ErrorMessages.ERROR_GOOGLE_MAP_CIRCLE_NOT_EXIST,
                    circleId);
        }
    }

    @SimpleFunction(description = "Get all circles Ids. A short cut to get all the references for the eixisting circles")
    public YailList GetAllCircleIDs() {
        return YailList.makeList(circles.values());

    }

    // this event flow comes from  Marker.onMarkerDragStart/onMarkerDragEnd/onMarkerDrag
    // --> DraggableCircle.onMarkerMove() --> FinishDraggingCircle()
    // if the user is dragging the circle radius marker, the draggable circle object with re-draw automatically
    // only when the user finishes dragging the circle will we propagate this event to the UI.

    @SimpleEvent(description = "Event been raised after the action of moving a draggable circle is finished. Possible a user "
            + "drag the center of the circle or drag the radius marker of the circle")
    public void FinishedDraggingCircle(final int id, final double centerLat, final double centerLng,
            final double radius) {
        // called by moving the marker that's the center of the circle
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "a draggable circle:" + id + "finished been dragged");
                EventDispatcher.dispatchEvent(GoogleMap.this, "FinishedDraggingCircle", id, centerLat, centerLng,
                        radius);
            }
        });

    }

    // AndroidViewComponent implementation

    @Override
    public View getView() {
        //return viewLayout.getLayoutManager();
        return viewLayout;
    }

    @Override
    public void onResume() {
        Log.i(TAG, "in onResume...Google Map redraw");
        //set up LocationClient for my location using GMS
        if (myLocationEnabled) {//only if my location is enabled
            setUpLocationClientIfNeeded();
            mLocationClient.connect();
        }
        setUpMapIfNeeded();
    }

    @Override
    public void onInitialize() {
    }

    private void prepareFragmentView() {

        mMapFragment = SupportMapFragment.newInstance();

        androidUIHandler.post(new Runnable() {
            public void run() {
                boolean dispatchEventNow = false;
                if (mMapFragment != null) {

                    dispatchEventNow = true;
                }
                if (dispatchEventNow) {

                    // Then we add it using a FragmentTransaction.
                    // add fragment to the view
                    FragmentTransaction fragmentTransaction = form.getSupportFragmentManager().beginTransaction();

                    //fragmentTransaction.add(viewLayout.getLayoutManager().getId(),
                    fragmentTransaction.add(viewLayout.getId(), mMapFragment, MAP_FRAGMENT_TAG);

                    fragmentTransaction.commit();

                    // set up map
                    setUpMapIfNeeded();
                } else {
                    // Try again later.
                    androidUIHandler.post(this);
                }
            }
        });
    }

    @SimpleFunction(description = "Enable or disable my location widget control for Google Map. One can call "
            + "GetMyLocation() to obtain the current location after enable this.\"")
    public void EnableMyLocation(boolean enabled) {
        Log.i(TAG, "@EnableMyLocation:" + enabled);
        if (this.myLocationEnabled != enabled)
            this.myLocationEnabled = enabled;

        if (mMap != null) {

            mMap.setMyLocationEnabled(enabled); // enable google map mylocation widget

            if (enabled) {
                setUpLocationClientIfNeeded();
                mLocationClient.connect();
            } else {
                mLocationClient.disconnect();
            }
        }
    }

    @SimpleProperty(description = "Indicates whether my locaiton UI control is currently enabled for the Google map.")
    public boolean MyLocationEnabled() {
        return this.myLocationEnabled;
    }

    @SimpleFunction(description = "Get current location using Google Map Service. Return a YailList with first item being"
            + "the latitude, the second item being the longitude, and last time being the accuracy of the reading.")
    public YailList GetMyLocation() {

        ArrayList<Object> latLng = new ArrayList<Object>();

        if (mLocationClient != null && mLocationClient.isConnected()) {
            Log.i(TAG, "client is connected");
            Location location = mLocationClient.getLastLocation();
            latLng.add(location.getLatitude());
            latLng.add(location.getLongitude());
            latLng.add(location.getAccuracy());
        }

        return YailList.makeList(latLng);
    }

    @SimpleFunction(description = "Set the layer of Google map. Default layer is \"normal\", other choices including \"hybrid\","
            + "\"satellite\", and \"terrain\" ")
    public void SetMapType(String layerName) {

        if (layerName.equals("normal")) {
            this.mapType = com.google.android.gms.maps.GoogleMap.MAP_TYPE_NORMAL;
        } else if (layerName.equals("hybrid")) {
            this.mapType = com.google.android.gms.maps.GoogleMap.MAP_TYPE_HYBRID;
        } else if (layerName.equals("satellite")) {
            this.mapType = com.google.android.gms.maps.GoogleMap.MAP_TYPE_SATELLITE;
        } else if (layerName.equals("terrain")) {
            this.mapType = com.google.android.gms.maps.GoogleMap.MAP_TYPE_TERRAIN;
        } else {
            Log.i(TAG, "Error setting layer with name " + layerName);
            form.dispatchErrorOccurredEvent(this, "SetMapType", ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                    layerName + " is not the correct type");
        }

        if (mMap != null) {
            mMap.setMapType(this.mapType);
        }
    }

    /**
     * Enable map click event listener for this component
     * @param enabled
     */
    @SimpleFunction(description = "Enable/Disable to listen to map's click event")
    public void EnableMapClickListener(boolean enabled) {
        Log.i(TAG, "@EnableMapClickListener:" + enabled);
        if (this.enableMapClickListener != enabled)
            this.enableMapClickListener = enabled;

        if (mMap != null) {
            Log.i(TAG, "enable map listener?: " + enabled);
            mMap.setOnMapClickListener(enabled ? this : null);
        }
    }

    /**
     * Indicates if the mapClick listener is currently enabled
     * @return
     */
    @SimpleProperty(description = "Indicates if the mapClick event listener is currently enabled")
    public boolean MapClickListenerEnabled() {
        return this.enableMapClickListener;
    }

    /**
     * Enable map long click event listener
     * @return
     */
    @SimpleFunction(description = "Enable/disable to listen to map's long click event")
    public void EnableMapLongClickListener(boolean enabled) {
        Log.i(TAG, "@EnableMapLongClickListener:" + enabled);
        if (this.enableMapLongClickListener != enabled) {
            this.enableMapLongClickListener = enabled;
        }
        if (mMap != null) {
            Log.i(TAG, "enable long click listener?:" + enabled);
            mMap.setOnMapLongClickListener(enabled ? this : null);
        }
    }

    /**
     * Indicates if the map's longClick event listener is currently enabled
     * @return
     */
    @SimpleProperty(description = "Indicates if the map longClick listener is currently enabled")
    public boolean MapLongClickListenerEnabled() {
        return this.enableMapLongClickListener;
    }

    /**
     * Enable/Disable map's camera position changed event
     * @param enabled
     */
    @SimpleFunction(description = "Enable/Disable to listen to map's camera position changed event")
    public void EnableMapCameraPosChangeListener(boolean enabled) {
        Log.i(TAG, "@EnableMapCameraPosChangeListener:" + enabled);
        if (this.enableCameraChangeListener != enabled) {
            this.enableCameraChangeListener = enabled;

        }

        if (mMap != null) {
            Log.i(TAG, "enable cameraChangedListener?:" + enabled);
            mMap.setOnCameraChangeListener(enabled ? this : null);
        }
    }

    /**
     * Indicates if the map camera's position changed listener is currently enabled
     * @return
     */
    @SimpleProperty(description = "Indicates if the map camera's position changed listener is currently enabled")
    public boolean MapCameraChangedListenerEnabled() {
        return this.enableCameraChangeListener;
    }

    @SimpleProperty(description = "Indicates the current map type")
    public String MapType() {
        switch (this.mapType) {
        case com.google.android.gms.maps.GoogleMap.MAP_TYPE_NORMAL:
            return "normal";
        case com.google.android.gms.maps.GoogleMap.MAP_TYPE_HYBRID:
            return "hybrid";
        case com.google.android.gms.maps.GoogleMap.MAP_TYPE_SATELLITE:
            return "satellite";
        case com.google.android.gms.maps.GoogleMap.MAP_TYPE_TERRAIN:
            return "terrain";
        }
        return null;
    }

    /**
     *
     * @param markers
     * @return
     * TODO: Adding customized icons, also too many error msgs (disable for now)
     */
    @SimpleFunction(description = "Adding a list of YailLists for markers. The representation of a maker in the "
            + "inner YailList is composed of: " + "lat(double) [required], long(double) [required], Color, "
            + "title(String), snippet(String), draggable(boolean). Return a list of unqiue ids for the added "
            + " markers. Note that the markers ids are not meant to persist after "
            + " the app is closed, but for temporary references to the markers within the program only. Return an empty list"
            + " if any error happen in the input")
    public YailList AddMarkers(YailList markers) {
        // For color, check out the code in Form.java$BackgroundColor() e.g. if
        // (argb != Component.COLOR_DEFAULT)
        // After the color is chosen, it's passed in as int into the method
        // We can have two ways for supporting color of map markers: 1) pass it in
        // as int in the Yailist,
        // 2) if the user omit the value for color, we will use the blue color\
        // what's a easier way for people to know about the color list?
        // App Inventor currently uses RGB (android.graphics.Color), but android map
        // marker uses HUE
        // http://developer.android.com/reference/com/google/android/gms/maps/model/BitmapDescriptorFactory.html#HUE_YELLOW
        // We can use Android.graphics.Color.colorToHSV(int rgbcolor, float[]hsv) to
        // get the hue value in the hsv array
        float[] hsv = new float[3];

        ArrayList<Integer> markerIds = new ArrayList<Integer>();
        for (Object marker : markers.toArray()) {
            boolean addOne = true;
            if (marker instanceof YailList) {
                Log.i(TAG, "interior YailLiat");
                if (((YailList) marker).size() < 2) {
                    addOne = false; // don't add this marker because its invalid inputs, going to the next one
                }
                // ((YailList) marker).getObject(0) will return type gnu.math.DFloNum
                Object latObj = ((YailList) marker).getObject(0);
                Object lngObj = ((YailList) marker).getObject(1);
                Log.i(TAG, "Type: " + latObj.getClass());
                Log.i(TAG, "Type: " + lngObj.getClass());
                Double lat = new Double(0);
                Double lng = new Double(0);

                if (!(latObj instanceof DFloNum && lngObj instanceof DFloNum)) {//if one of the lat or lng is not DFloNum
                    addOne = false;
                } else {
                    lat = ((DFloNum) latObj).doubleValue();
                    lng = ((DFloNum) lngObj).doubleValue();
                }
                //check for lat, lng range
                // Latitude measurements range from 0 to (+/)90.
                // Longitude measurements range from 0 to (+/)180
                if ((lat < -90) || (lat > 90) || (lng < -180) || (lng > 180)) {
                    addOne = false;
                }

                //default values for optional params
                int color = mMarkerColor;
                String title = "";
                String snippet = "";
                boolean draggable = mMarkerDraggable;

                if (((YailList) marker).size() >= 3) {
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(2).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(2).toString());
                    // Integer within Yaillist is of type gnu.math.IntNum
                    Object colorObj = ((YailList) marker).getObject(2);

                    if (colorObj instanceof gnu.math.IntNum)
                        color = ((IntNum) ((YailList) marker).getObject(2)).intValue();
                    else {
                        addOne = false;
                    }

                }
                if (((YailList) marker).size() >= 4) {
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(3).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(3).toString());
                    title = ((YailList) marker).getObject(3).toString();
                }
                if (((YailList) marker).size() >= 5) {
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(4).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(4).toString());
                    snippet = ((YailList) marker).getObject(4).toString();
                }
                if (((YailList) marker).size() >= 6) {
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(5).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(5).toString());

                    if (((YailList) marker).getObject(5) instanceof Boolean) {
                        draggable = (Boolean) ((YailList) marker).getObject(5);
                    } else {
                        addOne = false;
                    }
                }

                Color.colorToHSV(color, hsv);
                if (addOne) {
                    int uniqueId = generateMarkerId();
                    markerIds.add(uniqueId);
                    addMarkerToMap(lat, lng, uniqueId, hsv[0], title, snippet, draggable);
                }

            } else {
                // fire exception and throw error messages
                form.dispatchErrorOccurredEvent(this, "AddMarkers", ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                        "marker is not represented as list");
                continue; // don't add this marker because its invalid inputs, going to the next one

            }
        }
        return YailList.makeList(markerIds);

    }

    /**
     * generate unique marker id
     * @return
     */
    private static int generateMarkerId() {
        return snextMarkerId.incrementAndGet();

    }

    /**
     * generate unique circle id
     * @return
     */
    private static int generateCircleId() {
        return snextCircleId.incrementAndGet();

    }

    /**
     * generate unique circle id
     * @return
     */

    /**
     * Add a marker on the Google Map
     * @param lat
     * @param lng
     * @param title
     * @param snippet
     * @param hue
     */
    private int addMarkerToMap(Double lat, Double lng, int id, float hue, String title, String snippet,
            boolean draggable) {
        // what if there are too many markers on Google Map ?
        // TODO: https://code.google.com/p/android-maps-extensions/
        Log.i(TAG, "@addMarkerToMap");
        LatLng latlng = new LatLng(lat, lng);
        Marker marker = mMap
                .addMarker(new MarkerOptions().position(latlng).icon(BitmapDescriptorFactory.defaultMarker(hue)));

        if (!title.isEmpty()) {
            marker.setTitle(title);
        }
        if (!snippet.isEmpty()) {
            marker.setSnippet(snippet);
        }
        marker.setDraggable(draggable);

        markers.put(marker, id);
        return id;
    }

    @SimpleFunction(description = "Add a list of markers composed of name-value pairs. Name fields for a marker are: "
            + "\"lat\" (type double) [required], \"lng\"(type double) [required], "
            + "\"color\"(type int)[in hue value ranging from 0~360], "
            + "\"title\"(type String), \"snippet\"(type String), \"draggable\"(type boolean)")
    public YailList GetMarkers() {
        return markersList;
    }

    @SimpleFunction(description = "Adding a list of markers that are represented as JsonArray. "
            + " The inner JsonObject represents a marker"
            + "and is composed of name-value pairs. Name fields for a marker are: "
            + "\"lat\" (type double) [required], \"lng\"(type double) [required], "
            + "\"color\"(type int)[in hue value ranging from 0~360], "
            + "\"title\"(type String), \"snippet\"(type String), \"draggable\"(type boolean)")
    public void AddMarkersFromJson(String jsonString) {
        ArrayList<Integer> markerIds = new ArrayList<Integer>();
        JsonParser parser = new JsonParser();
        float[] hsv = new float[3];

        // parse jsonString into jsonArray
        try {
            JsonElement markerList = parser.parse(jsonString);
            if (markerList.isJsonArray()) {
                JsonArray markerArray = markerList.getAsJsonArray();

                Log.i(TAG, "It's a JsonArry: " + markerArray.toString());
                for (JsonElement marker : markerArray) {
                    boolean addOne = true;
                    // now we have marker
                    if (marker.isJsonObject()) {
                        JsonObject markerJson = marker.getAsJsonObject();
                        if (markerJson.get("lat") == null || markerJson.get("lng") == null) {
                            addOne = false;

                        } else { // having correct syntax of a marker in Json

                            // check for cases: "lat" : "40.7561"  (as String)
                            JsonPrimitive jpLat = (JsonPrimitive) markerJson.get("lat");
                            JsonPrimitive jpLng = (JsonPrimitive) markerJson.get("lng");

                            double latitude = 0;
                            double longitude = 0;

                            try { //it's possible that when converting to Double, we will have errors
                                  // for example, some json has "lat": "" (empty string for lat, lng values)

                                if (jpLat.isString() && jpLng.isString()) {
                                    Log.i(TAG, "jpLat:" + jpLat.toString());
                                    Log.i(TAG, "jpLng:" + jpLng.toString());

                                    latitude = new Double(jpLat.getAsString());
                                    longitude = new Double(jpLng.getAsString());
                                    Log.i(TAG, "convert to double:" + latitude + "," + longitude);
                                } else {
                                    latitude = markerJson.get("lat").getAsDouble();
                                    longitude = markerJson.get("lng").getAsDouble();
                                }

                            } catch (NumberFormatException e) {
                                addOne = false;
                            }
                            // check for Lat, Lng correct range

                            if ((latitude < -90) || (latitude > 90) || (longitude < -180) || (longitude > 180)) {
                                Log.i(TAG, "Lat/Lng wrong range:" + latitude + "," + longitude);
                                addOne = false;

                            }

                            Color.colorToHSV(mMarkerColor, hsv);
                            float defaultColor = hsv[0];
                            float color = (markerJson.get("color") == null) ? defaultColor
                                    : markerJson.get("color").getAsInt();

                            if ((color < 0) || (color > 360)) {
                                Log.i(TAG, "Wrong color");
                                addOne = false;
                            }

                            String title = (markerJson.get("title") == null) ? ""
                                    : markerJson.get("title").getAsString();
                            String snippet = (markerJson.get("snippet") == null) ? ""
                                    : markerJson.get("snippet").getAsString();
                            boolean draggable = (markerJson.get("draggable") == null) ? mMarkerDraggable
                                    : markerJson.get("draggable").getAsBoolean();

                            if (addOne) {
                                Log.i(TAG, "Adding marker" + latitude + "," + longitude);
                                int uniqueId = generateMarkerId();
                                markerIds.add(uniqueId);
                                addMarkerToMap(latitude, longitude, uniqueId, color, title, snippet, draggable);
                            }
                        }
                    }
                } //end of JsonArray

            } else { // not a JsonArray
                form.dispatchErrorOccurredEvent(this, "AddMarkersFromJson",
                        ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                        "markers needs to be represented as JsonArray");
                markersList = YailList.makeList(markerIds);
            }

        } catch (JsonSyntaxException e) {
            form.dispatchErrorOccurredEvent(this, "AddMarkersFromJson",
                    ErrorMessages.ERROR_GOOGLE_MAP_JSON_FORMAT_DECODE_FAILED, jsonString);
            markersList = YailList.makeList(markerIds); // return an empty markerIds list
        }

        markersList = YailList.makeList(markerIds);
        //  return YailList.makeList(markerIds);
    }

    /**
     * Add a list of YailList to the map
     * @param markers
     */
    @SimpleFunction(description = "Adding a list of YailList for markers. The inner YailList represents a marker "
            + "and is composed of lat(Double) [required], long(Double) [required], color(int)[in hue value ranging from 0-360], "
            + "title(String), snippet(String), draggable(boolean). Return a list of unique ids for the markers that are added")
    public YailList AddMarkersHue(YailList markers) {

        ArrayList<Integer> markerIds = new ArrayList<Integer>();

        for (Object marker : markers.toArray()) {
            boolean addOne = true;
            if (marker instanceof YailList) {
                Log.i(TAG, "Interior YailLiat");
                if (((YailList) marker).size() < 2) {
                    // throw an exception with error messages
                    form.dispatchErrorOccurredEvent(this, "AddMarkers",
                            ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT, "Need more than 2 inputs");
                    addOne = false;

                }

                // ((YailList) marker).getObject(0) will return type gnu.math.DFloNum
                Object latObj = ((YailList) marker).getObject(0);
                Object lngObj = ((YailList) marker).getObject(1);
                Log.i(TAG, "Type: " + latObj.getClass());
                Log.i(TAG, "Type: " + lngObj.getClass());
                Double lat = new Double(0);
                Double lng = new Double(0);

                if (!(latObj instanceof DFloNum && lngObj instanceof DFloNum)) {//if one of the lat or lng is not DFloNum
                    form.dispatchErrorOccurredEvent(this, "AddMarkersHue",
                            ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT, "Not a number for latitude or longitude");
                    addOne = false;
                } else {
                    lat = ((DFloNum) latObj).doubleValue();
                    lng = ((DFloNum) lngObj).doubleValue();
                }

                if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
                    addOne = false;
                    form.dispatchErrorOccurredEvent(this, "AddMarkers",
                            ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                            "Range for the latitude or longitude is wrong");
                }

                Integer uniqueId = generateMarkerId();
                float color = BitmapDescriptorFactory.HUE_BLUE;
                String title = "";
                String snippet = "";
                boolean draggable = mMarkerDraggable;

                if (((YailList) marker).size() >= 3) {
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(2).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(2).toString());
                    // Integer within Yaillist is of type gnu.math.IntNum
                    Object colorObj = ((YailList) marker).getObject(2);

                    if (colorObj instanceof gnu.math.IntNum)
                        color = new Float(((IntNum) ((YailList) marker).getObject(2)).intValue());//extract the int val and convert to Float
                    else {
                        addOne = false;
                        form.dispatchErrorOccurredEvent(this, "AddMarkersHue",
                                ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                                colorObj.toString() + " is not a number");
                    }
                }

                if (((YailList) marker).size() >= 4) {
                    title = (String) ((YailList) marker).getObject(3);
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(3).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(3).toString());
                }

                if (((YailList) marker).size() >= 5) {
                    snippet = (String) ((YailList) marker).getObject(4);
                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(4).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(4).toString());
                }

                if (((YailList) marker).size() >= 6) {

                    Log.i(TAG, "Type: " + ((YailList) marker).getObject(5).getClass());
                    Log.i(TAG, "Value: " + ((YailList) marker).getObject(5).toString());

                    if (((YailList) marker).getObject(5) instanceof Boolean) {
                        draggable = (Boolean) ((YailList) marker).getObject(5);
                    } else {
                        form.dispatchErrorOccurredEvent(this, "AddMarkers",
                                ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT, "marker not as a list");
                        addOne = false;
                    }

                }
                if (addOne) {
                    markerIds.add(uniqueId);
                    addMarkerToMap(lat, lng, uniqueId, color, title, snippet, draggable);
                }
            } else {
                // fire exception and throw error messages
                form.dispatchErrorOccurredEvent(this, "AddMarkersHue", ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT,
                        "Marker is not represented as list");
                return YailList.makeList(markerIds); // return an empty markerIds list
            }
        }
        return YailList.makeList(markerIds);

    }

    @SimpleFunction(description = "Set the property of a marker, note that the marker has to be added first or else will "
            + "throw an exception! Properties include: \"color\"(hue value ranging from 0~360), \"title\", "
            + "\"snippet\", \"draggable\"(give either true or false as the value).")
    public void UpdateMarker(int markerId, String propertyName, Object value) {
        // we don't support update lat, lng here, one can remove the marker and add
        // a new one
        String property = propertyName.trim();
        String propVal = value.toString().trim(); //convert everything to String first

        Log.i(TAG, "@UpdateMarker");
        Log.i(TAG, "markerId:" + markerId);
        Log.i(TAG, "prop:" + propertyName);
        Log.i(TAG, "value:" + value);
        Marker marker = getMarkerIfExisted(markerId);
        Log.i(TAG, "marker?:" + marker);

        if (marker != null) {
            if (property.equals("color")) {
                Log.i(TAG, "we are changing color");
                Float hue = new Float(propVal);
                if (hue < 0 || hue > 360) {
                    form.dispatchErrorOccurredEvent(this, "UpdateMarker",
                            ErrorMessages.ERROR_GOOGLE_MAP_INVALID_INPUT, hue.toString());
                } else {

                    marker.setIcon(BitmapDescriptorFactory.defaultMarker(new Float(propVal)));
                }
            }
            if (property.equals("title")) {
                Log.i(TAG, "we are changing title");
                marker.setTitle(propVal);
            }
            if (property.equals("snippet")) {
                Log.i(TAG, "we are changing snippet");
                marker.setSnippet(propVal);
            }
            if (property.equals("draggable")) {
                Log.i(TAG, "we are changing draggable");
                marker.setDraggable(new Boolean(propVal));
            }
        }
    }

    @SimpleFunction(description = "Get all the existing markers's Ids")
    public YailList GetAllMarkerID() {
        return YailList.makeList(markers.values());

    }

    private Marker getMarkerIfExisted(int markerId) {
        Marker marker = getKeyByValue(markers, markerId);

        if (marker.equals(null)) {
            form.dispatchErrorOccurredEvent(this, "getMarkerIfExisted",
                    ErrorMessages.ERROR_GOOGLE_MAP_MARKER_NOT_EXIST, Integer.toString(markerId));
        }
        return marker;
    }

    @SimpleFunction(description = "Remove a marker from the map")
    public void RemoveMarker(int markerId) {
        Marker marker = getMarkerIfExisted(markerId);
        if (marker != null) {
            markers.remove(marker); //remove from the Hashmap
            marker.remove(); // remove from the google map
        }

    }

    @Override
    public void onMarkerDrag(Marker marker) {
        // TODO Auto-generated method stub
        Log.i(TAG, "Dragging M:" + marker);
        Integer markerId = markers.get(marker);
        // if it's a marker for draggable circle then it's not in the hashmap, Ui will not receive this dragging event
        if (markerId != null) {
            LatLng latlng = marker.getPosition();
            OnMarkerDrag(markerId, latlng.latitude, latlng.longitude);
        }
        // find if the marker is the center or radius marker of any existing draggable circle,
        // then call the move or resize this draggable circle
        for (DraggableCircle dCircle : mCircles) {
            if ((dCircle.getCenterMarker().equals(marker)) || (dCircle.getRadiusMarker().equals(marker))) {
                dCircle.onMarkerMoved(marker); //ask the draggable circle to change it's appearance
            }
        }
    }

    @Override
    public void onMarkerDragEnd(Marker marker) {
        // TODO Auto-generated method stub

        Integer markerId = markers.get(marker);
        // if it's a marker for draggable circle then it's not in the hashmap, Ui will not receive this dragging event
        if (markerId != null) {
            LatLng latlng = marker.getPosition();
            OnMarkerDragEnd(markerId, latlng.latitude, latlng.longitude);
        }
        // find if the marker is the center or radius marker of any existing draggable circle, then call the move or resize
        // this draggable circle
        for (DraggableCircle dCircle : mCircles) {
            if ((dCircle.getCenterMarker().equals(marker)) || (dCircle.getRadiusMarker().equals(marker))) {
                dCircle.onMarkerMoved(marker); //ask the draggable circle to change it's appearance
                // also fire FinishedDraggingCircle() to UI
                int uid = circles.get(dCircle);
                LatLng center = dCircle.getCenterMarker().getPosition();
                FinishedDraggingCircle(uid, center.latitude, center.longitude, dCircle.getRadius());
            }
        }
    }

    @Override
    public void onMarkerDragStart(Marker marker) {
        // TODO Auto-generated method stub
        // if it's a marker for draggable circle then it's not in the hashmap, Ui will not receive this dragging event
        Integer markerId = markers.get(marker);
        if (markerId != null) {
            LatLng latLng = marker.getPosition();
            OnMarkerDragStart(markerId, latLng.latitude, latLng.longitude); //fire event to UI
        }
        // find if the marker is the center or radius marker of any existing draggable circle, then call the move or resize
        // this draggable circle
        for (DraggableCircle dCircle : mCircles) {
            if ((dCircle.getCenterMarker().equals(marker)) || (dCircle.getRadiusMarker().equals(marker))) {
                dCircle.onMarkerMoved(marker); //ask the draggable circle to change it's appearance
            }
        }

    }

    @SimpleEvent(description = "When a marker starts been dragged")
    public void OnMarkerDragStart(final int markerId, final double latitude, final double longitude) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "a marker:" + markerId + "starts been dragged");
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMarkerDragStart", markerId, latitude, longitude);
            }
        });
    }

    @SimpleEvent(description = "When a marker is been dragged")
    public void OnMarkerDrag(final int markerId, final double latitude, final double longitude) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "a marker:" + markerId + "is been dragged");
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMarkerDrag", markerId, latitude, longitude);
            }
        });
    }

    @SimpleEvent(description = "When the user drags a marker and finish the action, "
            + "returning marker's id and it's latest position")
    public void OnMarkerDragEnd(final int markerId, final double latitude, final double longitude) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "a marker:" + markerId + "finishes been dragged");
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMarkerDragEnd", markerId, latitude, longitude);
            }
        });
    }

    @SimpleEvent(description = "When a marker is clicked")
    public void OnMarkerClick(final int markerId, final double latitude, final double longitude) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "a marker:" + markerId + "is clicked");
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMarkerClick", markerId, latitude, longitude);
            }
        });
    }

    @SimpleEvent(description = "When the marker's infowindow is clicked, returning marker's id")
    public void InfoWindowClicked(final int markerId) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "A marker: " + markerId + " its info window is clicked");
                EventDispatcher.dispatchEvent(GoogleMap.this, "InfoWindowClicked", markerId);
            }
        });
    }

    @Override
    public void onInfoWindowClick(Marker marker) {
        // TODO Auto-generated method stub
        Integer markerId = markers.get(marker);
        InfoWindowClicked(markerId);
    }

    @Override
    public boolean onMarkerClick(Marker marker) {
        // TODO Auto-generated method stub

        Integer markerId = markers.get(marker);
        LatLng latLng = marker.getPosition();
        OnMarkerClick(markerId, latLng.latitude, latLng.longitude);

        // We return false to indicate that we have not consumed the event and that we wish
        // for the default behavior to occur (which is for the camera to move such that the
        // marker is centered and for the marker's info window to open, if it has one).
        return false;
    }

    /**
     * A small util function to get the key-value mapping in a map.
     * We use this to get our marker (key) using unique values
     * of marker identifiers
     * @param map
     * @param value
     * @param <T>
     * @param <E>
     * @return
     */
    private <T, E> T getKeyByValue(Map<T, E> map, E value) {
        for (Map.Entry<T, E> entry : map.entrySet()) {
            if (value.equals(entry.getValue())) {
                return entry.getKey();
            }
        }
        return null;
    }

    @Override
    public void onCameraChange(CameraPosition position) {
        Double lat = position.target.latitude;
        Double lng = position.target.longitude;
        Float bearing = position.bearing;
        Float tilt = position.tilt;
        Float zoom = position.zoom;
        CameraPositionChanged(lat, lng, bearing, tilt, zoom);
    }

    /**
     * Called after the camera position has changed, returning all camera position parameters.
     * @param lat
     * @param lng
     * @param bearing
     * @param tilt
     * @param zoom
     */
    @SimpleEvent(description = "Called after the camera position of a map has changed.")
    public void CameraPositionChanged(final double lat, final double lng, final float bearing, final float tilt,
            final float zoom) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "Camera's position has changed:" + lat + ", " + lng + ", " + bearing + "," + tilt + ", "
                        + zoom);
                EventDispatcher.dispatchEvent(GoogleMap.this, "CameraPositionChanged", lat, lng, bearing, tilt,
                        zoom);
            }
        });
    }

    @Override
    public void onMapLongClick(LatLng latLng) {
        // TODO Auto-generated method stub
        OnMapLongClick(latLng.latitude, latLng.longitude);
    }

    /**
     * Called when the user makes a long-press gesture on the map
     * @param lat
     * @param lng
     */
    @SimpleEvent(description = "Called when the user makes a long-press gesture on the map")
    public void OnMapLongClick(final double lat, final double lng) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "Map is longclicked at:" + lat + ", " + lng);
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMapLongClick", lat, lng);
            }
        });
    }

    @Override
    public void onMapClick(LatLng latLng) {
        // TODO Auto-generated method stub
        Log.i(TAG, "receive google maps's onMapClick");
        OnMapClick(latLng.latitude, latLng.longitude);

    }

    @SimpleEvent(description = "Called when the user makes a tap gesture on the map")
    public void OnMapClick(final double lat, final double lng) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "map is clicked at:" + lat + ", " + lng);
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnMapClick", lat, lng);
            }
        });
    }

    @SimpleFunction(description = "Move the map's camera to the specified position and zoom level")
    public void MoveCamera(double lat, double lng, float zoom) {
        if (mMap != null) {
            mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), zoom));
        }
    }

    /**
     *
     * @param neLat Latitude of the northeast location of the bounding box
     * @param neLng Longitude of the northeast location of the bounding box
     * @param swLat Latitude of the southwest location of the bounding box
     * @param swLng Longitude of the southwest location of the bounding box
     */
    @SimpleFunction(description = "Transforms the camera such that the specified latitude/longitude "
            + "bounds are centered on screen at the greatest possible zoom level. Need to specify both latitudes and "
            + "longitudes for both northeast location and southwest location of the bounding box")
    public void BoundCamera(double neLat, double neLng, double swLat, double swLng) {

        LatLng northeast = new LatLng(neLat, neLng);
        LatLng southwest = new LatLng(swLat, swLng);
        LatLngBounds bounds = new LatLngBounds(northeast, southwest);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, 0);
        mMap.moveCamera(cameraUpdate);

    }

    // private class representing the circle overlay. Code copied and extended from Google Example
    // We need to keep a data structure to tie circle and two markers together.
    private class DraggableCircle {
        private final Marker centerMarker;
        private Marker radiusMarker;
        private final Circle circle;
        private double radius;

        // In Draggable circle, AI user will not get reference of the inner objects (circle, markers)
        public DraggableCircle(LatLng center, double radius, float strokeWidth, int strokeColor, int fillColor) {
            this.radius = radius;
            centerMarker = mMap.addMarker(new MarkerOptions().position(center).draggable(true));
            radiusMarker = mMap
                    .addMarker(new MarkerOptions().position(toRadiusLatLng(center, radius)).draggable(true)
                            .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));
            circle = mMap.addCircle(new CircleOptions().center(center).radius(radius).strokeWidth(strokeWidth)
                    .strokeColor(strokeColor).fillColor(fillColor));

        }

        public DraggableCircle(LatLng center, LatLng radiusLatLng, float strokeWidth, int strokeColor,
                int fillColor) {
            this.radius = toRadiusMeters(center, radiusLatLng);
            centerMarker = mMap.addMarker(new MarkerOptions().position(center).draggable(true));
            radiusMarker = mMap.addMarker(new MarkerOptions().position(radiusLatLng).draggable(true)
                    .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));
            circle = mMap.addCircle(new CircleOptions().center(center).radius(radius).strokeWidth(strokeWidth)
                    .strokeColor(strokeColor).fillColor(fillColor));
        }

        // draw a circle between two known marker
        public DraggableCircle(Marker center, Marker radius, float strokeWidth, int strokeColor, int fillColor) {
            this.radius = toRadiusMeters(center.getPosition(), radius.getPosition());
            centerMarker = center;
            radiusMarker = radius;
            circle = mMap.addCircle(new CircleOptions().center(center.getPosition()).radius(this.radius)
                    .strokeWidth(strokeWidth).strokeColor(strokeColor).fillColor(fillColor));
        }

        public boolean onMarkerMoved(Marker marker) {
            if (marker.equals(centerMarker)) {
                circle.setCenter(marker.getPosition());
                radiusMarker.setPosition(toRadiusLatLng(marker.getPosition(), radius));
                return true;
            }
            if (marker.equals(radiusMarker)) {
                radius = toRadiusMeters(centerMarker.getPosition(), radiusMarker.getPosition());
                circle.setRadius(radius);
                return true;
            }
            return false;
        }

        public void removeFromMap() {
            this.circle.remove();
            this.centerMarker.remove();
            this.radiusMarker.remove();
        }

        public Marker getCenterMarker() {
            return this.centerMarker;
        }

        public Marker getRadiusMarker() {
            return this.radiusMarker;
        }

        public Circle getCircle() {
            return this.circle;
        }

        public Double getRadius() {
            return this.radius;
        }

        public void setRadiusMarker(Marker marker) {
            this.radiusMarker = marker;
        }
    }

    /** Generate LatLng of radius marker */
    private static LatLng toRadiusLatLng(LatLng center, double radius) {
        double radiusAngle = Math.toDegrees(radius / RADIUS_OF_EARTH_METERS)
                / Math.cos(Math.toRadians(center.latitude));
        return new LatLng(center.latitude, center.longitude + radiusAngle);
    }

    private static double toRadiusMeters(LatLng center, LatLng radius) {
        float[] result = new float[1];
        Location.distanceBetween(center.latitude, center.longitude, radius.latitude, radius.longitude, result);
        return result[0];
    }

    @Override
    public void onConnectionFailed(ConnectionResult arg0) {
        // TODO Auto-generated method stub
    }

    @Override
    public void onConnected(Bundle arg0) {
        Log.i(TAG, "onConnected to location listener.....");
        mLocationClient.requestLocationUpdates(REQUEST, this); // LocationListener
    }

    @Override
    public void onDisconnected() {
        // TODO Auto-generated method stub
    }

    @Override
    public void onPause() {
        // TODO Auto-generated method stub
        Log.i(TAG, "OnPause, remote LocationClient");
        if (mLocationClient != null) {
            Log.i(TAG, "before location client disconnect");
            mLocationClient.disconnect();
        }
    }

    @Override
    public void onLocationChanged(Location location) {
        // TODO Auto-generated method stub
        OnLocationChanged(location.getLatitude(), location.getLongitude());
    }

    @SimpleEvent(description = "Triggers this event when user location has changed. Only works when EnableMylocation is set to true")
    public void OnLocationChanged(final double lat, final double lng) {
        context.runOnUiThread(new Runnable() {
            public void run() {
                Log.i(TAG, "location changed" + lat + lng);
                EventDispatcher.dispatchEvent(GoogleMap.this, "OnLocationChanged", lat, lng);
            }
        });
    }
}