diff --git a/android/java/.gitignore b/android/java/.gitignore
new file mode 100644
index 00000000000..478d786980b
--- /dev/null
+++ b/android/java/.gitignore
@@ -0,0 +1,14 @@
+# Gradle files
+.gradle/
+
+# IntelliJ files
+.idea
+*.iml
+
+# Build files
+build/
+*/build/
+*.so
+
+# Local settings
+local.properties
diff --git a/android/java/app/build.gradle b/android/java/app/build.gradle
new file mode 100644
index 00000000000..cb3750b7d37
--- /dev/null
+++ b/android/java/app/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "21.1.0"
+
+ defaultConfig {
+ applicationId "com.mapbox.mapboxgl.app"
+ minSdkVersion 19
+ targetSdkVersion 20
+ }
+
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
+
+dependencies {
+ compile project(':lib')
+}
diff --git a/android/java/app/lint.xml b/android/java/app/lint.xml
new file mode 100644
index 00000000000..7328a1fd1c3
--- /dev/null
+++ b/android/java/app/lint.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/java/app/src/main/AndroidManifest.xml b/android/java/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..83995b8d3dd
--- /dev/null
+++ b/android/java/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/java/app/src/main/java/com/mapbox/mapboxgl/app/FragmentActivity.java b/android/java/app/src/main/java/com/mapbox/mapboxgl/app/FragmentActivity.java
new file mode 100644
index 00000000000..1b27ca2fdd1
--- /dev/null
+++ b/android/java/app/src/main/java/com/mapbox/mapboxgl/app/FragmentActivity.java
@@ -0,0 +1,29 @@
+package com.mapbox.mapboxgl.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+public class FragmentActivity extends Activity {
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "FragmentActivity";
+
+ //
+ // Lifecycle events
+ //
+
+ // Called when activity is created
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.v(TAG, "onCreate");
+
+ // Load the layout
+ setContentView(R.layout.activity_fragment);
+ }
+}
diff --git a/android/java/app/src/main/java/com/mapbox/mapboxgl/app/MainActivity.java b/android/java/app/src/main/java/com/mapbox/mapboxgl/app/MainActivity.java
new file mode 100644
index 00000000000..43537f9ac87
--- /dev/null
+++ b/android/java/app/src/main/java/com/mapbox/mapboxgl/app/MainActivity.java
@@ -0,0 +1,98 @@
+package com.mapbox.mapboxgl.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.mapbox.mapboxgl.lib.MapView;
+
+public class MainActivity extends Activity {
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "MainActivity";
+
+ // The map
+ private MapView mMapView;
+
+ //
+ // Lifecycle events
+ //
+
+ // Called when the activity is created
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.v(TAG, "onCreate");
+
+ // Load the layout
+ setContentView(R.layout.activity_main);
+ mMapView = (MapView) findViewById(R.id.map);
+
+ // Need to pass on any saved state to the map
+ mMapView.onCreate(savedInstanceState);
+ }
+
+ // Called when the activity is destroyed
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.v(TAG, "onDestroy");
+
+ // Need to pass on to view
+ mMapView.onDestroy();
+ }
+
+ // Called when the activity is visible
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Log.v(TAG, "onStart");
+
+ // Need to pass on to view
+ mMapView.onStart();
+ }
+
+ // Called when the activity is invisible
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.v(TAG, "onStop");
+
+ // Need to pass on to view
+ mMapView.onStop();
+ }
+
+ // Called when the activity is in the background
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Log.v(TAG, "onPause");
+
+ // Need to pass on to view
+ mMapView.onPause();
+ }
+
+ // Called when the activity is no longer in the background
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.v(TAG, "onResume");
+
+ // Need to pass on to view
+ mMapView.onResume();
+ }
+
+ // Called before activity is destroyed
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ Log.v(TAG, "onSaveInstanceState");
+
+ // Need to retrieve any saved state from the map
+ mMapView.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+}
diff --git a/android/java/app/src/main/res/drawable-hdpi/ic_launcher.png b/android/java/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..288b66551d1
Binary files /dev/null and b/android/java/app/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/android/java/app/src/main/res/drawable-mdpi/ic_launcher.png b/android/java/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..6ae570b4db4
Binary files /dev/null and b/android/java/app/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/android/java/app/src/main/res/drawable-xhdpi/ic_launcher.png b/android/java/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..d4fb7cd9d86
Binary files /dev/null and b/android/java/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/android/java/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/android/java/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..85a6081587e
Binary files /dev/null and b/android/java/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/android/java/app/src/main/res/layout/activity_fragment.xml b/android/java/app/src/main/res/layout/activity_fragment.xml
new file mode 100644
index 00000000000..6310106fce8
--- /dev/null
+++ b/android/java/app/src/main/res/layout/activity_fragment.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android/java/app/src/main/res/layout/activity_main.xml b/android/java/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000000..e4b11e3ada7
--- /dev/null
+++ b/android/java/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android/java/app/src/main/res/values-v11/styles.xml b/android/java/app/src/main/res/values-v11/styles.xml
new file mode 100644
index 00000000000..3c02242ad04
--- /dev/null
+++ b/android/java/app/src/main/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/android/java/app/src/main/res/values-v14/styles.xml b/android/java/app/src/main/res/values-v14/styles.xml
new file mode 100644
index 00000000000..a91fd0372b2
--- /dev/null
+++ b/android/java/app/src/main/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/android/java/app/src/main/res/values/strings.xml b/android/java/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..f6d4e76fadc
--- /dev/null
+++ b/android/java/app/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+ Mapbox GL
+
+
diff --git a/android/java/app/src/main/res/values/styles.xml b/android/java/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000000..6ce89c7ba43
--- /dev/null
+++ b/android/java/app/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/java/build.gradle b/android/java/build.gradle
new file mode 100644
index 00000000000..e1e1653021e
--- /dev/null
+++ b/android/java/build.gradle
@@ -0,0 +1,15 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.13.2'
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
diff --git a/android/java/gradle/wrapper/gradle-wrapper.jar b/android/java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000000..8c0fb64a869
Binary files /dev/null and b/android/java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/java/gradle/wrapper/gradle-wrapper.properties b/android/java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..ffaa281a453
--- /dev/null
+++ b/android/java/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip
diff --git a/android/java/gradlew b/android/java/gradlew
new file mode 100755
index 00000000000..91a7e269e19
--- /dev/null
+++ b/android/java/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android/java/gradlew.bat b/android/java/gradlew.bat
new file mode 100644
index 00000000000..8a0b282aa68
--- /dev/null
+++ b/android/java/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/java/lib/build.gradle b/android/java/lib/build.gradle
new file mode 100644
index 00000000000..d6e4f141c30
--- /dev/null
+++ b/android/java/lib/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "21.1.0"
+
+ defaultConfig {
+ applicationId "com.mapbox.mapboxgl.lib"
+ minSdkVersion 19
+ targetSdkVersion 20
+
+ ndk {
+ moduleName "NativeMapView"
+ }
+ }
+
+ buildTypes {
+ debug {
+ jniDebugBuild true
+ }
+
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
+
+dependencies {
+ compile "commons-io:commons-io:2.4"
+}
diff --git a/android/java/lib/lint.xml b/android/java/lib/lint.xml
new file mode 100644
index 00000000000..84598b8d4ba
--- /dev/null
+++ b/android/java/lib/lint.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/java/lib/src/main/AndroidManifest.xml b/android/java/lib/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..b947bc7d0b4
--- /dev/null
+++ b/android/java/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java
new file mode 100644
index 00000000000..86d1d50f687
--- /dev/null
+++ b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java
@@ -0,0 +1,160 @@
+package com.almeros.android.multitouch.gesturedetectors;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+/**
+ * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie
+ * (code.almeros.com)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+public abstract class BaseGestureDetector {
+ protected final Context mContext;
+ protected boolean mGestureInProgress;
+
+ protected MotionEvent mPrevEvent;
+ protected MotionEvent mCurrEvent;
+
+ protected float mCurrPressure;
+ protected float mPrevPressure;
+ protected long mTimeDelta;
+
+ /**
+ * This value is the threshold ratio between the previous combined pressure
+ * and the current combined pressure. When pressure decreases rapidly
+ * between events the position values can often be imprecise, as it usually
+ * indicates that the user is in the process of lifting a pointer off of the
+ * device. This value was tuned experimentally.
+ */
+ protected static final float PRESSURE_THRESHOLD = 0.67f;
+
+ public BaseGestureDetector(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * All gesture detectors need to be called through this method to be able to
+ * detect gestures. This method delegates work to handler methods
+ * (handleStartProgressEvent, handleInProgressEvent) implemented in
+ * extending classes.
+ *
+ * @param event
+ * @return
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ final int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
+ if (!mGestureInProgress) {
+ handleStartProgressEvent(actionCode, event);
+ } else {
+ handleInProgressEvent(actionCode, event);
+ }
+ return true;
+ }
+
+ /**
+ * Called when the current event occurred when NO gesture is in progress
+ * yet. The handling in this implementation may set the gesture in progress
+ * (via mGestureInProgress) or out of progress
+ *
+ * @param actionCode
+ * @param event
+ */
+ protected abstract void handleStartProgressEvent(int actionCode,
+ MotionEvent event);
+
+ /**
+ * Called when the current event occurred when a gesture IS in progress. The
+ * handling in this implementation may set the gesture out of progress (via
+ * mGestureInProgress).
+ *
+ * @param action
+ * @param event
+ */
+ protected abstract void handleInProgressEvent(int actionCode,
+ MotionEvent event);
+
+ protected void updateStateByEvent(MotionEvent curr) {
+ final MotionEvent prev = mPrevEvent;
+
+ // Reset mCurrEvent
+ if (mCurrEvent != null) {
+ mCurrEvent.recycle();
+ mCurrEvent = null;
+ }
+ mCurrEvent = MotionEvent.obtain(curr);
+
+ // Delta time
+ mTimeDelta = curr.getEventTime() - prev.getEventTime();
+
+ // Pressure
+ mCurrPressure = curr.getPressure(curr.getActionIndex());
+ mPrevPressure = prev.getPressure(prev.getActionIndex());
+ }
+
+ protected void resetState() {
+ if (mPrevEvent != null) {
+ mPrevEvent.recycle();
+ mPrevEvent = null;
+ }
+ if (mCurrEvent != null) {
+ mCurrEvent.recycle();
+ mCurrEvent = null;
+ }
+ mGestureInProgress = false;
+ }
+
+ /**
+ * Returns {@code true} if a gesture is currently in progress.
+ *
+ * @return {@code true} if a gesture is currently in progress, {@code false}
+ * otherwise.
+ */
+ public boolean isInProgress() {
+ return mGestureInProgress;
+ }
+
+ /**
+ * Return the time difference in milliseconds between the previous accepted
+ * GestureDetector event and the current GestureDetector event.
+ *
+ * @return Time difference since the last move event in milliseconds.
+ */
+ public long getTimeDelta() {
+ return mTimeDelta;
+ }
+
+ /**
+ * Return the event time of the current GestureDetector event being
+ * processed.
+ *
+ * @return Current GestureDetector event time in milliseconds.
+ */
+ public long getEventTime() {
+ return mCurrEvent.getEventTime();
+ }
+
+}
diff --git a/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java
new file mode 100644
index 00000000000..0136183ed27
--- /dev/null
+++ b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java
@@ -0,0 +1,188 @@
+package com.almeros.android.multitouch.gesturedetectors;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+/**
+ * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie
+ * (code.almeros.com)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+public class MoveGestureDetector extends BaseGestureDetector {
+
+ /**
+ * Listener which must be implemented which is used by MoveGestureDetector
+ * to perform callbacks to any implementing class which is registered to a
+ * MoveGestureDetector via the constructor.
+ *
+ * @see MoveGestureDetector.SimpleOnMoveGestureListener
+ */
+ public interface OnMoveGestureListener {
+ public boolean onMove(MoveGestureDetector detector);
+
+ public boolean onMoveBegin(MoveGestureDetector detector);
+
+ public void onMoveEnd(MoveGestureDetector detector);
+ }
+
+ /**
+ * Helper class which may be extended and where the methods may be
+ * implemented. This way it is not necessary to implement all methods of
+ * OnMoveGestureListener.
+ */
+ public static class SimpleOnMoveGestureListener implements
+ OnMoveGestureListener {
+ public boolean onMove(MoveGestureDetector detector) {
+ return false;
+ }
+
+ public boolean onMoveBegin(MoveGestureDetector detector) {
+ return true;
+ }
+
+ public void onMoveEnd(MoveGestureDetector detector) {
+ // Do nothing, overridden implementation may be used
+ }
+ }
+
+ private static final PointF FOCUS_DELTA_ZERO = new PointF();
+
+ private final OnMoveGestureListener mListener;
+
+ private PointF mCurrFocusInternal;
+ private PointF mPrevFocusInternal;
+ private PointF mFocusExternal = new PointF();
+ private PointF mFocusDeltaExternal = new PointF();
+
+ public MoveGestureDetector(Context context, OnMoveGestureListener listener) {
+ super(context);
+ mListener = listener;
+ }
+
+ @Override
+ protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_DOWN:
+ resetState(); // In case we missed an UP/CANCEL event
+
+ mPrevEvent = MotionEvent.obtain(event);
+ mTimeDelta = 0;
+
+ updateStateByEvent(event);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ mGestureInProgress = mListener.onMoveBegin(this);
+ break;
+ }
+ }
+
+ @Override
+ protected void handleInProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mListener.onMoveEnd(this);
+ resetState();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ updateStateByEvent(event);
+
+ // Only accept the event if our relative pressure is within
+ // a certain limit. This can help filter shaky data as a
+ // finger is lifted.
+ if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
+ final boolean updatePrevious = mListener.onMove(this);
+ if (updatePrevious) {
+ mPrevEvent.recycle();
+ mPrevEvent = MotionEvent.obtain(event);
+ }
+ }
+ break;
+ }
+ }
+
+ protected void updateStateByEvent(MotionEvent curr) {
+ super.updateStateByEvent(curr);
+
+ final MotionEvent prev = mPrevEvent;
+
+ // Focus intenal
+ mCurrFocusInternal = determineFocalPoint(curr);
+ mPrevFocusInternal = determineFocalPoint(prev);
+
+ // Focus external
+ // - Prevent skipping of focus delta when a finger is added or removed
+ boolean mSkipNextMoveEvent = prev.getPointerCount() != curr
+ .getPointerCount();
+ mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO
+ : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x,
+ mCurrFocusInternal.y - mPrevFocusInternal.y);
+
+ // - Don't directly use mFocusInternal (or skipping will occur). Add
+ // unskipped delta values to mFocusExternal instead.
+ mFocusExternal.x += mFocusDeltaExternal.x;
+ mFocusExternal.y += mFocusDeltaExternal.y;
+ }
+
+ /**
+ * Determine (multi)finger focal point (a.k.a. center point between all
+ * fingers)
+ *
+ * @param MotionEvent
+ * e
+ * @return PointF focal point
+ */
+ private PointF determineFocalPoint(MotionEvent e) {
+ // Number of fingers on screen
+ final int pCount = e.getPointerCount();
+ float x = 0.0f;
+ float y = 0.0f;
+
+ for (int i = 0; i < pCount; i++) {
+ x += e.getX(i);
+ y += e.getY(i);
+ }
+
+ return new PointF(x / pCount, y / pCount);
+ }
+
+ public float getFocusX() {
+ return mFocusExternal.x;
+ }
+
+ public float getFocusY() {
+ return mFocusExternal.y;
+ }
+
+ public PointF getFocusDelta() {
+ return mFocusDeltaExternal;
+ }
+
+}
diff --git a/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java
new file mode 100644
index 00000000000..124fe8509c8
--- /dev/null
+++ b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java
@@ -0,0 +1,180 @@
+package com.almeros.android.multitouch.gesturedetectors;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+/**
+ * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie
+ * (code.almeros.com)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+public class RotateGestureDetector extends TwoFingerGestureDetector {
+
+ /**
+ * Listener which must be implemented which is used by RotateGestureDetector
+ * to perform callbacks to any implementing class which is registered to a
+ * RotateGestureDetector via the constructor.
+ *
+ * @see RotateGestureDetector.SimpleOnRotateGestureListener
+ */
+ public interface OnRotateGestureListener {
+ public boolean onRotate(RotateGestureDetector detector);
+
+ public boolean onRotateBegin(RotateGestureDetector detector);
+
+ public void onRotateEnd(RotateGestureDetector detector);
+ }
+
+ /**
+ * Helper class which may be extended and where the methods may be
+ * implemented. This way it is not necessary to implement all methods of
+ * OnRotateGestureListener.
+ */
+ public static class SimpleOnRotateGestureListener implements
+ OnRotateGestureListener {
+ public boolean onRotate(RotateGestureDetector detector) {
+ return false;
+ }
+
+ public boolean onRotateBegin(RotateGestureDetector detector) {
+ return true;
+ }
+
+ public void onRotateEnd(RotateGestureDetector detector) {
+ // Do nothing, overridden implementation may be used
+ }
+ }
+
+ private final OnRotateGestureListener mListener;
+ private boolean mSloppyGesture;
+
+ public RotateGestureDetector(Context context,
+ OnRotateGestureListener listener) {
+ super(context);
+ mListener = listener;
+ }
+
+ @Override
+ protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // At least the second finger is on screen now
+
+ resetState(); // In case we missed an UP/CANCEL event
+ mPrevEvent = MotionEvent.obtain(event);
+ mTimeDelta = 0;
+
+ updateStateByEvent(event);
+
+ // See if we have a sloppy gesture
+ mSloppyGesture = isSloppyGesture(event);
+ if (!mSloppyGesture) {
+ // No, start gesture now
+ mGestureInProgress = mListener.onRotateBegin(this);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (!mSloppyGesture) {
+ break;
+ }
+
+ // See if we still have a sloppy gesture
+ mSloppyGesture = isSloppyGesture(event);
+ if (!mSloppyGesture) {
+ // No, start normal gesture now
+ mGestureInProgress = mListener.onRotateBegin(this);
+ }
+
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ if (!mSloppyGesture) {
+ break;
+ }
+
+ break;
+ }
+ }
+
+ @Override
+ protected void handleInProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_POINTER_UP:
+ // Gesture ended but
+ updateStateByEvent(event);
+
+ if (!mSloppyGesture) {
+ mListener.onRotateEnd(this);
+ }
+
+ resetState();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (!mSloppyGesture) {
+ mListener.onRotateEnd(this);
+ }
+
+ resetState();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ updateStateByEvent(event);
+
+ // Only accept the event if our relative pressure is within
+ // a certain limit. This can help filter shaky data as a
+ // finger is lifted.
+ if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
+ final boolean updatePrevious = mListener.onRotate(this);
+ if (updatePrevious) {
+ mPrevEvent.recycle();
+ mPrevEvent = MotionEvent.obtain(event);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void resetState() {
+ super.resetState();
+ mSloppyGesture = false;
+ }
+
+ /**
+ * Return the rotation difference from the previous rotate event to the
+ * current event.
+ *
+ * @return The current rotation //difference in degrees.
+ */
+ public float getRotationDegreesDelta() {
+ double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX)
+ - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX);
+ return (float) (diffRadians * 180.0 / Math.PI);
+ }
+}
diff --git a/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java
new file mode 100644
index 00000000000..254597105b1
--- /dev/null
+++ b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java
@@ -0,0 +1,213 @@
+package com.almeros.android.multitouch.gesturedetectors;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+/**
+ * @author Robert Nordan (robert.nordan@norkart.no)
+ *
+ * Copyright (c) 2013, Norkart AS
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+public class ShoveGestureDetector extends TwoFingerGestureDetector {
+
+ /**
+ * Listener which must be implemented which is used by ShoveGestureDetector
+ * to perform callbacks to any implementing class which is registered to a
+ * ShoveGestureDetector via the constructor.
+ *
+ * @see ShoveGestureDetector.SimpleOnShoveGestureListener
+ */
+ public interface OnShoveGestureListener {
+ public boolean onShove(ShoveGestureDetector detector);
+
+ public boolean onShoveBegin(ShoveGestureDetector detector);
+
+ public void onShoveEnd(ShoveGestureDetector detector);
+ }
+
+ /**
+ * Helper class which may be extended and where the methods may be
+ * implemented. This way it is not necessary to implement all methods of
+ * OnShoveGestureListener.
+ */
+ public static class SimpleOnShoveGestureListener implements
+ OnShoveGestureListener {
+ public boolean onShove(ShoveGestureDetector detector) {
+ return false;
+ }
+
+ public boolean onShoveBegin(ShoveGestureDetector detector) {
+ return true;
+ }
+
+ public void onShoveEnd(ShoveGestureDetector detector) {
+ // Do nothing, overridden implementation may be used
+ }
+ }
+
+ private float mPrevAverageY;
+ private float mCurrAverageY;
+
+ private final OnShoveGestureListener mListener;
+ private boolean mSloppyGesture;
+
+ public ShoveGestureDetector(Context context, OnShoveGestureListener listener) {
+ super(context);
+ mListener = listener;
+ }
+
+ @Override
+ protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // At least the second finger is on screen now
+
+ resetState(); // In case we missed an UP/CANCEL event
+ mPrevEvent = MotionEvent.obtain(event);
+ mTimeDelta = 0;
+
+ updateStateByEvent(event);
+
+ // See if we have a sloppy gesture
+ mSloppyGesture = isSloppyGesture(event);
+ if (!mSloppyGesture) {
+ // No, start gesture now
+ mGestureInProgress = mListener.onShoveBegin(this);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (!mSloppyGesture) {
+ break;
+ }
+
+ // See if we still have a sloppy gesture
+ mSloppyGesture = isSloppyGesture(event);
+ if (!mSloppyGesture) {
+ // No, start normal gesture now
+ mGestureInProgress = mListener.onShoveBegin(this);
+ }
+
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ if (!mSloppyGesture) {
+ break;
+ }
+
+ break;
+ }
+ }
+
+ @Override
+ protected void handleInProgressEvent(int actionCode, MotionEvent event) {
+ switch (actionCode) {
+ case MotionEvent.ACTION_POINTER_UP:
+ // Gesture ended but
+ updateStateByEvent(event);
+
+ if (!mSloppyGesture) {
+ mListener.onShoveEnd(this);
+ }
+
+ resetState();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (!mSloppyGesture) {
+ mListener.onShoveEnd(this);
+ }
+
+ resetState();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ updateStateByEvent(event);
+
+ // Only accept the event if our relative pressure is within
+ // a certain limit. This can help filter shaky data as a
+ // finger is lifted. Also check that shove is meaningful.
+ if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD
+ && Math.abs(getShovePixelsDelta()) > 0.5f) {
+ final boolean updatePrevious = mListener.onShove(this);
+ if (updatePrevious) {
+ mPrevEvent.recycle();
+ mPrevEvent = MotionEvent.obtain(event);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void resetState() {
+ super.resetState();
+ mSloppyGesture = false;
+ mPrevAverageY = 0.0f;
+ mCurrAverageY = 0.0f;
+ }
+
+ @Override
+ protected void updateStateByEvent(MotionEvent curr) {
+ super.updateStateByEvent(curr);
+
+ final MotionEvent prev = mPrevEvent;
+ float py0 = prev.getY(0);
+ float py1 = prev.getY(1);
+ mPrevAverageY = (py0 + py1) / 2.0f;
+
+ float cy0 = curr.getY(0);
+ float cy1 = curr.getY(1);
+ mCurrAverageY = (cy0 + cy1) / 2.0f;
+ }
+
+ @Override
+ protected boolean isSloppyGesture(MotionEvent event) {
+ boolean sloppy = super.isSloppyGesture(event);
+ if (sloppy)
+ return true;
+
+ // If it's not traditionally sloppy, we check if the angle between
+ // fingers
+ // is acceptable.
+ double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX));
+ // about 20 degrees, left or right
+ return !((0.0f < angle && angle < 0.35f) || 2.79f < angle
+ && angle < Math.PI);
+ }
+
+ /**
+ * Return the distance in pixels from the previous shove event to the
+ * current event.
+ *
+ * @return The current distance in pixels.
+ */
+ public float getShovePixelsDelta() {
+ return mCurrAverageY - mPrevAverageY;
+ }
+}
diff --git a/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java
new file mode 100644
index 00000000000..5215a91bba1
--- /dev/null
+++ b/android/java/lib/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java
@@ -0,0 +1,228 @@
+package com.almeros.android.multitouch.gesturedetectors;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie
+ * (code.almeros.com)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+public abstract class TwoFingerGestureDetector extends BaseGestureDetector {
+
+ private final float mEdgeSlop;
+ private float mRightSlopEdge;
+ private float mBottomSlopEdge;
+
+ protected float mPrevFingerDiffX;
+ protected float mPrevFingerDiffY;
+ protected float mCurrFingerDiffX;
+ protected float mCurrFingerDiffY;
+
+ private float mCurrLen;
+ private float mPrevLen;
+
+ private PointF mFocus;
+
+ public TwoFingerGestureDetector(Context context) {
+ super(context);
+
+ ViewConfiguration config = ViewConfiguration.get(context);
+ mEdgeSlop = config.getScaledEdgeSlop();
+ }
+
+ @Override
+ protected abstract void handleStartProgressEvent(int actionCode,
+ MotionEvent event);
+
+ @Override
+ protected abstract void handleInProgressEvent(int actionCode,
+ MotionEvent event);
+
+ protected void updateStateByEvent(MotionEvent curr) {
+ super.updateStateByEvent(curr);
+
+ final MotionEvent prev = mPrevEvent;
+
+ mCurrLen = -1;
+ mPrevLen = -1;
+
+ // Previous
+ final float px0 = prev.getX(0);
+ final float py0 = prev.getY(0);
+ final float px1 = prev.getX(1);
+ final float py1 = prev.getY(1);
+ final float pvx = px1 - px0;
+ final float pvy = py1 - py0;
+ mPrevFingerDiffX = pvx;
+ mPrevFingerDiffY = pvy;
+
+ // Current
+ final float cx0 = curr.getX(0);
+ final float cy0 = curr.getY(0);
+ final float cx1 = curr.getX(1);
+ final float cy1 = curr.getY(1);
+ final float cvx = cx1 - cx0;
+ final float cvy = cy1 - cy0;
+ mCurrFingerDiffX = cvx;
+ mCurrFingerDiffY = cvy;
+ mFocus = determineFocalPoint(curr);
+ }
+
+ /**
+ * Return the current distance between the two pointers forming the gesture
+ * in progress.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpan() {
+ if (mCurrLen == -1) {
+ final float cvx = mCurrFingerDiffX;
+ final float cvy = mCurrFingerDiffY;
+ mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy);
+ }
+ return mCurrLen;
+ }
+
+ /**
+ * Return the previous distance between the two pointers forming the gesture
+ * in progress.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpan() {
+ if (mPrevLen == -1) {
+ final float pvx = mPrevFingerDiffX;
+ final float pvy = mPrevFingerDiffY;
+ mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy);
+ }
+ return mPrevLen;
+ }
+
+ /**
+ * MotionEvent has no getRawX(int) method; simulate it pending future API
+ * approval.
+ *
+ * @param event
+ * @param pointerIndex
+ * @return
+ */
+ protected static float getRawX(MotionEvent event, int pointerIndex) {
+ float offset = event.getX() - event.getRawX();
+ if (pointerIndex < event.getPointerCount()) {
+ return event.getX(pointerIndex) + offset;
+ }
+ return 0.0f;
+ }
+
+ /**
+ * MotionEvent has no getRawY(int) method; simulate it pending future API
+ * approval.
+ *
+ * @param event
+ * @param pointerIndex
+ * @return
+ */
+ protected static float getRawY(MotionEvent event, int pointerIndex) {
+ float offset = event.getY() - event.getRawY();
+ if (pointerIndex < event.getPointerCount()) {
+ return event.getY(pointerIndex) + offset;
+ }
+ return 0.0f;
+ }
+
+ /**
+ * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge
+ * of the user's hand is touching the screen, for example.
+ *
+ * @param event
+ * @return
+ */
+ protected boolean isSloppyGesture(MotionEvent event) {
+ // As orientation can change, query the metrics in touch down
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
+ mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
+
+ final float edgeSlop = mEdgeSlop;
+ final float rightSlop = mRightSlopEdge;
+ final float bottomSlop = mBottomSlopEdge;
+
+ final float x0 = event.getRawX();
+ final float y0 = event.getRawY();
+ final float x1 = getRawX(event, 1);
+ final float y1 = getRawY(event, 1);
+
+ boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop || x0 > rightSlop
+ || y0 > bottomSlop;
+ boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop || x1 > rightSlop
+ || y1 > bottomSlop;
+
+ if (p0sloppy && p1sloppy) {
+ return true;
+ } else if (p0sloppy) {
+ return true;
+ } else if (p1sloppy) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determine (multi)finger focal point (a.k.a. center point between all
+ * fingers)
+ *
+ * @param MotionEvent
+ * e
+ * @return PointF focal point
+ */
+ public static PointF determineFocalPoint(MotionEvent e) {
+ // Number of fingers on screen
+ final int pCount = e.getPointerCount();
+ float x = 0.0f;
+ float y = 0.0f;
+
+ for (int i = 0; i < pCount; i++) {
+ x += e.getX(i);
+ y += e.getY(i);
+ }
+
+ return new PointF(x / pCount, y / pCount);
+ }
+
+ public float getFocusX() {
+ return mFocus.x;
+ }
+
+ public float getFocusY() {
+ return mFocus.y;
+ }
+
+}
diff --git a/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLat.java b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLat.java
new file mode 100644
index 00000000000..87260c2a9e1
--- /dev/null
+++ b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLat.java
@@ -0,0 +1,91 @@
+package com.mapbox.mapboxgl.lib;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class LonLat implements Parcelable {
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public LonLat createFromParcel(Parcel in) {
+ return new LonLat(in);
+ }
+
+ public LonLat[] newArray(int size) {
+ return new LonLat[size];
+ }
+ };
+
+ private double lon;
+ private double lat;
+
+ public LonLat(double lon, double lat) {
+ this.lon = lon;
+ this.lat = lat;
+ }
+
+ private LonLat(Parcel in) {
+ lon = in.readDouble();
+ lat = in.readDouble();
+ }
+
+ public double getLon() {
+ return lon;
+ }
+
+ public void setLon(double lon) {
+ this.lon = lon;
+ }
+
+ public double getLat() {
+ return lat;
+ }
+
+ public void setLat(double lat) {
+ this.lat = lat;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ long temp;
+ temp = Double.doubleToLongBits(lat);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(lon);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LonLat other = (LonLat) obj;
+ if (Double.doubleToLongBits(lat) != Double.doubleToLongBits(other.lat))
+ return false;
+ if (Double.doubleToLongBits(lon) != Double.doubleToLongBits(other.lon))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "LonLat [lon=" + lon + ", lat=" + lat + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeDouble(lon);
+ out.writeDouble(lat);
+ }
+
+}
diff --git a/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLatZoom.java b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLatZoom.java
new file mode 100644
index 00000000000..d20cccd47b6
--- /dev/null
+++ b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/LonLatZoom.java
@@ -0,0 +1,124 @@
+package com.mapbox.mapboxgl.lib;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class LonLatZoom implements Parcelable {
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public LonLatZoom createFromParcel(Parcel in) {
+ return new LonLatZoom(in);
+ }
+
+ public LonLatZoom[] newArray(int size) {
+ return new LonLatZoom[size];
+ }
+ };
+
+ private double lon;
+ private double lat;
+ private double zoom;
+
+ public LonLatZoom(double lon, double lat, double zoom) {
+ this.lon = lon;
+ this.lat = lat;
+ this.zoom = zoom;
+ }
+
+ public LonLatZoom(LonLat lonLat, double zoom) {
+ this.lon = lonLat.getLon();
+ this.lat = lonLat.getLat();
+ this.zoom = zoom;
+ }
+
+ private LonLatZoom(Parcel in) {
+ lon = in.readDouble();
+ lat = in.readDouble();
+ zoom = in.readDouble();
+ }
+
+ public double getLon() {
+ return lon;
+ }
+
+ public void setLon(double lon) {
+ this.lon = lon;
+ }
+
+ public double getLat() {
+ return lat;
+ }
+
+ public void setLat(double lat) {
+ this.lat = lat;
+ }
+
+ public double getZoom() {
+ return zoom;
+ }
+
+ public void setZoom(double zoom) {
+ this.zoom = zoom;
+ }
+
+ public LonLat getLonLat() {
+ return new LonLat(lon, lat);
+ }
+
+ public void setLonLat(LonLat lonLat) {
+ this.lon = lonLat.getLon();
+ this.lat = lonLat.getLat();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ long temp;
+ temp = Double.doubleToLongBits(lat);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(lon);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(zoom);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LonLatZoom other = (LonLatZoom) obj;
+ if (Double.doubleToLongBits(lat) != Double.doubleToLongBits(other.lat))
+ return false;
+ if (Double.doubleToLongBits(lon) != Double.doubleToLongBits(other.lon))
+ return false;
+ if (Double.doubleToLongBits(zoom) != Double
+ .doubleToLongBits(other.zoom))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "LonLatZoom [lon=" + lon + ", lat=" + lat + ", zoom=" + zoom
+ + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeDouble(lon);
+ out.writeDouble(lat);
+ out.writeDouble(zoom);
+ }
+
+}
diff --git a/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapFragment.java b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapFragment.java
new file mode 100644
index 00000000000..1263ac56e89
--- /dev/null
+++ b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapFragment.java
@@ -0,0 +1,150 @@
+package com.mapbox.mapboxgl.lib;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class MapFragment extends Fragment {
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "MapFragment";
+
+ // The map
+ private MapView mMapView;
+
+ // The style attrs to load into the map
+ AttributeSet mAttrs;
+
+ //
+ // Lifecycle events
+ //
+
+ // Called when the fragment is created
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ Log.v(TAG, "onCreateView");
+
+ // Create the map
+ mMapView = new MapView(getActivity());
+
+ // Load the attributes
+ TypedArray typedArray = getActivity().obtainStyledAttributes(mAttrs,
+ R.styleable.MapFragment, 0, 0);
+ try {
+ double centerLongitude = typedArray.getFloat(
+ R.styleable.MapFragment_centerLongitude, 0.0f);
+ double centerLatitude = typedArray.getFloat(
+ R.styleable.MapFragment_centerLatitude, 0.0f);
+ LonLat centerCoordinate = new LonLat(centerLongitude,
+ centerLatitude);
+ mMapView.setCenterCoordinate(centerCoordinate);
+ mMapView.setDirection(typedArray.getFloat(
+ R.styleable.MapFragment_direction, 0.0f));
+ mMapView.setZoomLevel(typedArray.getFloat(
+ R.styleable.MapFragment_zoomLevel, 0.0f));
+ mMapView.setZoomEnabled(typedArray.getBoolean(
+ R.styleable.MapFragment_zoomEnabled, true));
+ mMapView.setScrollEnabled(typedArray.getBoolean(
+ R.styleable.MapFragment_scrollEnabled, true));
+ mMapView.setRotateEnabled(typedArray.getBoolean(
+ R.styleable.MapFragment_rotateEnabled, true));
+ mMapView.setDebugActive(typedArray.getBoolean(
+ R.styleable.MapFragment_debugActive, false));
+ } finally {
+ typedArray.recycle();
+ }
+
+ // Need to pass on any saved state to the map
+ mMapView.onCreate(savedInstanceState);
+
+ // Return the map as the root view
+ return mMapView;
+ }
+
+ // Called when the fragment is destroyed
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ Log.v(TAG, "onDestroyView");
+
+ // Need to pass on to view
+ mMapView.onDestroy();
+ mMapView = null;
+ }
+
+ // Called to load any fragment attributes set in the activity's layout
+ @Override
+ public void onInflate(Activity activity, AttributeSet attrs,
+ Bundle savedInstanceState) {
+ super.onInflate(activity, attrs, savedInstanceState);
+ Log.v(TAG, "onInflate");
+
+ // TODO seems this is called before onCreateView
+ // Kinda strange, is it supposed to be like this?
+ // Need to keep a copy of these and pass on to MapView later
+
+ mAttrs = attrs;
+ }
+
+ // Called when the fragment is visible
+ @Override
+ public void onStart() {
+ super.onStart();
+ Log.v(TAG, "onStart");
+
+ // Need to pass on to view
+ mMapView.onStart();
+ }
+
+ // Called when the fragment is invisible
+ @Override
+ public void onStop() {
+ super.onStop();
+ Log.v(TAG, "onStop");
+
+ // Need to pass on to view
+ mMapView.onStop();
+ }
+
+ // Called when the fragment is in the background
+ @Override
+ public void onPause() {
+ super.onPause();
+ Log.v(TAG, "onPause");
+
+ // Need to pass on to view
+ mMapView.onPause();
+ }
+
+ // Called when the fragment is no longer in the background
+ @Override
+ public void onResume() {
+ super.onResume();
+ Log.v(TAG, "onResume");
+
+ // Need to pass on to view
+ mMapView.onResume();
+ }
+
+ // Called before fragment is destroyed
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ Log.v(TAG, "onSaveInstanceState");
+
+ // Need to retrieve any saved state from the map
+ mMapView.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+}
diff --git a/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapView.java b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapView.java
new file mode 100644
index 00000000000..0f1961d2403
--- /dev/null
+++ b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/MapView.java
@@ -0,0 +1,1178 @@
+package com.mapbox.mapboxgl.lib;
+
+import java.io.IOException;
+
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.ZoomButtonsController;
+
+import com.almeros.android.multitouch.gesturedetectors.RotateGestureDetector;
+import com.almeros.android.multitouch.gesturedetectors.TwoFingerGestureDetector;
+
+// Custom view that shows a Map
+// Based on SurfaceView as we use OpenGL ES to render
+public class MapView extends SurfaceView {
+ // TODO try TextureView
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "MapView";
+
+ // Used for saving instance state
+ private static final String STATE_CENTER_COORDINATE = "centerCoordinate";
+ private static final String STATE_CENTER_DIRECTION = "centerDirection";
+ private static final String STATE_ZOOM_LEVEL = "zoomLevel";
+ private static final String STATE_ZOOM_ENABLED = "zoomEnabled";
+ private static final String STATE_SCROLL_ENABLED = "scrollEnabled";
+ private static final String STATE_ROTATE_ENABLED = "rotateEnabled";
+ private static final String STATE_DEBUG_ACTIVE = "debugActive";
+
+ //
+ // Instance members
+ //
+
+ // Used to call JNI NativeMapView
+ private NativeMapView mNativeMapView;
+
+ // Used to style the map
+ private String mDefaultStyleJSON;
+
+ // Touch gesture detectors
+ private GestureDetector mGestureDetector;
+ private ScaleGestureDetector mScaleGestureDetector;
+ private RotateGestureDetector mRotateGestureDetector;
+ private boolean mTwoTap = false;
+
+ // Shows zoom buttons
+ private ZoomButtonsController mZoomButtonsController;
+
+ // Used to track trackball long presses
+ private TrackballLongPressTimeOut mCurrentTrackballLongPressTimeOut;
+
+ //
+ // Properties
+ //
+
+ private boolean mZoomEnabled = true;
+ private boolean mScrollEnabled = true;
+ private boolean mRotateEnabled = true;
+
+ //
+ // Constructors
+ //
+
+ // Called when no properties are being set from XML
+ public MapView(Context context) {
+ super(context);
+ initialize(context, null, 0);
+ }
+
+ // Called when properties are being set from XML
+ public MapView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context, attrs, 0);
+ }
+
+ // Called when properties are being set from XML
+ public MapView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize(context, attrs, defStyle);
+ }
+
+ //
+ // Initialization
+ //
+
+ // Common initialization code goes here
+ private void initialize(Context context, AttributeSet attrs, int defStyle) {
+ Log.v(TAG, "initialize");
+
+ // Check if we are in Eclipse UI editor
+ if (isInEditMode()) {
+ // TODO editor does not load properly because
+ return;
+ }
+
+ // Load the map style
+ try {
+ mDefaultStyleJSON = IOUtils.toString(context.getResources()
+ // .openRawResource(R.raw.style_leith), Charsets.UTF_8);
+ .openRawResource(R.raw.style), Charsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Couldn't load default style JSON resource", e);
+ }
+
+ // Create the NativeMapView
+ mNativeMapView = new NativeMapView(this, mDefaultStyleJSON);
+ mNativeMapView.addDefaultSource();
+ // mNativeMapView.addSource("countries",
+ // "http://192.168.20.103/%d/%d/%d.pbf");
+
+ // Load the attributes
+ TypedArray typedArray = context.obtainStyledAttributes(attrs,
+ R.styleable.MapView, 0, 0);
+ try {
+ double centerLongitude = typedArray.getFloat(
+ R.styleable.MapView_centerLongitude, 0.0f);
+ double centerLatitude = typedArray.getFloat(
+ R.styleable.MapView_centerLatitude, 0.0f);
+ LonLat centerCoordinate = new LonLat(centerLongitude,
+ centerLatitude);
+ setCenterCoordinate(centerCoordinate);
+ setDirection(typedArray.getFloat(R.styleable.MapView_direction,
+ 0.0f));
+ setZoomLevel(typedArray.getFloat(R.styleable.MapView_zoomLevel,
+ 0.0f));
+ setZoomEnabled(typedArray.getBoolean(
+ R.styleable.MapView_zoomEnabled, true));
+ setScrollEnabled(typedArray.getBoolean(
+ R.styleable.MapView_scrollEnabled, true));
+ setRotateEnabled(typedArray.getBoolean(
+ R.styleable.MapView_rotateEnabled, true));
+ setDebugActive(typedArray.getBoolean(
+ R.styleable.MapView_debugActive, false));
+ } finally {
+ typedArray.recycle();
+ }
+
+ // Ensure this view is interactable
+ setClickable(true);
+ setLongClickable(true);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+
+ // Register the SurfaceHolder callbacks
+ getHolder().addCallback(new Callbacks());
+
+ // Touch gesture detectors
+ mGestureDetector = new GestureDetector(context, new GestureListener());
+ mGestureDetector.setIsLongpressEnabled(true);
+ mScaleGestureDetector = new ScaleGestureDetector(context,
+ new ScaleGestureListener());
+ mRotateGestureDetector = new RotateGestureDetector(context,
+ new RotateGestureListener());
+
+ // Shows the zoom controls
+ // But not when in Eclipse UI editor
+ if (!isInEditMode()) {
+ if (!context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ mZoomButtonsController = new ZoomButtonsController(this);
+ mZoomButtonsController.setZoomSpeed(300);
+ mZoomButtonsController.setOnZoomListener(new OnZoomListener());
+ }
+ }
+ }
+
+ //
+ // Property methods
+ //
+
+ public LonLat getCenterCoordinate() {
+ return mNativeMapView.getLonLat();
+ }
+
+ public void setCenterCoordinate(LonLat centerCoordinate) {
+ setCenterCoordinate(centerCoordinate, false);
+ }
+
+ public void setCenterCoordinate(LonLat centerCoordinate, boolean animated) {
+ double duration = animated ? 0.3 : 0.0;
+ mNativeMapView.setLonLat(centerCoordinate, duration);
+ }
+
+ public void setCenterCoordinate(LonLatZoom centerCoordinate) {
+ setCenterCoordinate(centerCoordinate, false);
+ }
+
+ public void setCenterCoordinate(LonLatZoom centerCoordinate,
+ boolean animated) {
+ double duration = animated ? 0.3 : 0.0;
+ mNativeMapView.setLonLatZoom(centerCoordinate, duration);
+ }
+
+ public double getDirection() {
+ double direction = mNativeMapView.getAngle();
+
+ direction *= 180 / Math.PI;
+
+ while (direction > 360)
+ direction -= 360;
+ while (direction < 0)
+ direction += 360;
+
+ return direction;
+ }
+
+ public void setDirection(double direction) {
+ setDirection(direction, false);
+ }
+
+ public void setDirection(double direction, boolean animated) {
+ double duration = animated ? 0.3 : 0.0;
+
+ direction *= Math.PI / 180;
+
+ mNativeMapView.setAngle(direction, duration);
+ }
+
+ public void resetPosition() {
+ mNativeMapView.resetPosition();
+ }
+
+ public void resetNorth() {
+ mNativeMapView.resetNorth();
+ }
+
+ public double getZoomLevel() {
+ return mNativeMapView.getZoom();
+ }
+
+ public void setZoomLevel(double zoomLevel) {
+ setZoomLevel(zoomLevel, false);
+ }
+
+ public void setZoomLevel(double zoomLevel, boolean animated) {
+ double duration = animated ? 0.3 : 0.0;
+ mNativeMapView.setZoom(zoomLevel, duration);
+ }
+
+ public boolean isZoomEnabled() {
+ return mZoomEnabled;
+ }
+
+ public void setZoomEnabled(boolean zoomEnabled) {
+ this.mZoomEnabled = zoomEnabled;
+
+ if ((mZoomButtonsController != null)
+ && (getVisibility() == View.VISIBLE) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ }
+
+ public boolean isScrollEnabled() {
+ return mScrollEnabled;
+ }
+
+ public void setScrollEnabled(boolean scrollEnabled) {
+ this.mScrollEnabled = scrollEnabled;
+ }
+
+ public boolean isRotateEnabled() {
+ return mRotateEnabled;
+ }
+
+ public void setRotateEnabled(boolean rotateEnabled) {
+ this.mRotateEnabled = rotateEnabled;
+ }
+
+ public boolean isDebugActive() {
+ return mNativeMapView.getDebug();
+ }
+
+ public void setDebugActive(boolean debugActive) {
+ mNativeMapView.setDebug(debugActive);
+ }
+
+ public void toggleDebug() {
+ mNativeMapView.toggleDebug();
+ }
+
+ //
+ // Style methods
+ //
+
+ public void toggleStyle() {
+ // TODO
+ }
+
+ // TODO seems like JSON simple may be better since it implements Map
+ // interface
+ // Other candidates: fastjson, json-smart, fossnova json,
+ public JSONObject getRawStyle() {
+ try {
+ return new JSONObject(mNativeMapView.getStyleJSON());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void setRawStyle(JSONObject style) {
+ mNativeMapView.setStyleJSON(style.toString());
+ }
+
+ public void getStyleOrderedLayerNames() {
+ // TODO
+ }
+
+ public void setStyleOrderedLayerNames() {
+ // TODO
+ }
+
+ public void getAllStyleClasses() {
+ // TODO
+ }
+
+ public void getAppliedStyleClasses() {
+ // TODO
+ }
+
+ public void setAppliedStyleClasses() {
+ // TODO
+ }
+
+ public void getStyleDescriptionForLayer() {
+ // TODO
+ }
+
+ public void setStyleDescription() {
+ // TODO
+ }
+
+ //
+ // Lifecycle events
+ //
+
+ // Called when we need to restore instance state
+ // Must be called from Activity onCreate
+ public void onCreate(Bundle savedInstanceState) {
+ Log.v(TAG, "onCreate");
+ if (savedInstanceState != null) {
+ lonlat = (LonLat) savedInstanceState
+ .getParcelable(STATE_CENTER_COORDINATE);
+ angle = savedInstanceState.getDouble(STATE_CENTER_DIRECTION);
+ zoom = savedInstanceState.getDouble(STATE_ZOOM_LEVEL);
+ setCenterCoordinate((LonLat) savedInstanceState
+ .getParcelable(STATE_CENTER_COORDINATE));
+ setDirection(savedInstanceState.getDouble(STATE_CENTER_DIRECTION));
+ setZoomLevel(savedInstanceState.getDouble(STATE_ZOOM_LEVEL));
+ setZoomEnabled(savedInstanceState.getBoolean(STATE_ZOOM_ENABLED));
+ setScrollEnabled(savedInstanceState
+ .getBoolean(STATE_SCROLL_ENABLED));
+ setRotateEnabled(savedInstanceState
+ .getBoolean(STATE_ROTATE_ENABLED));
+ setDebugActive(savedInstanceState.getBoolean(STATE_DEBUG_ACTIVE));
+ }
+
+ mNativeMapView.initializeDisplay();
+ }
+
+ // Called when we need to save instance state
+ // Must be called from Activity onSaveInstanceState
+ public void onSaveInstanceState(Bundle outState) {
+ Log.v(TAG, "onSaveInstanceState");
+ outState.putParcelable(STATE_CENTER_COORDINATE, getCenterCoordinate());
+ outState.putDouble(STATE_CENTER_DIRECTION, getDirection());
+ outState.putDouble(STATE_ZOOM_LEVEL, getZoomLevel());
+ outState.putBoolean(STATE_ZOOM_ENABLED, isZoomEnabled());
+ outState.putBoolean(STATE_SCROLL_ENABLED, isScrollEnabled());
+ outState.putBoolean(STATE_ROTATE_ENABLED, isRotateEnabled());
+ outState.putBoolean(STATE_DEBUG_ACTIVE, isDebugActive());
+ }
+
+ // Called when we need to clean up
+ // Must be called from Activity onDestroy
+ public void onDestroy() {
+ Log.v(TAG, "onDestroy");
+ mNativeMapView.terminateDisplay();
+ }
+
+ // Called when we need to create the GL context
+ // Must be called from Activity onStart
+ public void onStart() {
+ Log.v(TAG, "onStart");
+ mNativeMapView.initializeContext();
+ }
+
+ // Called when we need to terminate the GL context
+ // Must be called from Activity onPause
+ public void onStop() {
+ Log.v(TAG, "onStop");
+ mNativeMapView.terminateContext();
+ }
+
+ // Called when we need to stop the render thread
+ // Must be called from Activity onPause
+ public void onPause() {
+ Log.v(TAG, "onPause");
+ lonlat = mNativeMapView.getLonLat();
+ angle = mNativeMapView.getAngle();
+ zoom = mNativeMapView.getZoom();
+ mNativeMapView.stop();
+ }
+
+ // Called when we need to start the render thread
+ // Must be called from Activity onResume
+
+ // TODO need to fix this in Map C++ code
+ // Seems map state gets reset when we start()
+ private LonLat lonlat;
+ private double angle, zoom;
+
+ public void onResume() {
+ Log.v(TAG, "onResume");
+ mNativeMapView.start();
+ if (lonlat != null) {
+ mNativeMapView.setLonLat(lonlat);
+ mNativeMapView.setAngle(angle);
+ mNativeMapView.setZoom(zoom);
+ }
+ }
+
+ // This class handles SurfaceHolder callbacks
+ private class Callbacks implements SurfaceHolder.Callback2 {
+
+ // Called when we need to redraw the view
+ // This is called before our view is first visible to prevent an initial
+ // flicker (see Android SDK documentation)
+ @Override
+ public void surfaceRedrawNeeded(SurfaceHolder holder) {
+ Log.v(TAG, "surfaceRedrawNeeded");
+ mNativeMapView.update();
+ }
+
+ // Called when the native surface buffer has been created
+ // Must do all EGL/GL ES initialization here
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.v(TAG, "surfaceCreated");
+ mNativeMapView.createSurface(holder.getSurface());
+ if (lonlat != null) {
+ mNativeMapView.setLonLat(lonlat);
+ mNativeMapView.setAngle(angle);
+ mNativeMapView.setZoom(zoom);
+ }
+ }
+
+ // Called when the native surface buffer has been destroyed
+ // Must do all EGL/GL ES destruction here
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.v(TAG, "surfaceDestroyed");
+ mNativeMapView.destroySurface();
+ }
+
+ // Called when the format or size of the native surface buffer has been
+ // changed
+ // Must handle window resizing here.
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ Log.v(TAG, "surfaceChanged");
+ Log.i(TAG, "resize " + format + " " + width + " " + height);
+ mNativeMapView.resize(width, height);
+ // TODO fix bug when rotating - sometimes 1/2 map black
+ }
+ }
+
+ // Called when view is no longer connected
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Required by ZoomButtonController (from Android SDK documentation)
+ if (mZoomButtonsController != null) {
+ mZoomButtonsController.setVisible(false);
+ }
+ }
+
+ // Called when view is hidden and shown
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ // Required by ZoomButtonController (from Android SDK documentation)
+ if ((mZoomButtonsController != null) && (visibility != View.VISIBLE)) {
+ mZoomButtonsController.setVisible(false);
+ }
+ if ((mZoomButtonsController != null) && (visibility == View.VISIBLE)
+ && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ }
+
+ //
+ // Draw events
+ //
+
+ // TODO: onDraw for editor?
+ // By default it just shows a gray screen with "MapView"
+ // Not too important but perhaps we could put a static demo map image there
+
+ //
+ // Input events
+ //
+
+ // Zoom in or out
+ private void zoom(boolean zoomIn) {
+ zoom(zoomIn, -1.0f, -1.0f);
+ }
+
+ private void zoom(boolean zoomIn, float x, float y) {
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ if (zoomIn) {
+ mNativeMapView.scaleBy(2.0, x, y, 0.3);
+ } else {
+ // TODO two finger tap zoom out
+ mNativeMapView.scaleBy(0.5, x, y, 0.3);
+ }
+ }
+
+ // Called when user touches the screen, all positions are absolute
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Check and ignore non touch or left clicks
+ if ((event.getButtonState() != 0)
+ && (event.getButtonState() != MotionEvent.BUTTON_PRIMARY)) {
+ return false;
+ }
+
+ // Check two finger gestures first
+ boolean rotateRetVal = false;
+ mRotateGestureDetector.onTouchEvent(event);
+ boolean scaleRetVal = false;
+ mScaleGestureDetector.onTouchEvent(event);
+
+ // Handle two finger tap
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // First pointer down
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // Second pointer down
+ if (event.getPointerCount() == 2) {
+ mTwoTap = true;
+ } else {
+ mTwoTap = false;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ // Second pointer up
+ break;
+
+ case MotionEvent.ACTION_UP:
+ // First pointer up
+ long tapInterval = event.getEventTime() - event.getDownTime();
+ boolean isTap = tapInterval <= ViewConfiguration.getTapTimeout();
+ boolean inProgress = mRotateGestureDetector.isInProgress()
+ || mScaleGestureDetector.isInProgress();
+
+ if (mTwoTap && isTap && !inProgress) {
+ PointF focalPoint = TwoFingerGestureDetector
+ .determineFocalPoint(event);
+ zoom(false, focalPoint.x, focalPoint.y);
+ mTwoTap = false;
+ return true;
+ }
+
+ mTwoTap = false;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTwoTap = false;
+ break;
+ }
+
+ // Do not change this code! It will break very easily.
+ boolean retVal = rotateRetVal || scaleRetVal;
+ retVal = mGestureDetector.onTouchEvent(event) || retVal;
+ return retVal || super.onTouchEvent(event);
+ }
+
+ // This class handles one finger gestures
+ private class GestureListener extends
+ GestureDetector.SimpleOnGestureListener {
+
+ // Must always return true otherwise all events are ignored
+ @Override
+ public boolean onDown(MotionEvent e) {
+ // Show the zoom controls
+ if ((mZoomButtonsController != null) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+
+ return true;
+ }
+
+ // Called for double taps
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Single finger double tap
+ // Zoom in
+ zoom(true, e.getX(), e.getY());
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ return true;
+ }
+
+ // Called for single taps after a delay
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return false;
+ }
+
+ // Called for a long press
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // TODO
+ }
+
+ // Called for flings
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Fling the map
+ // TODO Google Maps also has a rotate and zoom fling
+ // TODO does not work
+ /*
+ * float ease = 0.25f;
+ *
+ * velocityX = velocityX * ease; velocityY = velocityY * ease;
+ *
+ * double speed = Math.sqrt(velocityX * velocityX + velocityY *
+ * velocityY); double deceleration = 2500; double duration = speed /
+ * (deceleration * ease);
+ *
+ *
+ * // Cancel any animation mNativeMapView.cancelTransitions();
+ *
+ * mNativeMapView.moveBy(velocityX * duration / 2.0, velocityY *
+ * duration / 2.0, duration);
+ *
+ * return true;
+ */
+ return false;
+ }
+
+ // Called for drags
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions(); // TODO need to test canceling
+ // transitions with touch
+
+ // Scroll the map
+ mNativeMapView.moveBy(-distanceX, -distanceY);
+ return true;
+ }
+ }
+
+ // This class handles two finger gestures
+ private class ScaleGestureListener extends
+ ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ long mBeginTime = 0;
+ float mScaleFactor = 1.0f;
+ boolean mStarted = false;
+
+ // Called when two fingers first touch the screen
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ mBeginTime = detector.getEventTime();
+
+ return true;
+ }
+
+ // Called when fingers leave screen
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mBeginTime = 0;
+ mScaleFactor = 1.0f;
+ mStarted = false;
+ }
+
+ // Called each time one of the two fingers moves
+ // Called for pinch zooms
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // If scale is large enough ignore a tap
+ // TODO: Google Maps seem to use a velocity rather than absolute
+ // value?
+ mScaleFactor *= detector.getScaleFactor();
+ if ((mScaleFactor > 1.05f) || (mScaleFactor < 0.95f)) {
+ mStarted = true;
+ }
+
+ // Ignore short touches in case it is a tap
+ // Also ignore small scales
+ long time = detector.getEventTime();
+ long interval = time - mBeginTime;
+ if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) {
+ return false;
+ }
+
+ // TODO complex decision between roate or scale or both (see Google
+ // Maps app)
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Scale the map
+ mNativeMapView.scaleBy(detector.getScaleFactor(),
+ detector.getFocusX(), detector.getFocusY());
+
+ return true;
+ }
+ }
+
+ // This class handles two rotate gestures
+ // TODO need way to single finger rotate - need to research how google maps
+ // does this - for phones with single touch, or when using mouse etc
+ private class RotateGestureListener extends
+ RotateGestureDetector.SimpleOnRotateGestureListener {
+
+ long mBeginTime = 0;
+ float mTotalAngle = 0.0f;
+ boolean mStarted = false;
+
+ // Called when two fingers first touch the screen
+ @Override
+ public boolean onRotateBegin(RotateGestureDetector detector) {
+ if (!mRotateEnabled) {
+ return false;
+ }
+
+ mBeginTime = detector.getEventTime();
+ Log.d("rotate", "rotate begin");
+ return true;
+ }
+
+ // Called when the fingers leave the screen
+ @Override
+ public void onRotateEnd(RotateGestureDetector detector) {
+ mBeginTime = 0;
+ mTotalAngle = 0.0f;
+ mStarted = false;
+ Log.d("rotate", "rotate end");
+ }
+
+ // Called each time one of the two fingers moves
+ // Called for rotation
+ @Override
+ public boolean onRotate(RotateGestureDetector detector) {
+ if (!mRotateEnabled) {
+ return false;
+ }
+
+ Log.d("rotate", "rotate evt");
+
+ // If rotate is large enough ignore a tap
+ // TODO: Google Maps seem to use a velocity rather than absolute
+ // value, up to a point then they always rotate
+ mTotalAngle += detector.getRotationDegreesDelta();
+ Log.d("rotate", "ttl angle " + mTotalAngle);
+ if ((mTotalAngle > 5.0f) || (mTotalAngle < -5.0f)) {
+ mStarted = true;
+ Log.d("rotate", "rotate started");
+ }
+
+ // Ignore short touches in case it is a tap
+ // Also ignore small rotate
+ long time = detector.getEventTime();
+ long interval = time - mBeginTime;
+ if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) {
+ Log.d("rotate", "rotate ignored");
+ return false;
+ }
+
+ // TODO complex decision between rotate or scale or both (see Google
+ // Maps app). It seems if you start one or the other it takes more
+ // to start the other too. Haven't figured out what it uses to
+ // decide when to transition to both at the same time.
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Rotate the map
+ double angle = mNativeMapView.getAngle();
+ angle -= detector.getRotationDegreesDelta() * Math.PI / 180.0;
+ Log.d("rotate", "rotate to " + angle);
+ mNativeMapView.setAngle(angle, detector.getFocusX(),
+ detector.getFocusY());
+
+ return true;
+ }
+ }
+
+ // This class handles input events from the zoom control buttons
+ // Zoom controls allow single touch only devices to zoom in and out
+ private class OnZoomListener implements
+ ZoomButtonsController.OnZoomListener {
+
+ // Not used
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ // Ignore
+ }
+
+ // Called when user pushes a zoom button
+ @Override
+ public void onZoom(boolean zoomIn) {
+ if (!mZoomEnabled) {
+ return;
+ }
+
+ // Zoom in or out
+ zoom(zoomIn);
+ }
+ }
+
+ // Called when the user presses a key, also called for repeating keys held
+ // down
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // If the user has held the scroll key down for a while then accelerate
+ // the scroll speed
+ double scrollDist = event.getRepeatCount() >= 5 ? 50.0 : 10.0;
+
+ // Check which key was pressed via hardware/real key code
+ switch (keyCode) {
+ // Tell the system to track these keys for long presses on
+ // onKeyLongPress is fired
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ event.startTracking();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move left
+ mNativeMapView.moveBy(scrollDist, 0.0);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move right
+ mNativeMapView.moveBy(-scrollDist, 0.0);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move up
+ mNativeMapView.moveBy(0.0, scrollDist);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move down
+ mNativeMapView.moveBy(0.0, -scrollDist);
+ return true;
+
+ default:
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ // Called when the user long presses a key that is being tracked
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ // Check which key was pressed via hardware/real key code
+ switch (keyCode) {
+ // Tell the system to track these keys for long presses on
+ // onKeyLongPress is fired
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Zoom out
+ zoom(false);
+ return true;
+
+ default:
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ // Called when the user releases a key
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // Check if the key action was canceled (used for virtual keyboards)
+ if (event.isCanceled()) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ // Check which key was pressed via hardware/real key code
+ // Note if keyboard does not have physical key (ie primary non-shifted
+ // key) then it will not appear here
+ // Must use the key character map as physical to character is not
+ // fixed/guaranteed
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Zoom in
+ zoom(true);
+ return true;
+ }
+
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+
+ // Called for trackball events, all motions are relative in device specific
+ // units
+ // TODO: test trackball click and long click
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ // Choose the action
+ switch (event.getActionMasked()) {
+ // The trackball was rotated
+ case MotionEvent.ACTION_MOVE:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Scroll the map
+ mNativeMapView.moveBy(-10.0 * event.getX(), -10.0 * event.getY());
+ return true;
+
+ // Trackball was pushed in so start tracking and tell system we are
+ // interested
+ // We will then get the up action
+ case MotionEvent.ACTION_DOWN:
+ // Set up a delayed callback to check if trackball is still
+ // After waiting the system long press time out
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ mCurrentTrackballLongPressTimeOut.cancel();
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ mCurrentTrackballLongPressTimeOut = new TrackballLongPressTimeOut();
+ postDelayed(mCurrentTrackballLongPressTimeOut,
+ ViewConfiguration.getLongPressTimeout());
+ return true;
+
+ // Trackball was released
+ case MotionEvent.ACTION_UP:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Only handle if we have not already long pressed
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ // Zoom in
+ zoom(true);
+ }
+ return true;
+
+ // Trackball was cancelled
+ case MotionEvent.ACTION_CANCEL:
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ mCurrentTrackballLongPressTimeOut.cancel();
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ return true;
+
+ default:
+ // We are not interested in this event
+ return super.onTrackballEvent(event);
+ }
+ }
+
+ // This class implements the trackball long press time out callback
+ private class TrackballLongPressTimeOut implements Runnable {
+
+ // Track if we have been cancelled
+ private boolean cancelled;
+
+ public TrackballLongPressTimeOut() {
+ cancelled = false;
+ }
+
+ // Cancel the timeoht
+ public void cancel() {
+ cancelled = true;
+ }
+
+ // Called when long press time out expires
+ @Override
+ public void run() {
+ // Check if the trackball is still pressed
+ if (!cancelled) {
+ // Zoom out
+ zoom(false);
+
+ // Ensure the up action is not run
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ }
+ }
+
+ // Called for events that don't fit the other handlers
+ // such as mouse scroll events, mouse moves, joystick, trackpad
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ // Mouse events
+ // TODO: SOURCE_TOUCH_NAVIGATION?
+ // TODO: source device resolution?
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ // Choose the action
+ switch (event.getActionMasked()) {
+ // Mouse scrolls
+ case MotionEvent.ACTION_SCROLL:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Get the vertical scroll amount, one click = 1
+ float scrollDist = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+
+ // Scale the map by the appropriate power of two factor
+ mNativeMapView.scaleBy(Math.pow(2.0, scrollDist), event.getX(),
+ event.getY());
+
+ return true;
+
+ default:
+ // We are not interested in this event
+ return super.onGenericMotionEvent(event);
+ }
+ }
+
+ // We are not interested in this event
+ return super.onGenericMotionEvent(event);
+ }
+
+ // Called when the mouse pointer enters or exits the view
+ // or when it fades in or out due to movement
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ // Show the zoom controls
+ if ((mZoomButtonsController != null) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ return true;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ // Hide the zoom controls
+ if (mZoomButtonsController != null) {
+ mZoomButtonsController.setVisible(false);
+ }
+
+ default:
+ // We are not interested in this event
+ return super.onHoverEvent(event);
+ }
+ }
+
+ //
+ // Accessibility events
+ //
+
+ //
+ // Map events
+ //
+
+ public interface OnMapChangedListener {
+ void onMapChanged();
+ }
+
+ private OnMapChangedListener mOnMapChangedListener;
+
+ // Adds a listener for onMapChanged
+ public void setOnMapChangedListener(OnMapChangedListener listener) {
+ mOnMapChangedListener = listener;
+ }
+
+ // Called when the map view transformation has changed
+ // Called via JNI from NativeMapView
+ // Need to update anything that relies on map state
+ protected void onMapChanged() {
+ Log.v(TAG, "onMapChanged");
+ if (mOnMapChangedListener != null) {
+ mOnMapChangedListener.onMapChanged();
+ }
+ }
+}
diff --git a/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/NativeMapView.java b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/NativeMapView.java
new file mode 100644
index 00000000000..468d4713d46
--- /dev/null
+++ b/android/java/lib/src/main/java/com/mapbox/mapboxgl/lib/NativeMapView.java
@@ -0,0 +1,501 @@
+package com.mapbox.mapboxgl.lib;
+
+import java.util.Set;
+
+import android.util.Log;
+import android.view.Surface;
+
+// Class that wraps the native methods for convenience
+class NativeMapView {
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "NativeMapView";
+
+ //
+ // Instance members
+ //
+
+ // Holds the pointer to JNI NativeMapView
+ private long mNativeMapViewPtr = 0;
+
+ // Used for callbacks
+ private MapView mMapView;
+
+ //
+ // Static methods
+ //
+
+ static {
+ System.loadLibrary("mapbox-gl");
+ }
+
+ //
+ // Constructors
+ //
+
+ public NativeMapView(MapView mapView, String defaultStyleJSON) {
+ mMapView = mapView;
+
+ // Create the NativeMapView
+ mNativeMapViewPtr = nativeCreate(defaultStyleJSON);
+ }
+
+ //
+ // Methods
+ //
+
+ public void initializeDisplay() {
+ nativeInitializeDisplay(mNativeMapViewPtr);
+ }
+
+ public void terminateDisplay() {
+ nativeTerminateDisplay(mNativeMapViewPtr);
+ }
+
+ public void initializeContext() {
+ nativeInitializeContext(mNativeMapViewPtr);
+ }
+
+ public void terminateContext() {
+ nativeTerminateContext(mNativeMapViewPtr);
+ }
+
+ public void createSurface(Surface surface) {
+ nativeCreateSurface(mNativeMapViewPtr, surface);
+ }
+
+ public void destroySurface() {
+ nativeDestroySurface(mNativeMapViewPtr);
+ }
+
+ public void start() {
+ nativeStart(mNativeMapViewPtr);
+ }
+
+ public void stop() {
+ nativeStop(mNativeMapViewPtr);
+ }
+
+ public void rerender() {
+ nativeRerender(mNativeMapViewPtr);
+ }
+
+ public void update() {
+ nativeUpdate(mNativeMapViewPtr);
+ }
+
+ public void cleanup() {
+ nativeCleanup(mNativeMapViewPtr);
+ }
+
+ public void addDefaultSource() {
+ nativeAddDefaultSource(mNativeMapViewPtr);
+ }
+
+ public void removeDefaultSource() {
+ nativeRemoveDefaultSource(mNativeMapViewPtr);
+ }
+
+ public void addSource(String name, String url) {
+ nativeAddSource(mNativeMapViewPtr, name, url);
+ }
+
+ public void removeSource(String name) {
+ nativeRemoveSource(mNativeMapViewPtr, name);
+ }
+
+ public void resize(int width, int height) {
+ resize(width, height, 1.0f);
+ }
+
+ public void resize(int width, int height, float ratio) {
+ if (width < 0) {
+ throw new IllegalArgumentException("width cannot be negative.");
+ }
+
+ if (height < 0) {
+ throw new IllegalArgumentException("height cannot be negative.");
+ }
+
+ if (width > 65535) {
+ throw new IllegalArgumentException(
+ "width cannot be greater than 65535.");
+ }
+
+ if (height > 65535) {
+ throw new IllegalArgumentException(
+ "height cannot be greater than 65535.");
+ }
+
+ nativeResize(mNativeMapViewPtr, width, height, ratio);
+ }
+
+ public void resize(int width, int height, float ratio, int fbWidth,
+ int fbHeight) {
+ if (width < 0) {
+ throw new IllegalArgumentException("width cannot be negative.");
+ }
+
+ if (height < 0) {
+ throw new IllegalArgumentException("height cannot be negative.");
+ }
+
+ if (fbWidth < 0) {
+ throw new IllegalArgumentException("fbWidth cannot be negative.");
+ }
+
+ if (fbHeight < 0) {
+ throw new IllegalArgumentException("fbHeight cannot be negative.");
+ }
+
+ if (fbWidth > 65535) {
+ throw new IllegalArgumentException(
+ "fbWidth cannot be greater than 65535.");
+ }
+
+ if (fbHeight > 65535) {
+ throw new IllegalArgumentException(
+ "fbHeight cannot be greater than 65535.");
+ }
+ nativeResize(mNativeMapViewPtr, width, height, ratio, fbWidth, fbHeight);
+ }
+
+ public void setAppliedClasses(Set appliedClasses) {
+ nativeSetAppliedClasses(mNativeMapViewPtr, appliedClasses);
+ }
+
+ public Set getAppliedClasses() {
+ return nativeGetAppliedClasses(mNativeMapViewPtr);
+ }
+
+ public void setDefaultTransitionDuration() {
+ setDefaultTransitionDuration(0);
+ }
+
+ public void setDefaultTransitionDuration(long durationMilliseconds) {
+ if (durationMilliseconds < 0) {
+ throw new IllegalArgumentException(
+ "durationMilliseconds cannot be negative.");
+ }
+
+ nativeSetDefaultTransitionDuration(mNativeMapViewPtr,
+ durationMilliseconds);
+ }
+
+ public void setStyleJSON(String newStyleJSON) {
+ nativeSetStyleJSON(mNativeMapViewPtr, newStyleJSON);
+ }
+
+ public String getStyleJSON() {
+ return nativeGetStyleJSON(mNativeMapViewPtr);
+ }
+
+ public void cancelTransitions() {
+ nativeCancelTransitions(mNativeMapViewPtr);
+ }
+
+ public void moveBy(double dx, double dy) {
+ moveBy(dx, dy, 0.0);
+ }
+
+ public void moveBy(double dx, double dy, double duration) {
+ nativeMoveBy(mNativeMapViewPtr, dx, dy, duration);
+ }
+
+ public void setLonLat(LonLat lonLat) {
+ setLonLat(lonLat, 0.0);
+ }
+
+ public void setLonLat(LonLat lonLat, double duration) {
+ nativeSetLonLat(mNativeMapViewPtr, lonLat, duration);
+ }
+
+ public LonLat getLonLat() {
+ return nativeGetLonLat(mNativeMapViewPtr);
+ }
+
+ public void startPanning() {
+ nativeStartPanning(mNativeMapViewPtr);
+ }
+
+ public void stopPanning() {
+ nativeStopPanning(mNativeMapViewPtr);
+ }
+
+ public void resetPosition() {
+ nativeResetPosition(mNativeMapViewPtr);
+ }
+
+ public void scaleBy(double ds) {
+ scaleBy(ds, -1.0, -1.0);
+ }
+
+ public void scaleBy(double ds, double cx, double cy) {
+ scaleBy(ds, cx, cy, 0.0);
+ }
+
+ public void scaleBy(double ds, double cx, double cy, double duration) {
+ nativeScaleBy(mNativeMapViewPtr, ds, cx, cy, duration);
+ }
+
+ public void setScale(double scale) {
+ setScale(scale, -1.0, -1.0);
+ }
+
+ public void setScale(double scale, double cx, double cy) {
+ setScale(scale, cx, cy, 0.0);
+ }
+
+ public void setScale(double scale, double cx, double cy, double duration) {
+ nativeSetScale(mNativeMapViewPtr, scale, cx, cy, duration);
+ }
+
+ public double getScale() {
+ return nativeGetScale(mNativeMapViewPtr);
+ }
+
+ public void setZoom(double zoom) {
+ setZoom(zoom, 0.0);
+ }
+
+ public void setZoom(double zoom, double duration) {
+ nativeSetZoom(mNativeMapViewPtr, zoom, duration);
+ }
+
+ public double getZoom() {
+ return nativeGetZoom(mNativeMapViewPtr);
+ }
+
+ public void setLonLatZoom(LonLatZoom lonLatZoom) {
+ setLonLatZoom(lonLatZoom, 0.0);
+ }
+
+ public void setLonLatZoom(LonLatZoom lonLatZoom, double duration) {
+ nativeSetLonLatZoom(mNativeMapViewPtr, lonLatZoom, duration);
+ }
+
+ public LonLatZoom getLonLatZoom() {
+ return nativeGetLonLatZoom(mNativeMapViewPtr);
+ }
+
+ public void resetZoom() {
+ nativeResetZoom(mNativeMapViewPtr);
+ }
+
+ public void startScaling() {
+ nativeStartScaling(mNativeMapViewPtr);
+ }
+
+ public void stopScaling() {
+ nativeStopScaling(mNativeMapViewPtr);
+ }
+
+ public double getMinZoom() {
+ return nativeGetMinZoom(mNativeMapViewPtr);
+ }
+
+ public double getMaxZoom() {
+ return nativeGetMaxZoom(mNativeMapViewPtr);
+ }
+
+ public void rotateBy(double sx, double sy, double ex, double ey) {
+ rotateBy(sx, sy, ex, ey, 0.0);
+ }
+
+ public void rotateBy(double sx, double sy, double ex, double ey,
+ double duration) {
+ nativeRotateBy(mNativeMapViewPtr, sx, sy, ex, ey, duration);
+ }
+
+ public void setAngle(double angle) {
+ setAngle(angle, 0.0);
+ }
+
+ public void setAngle(double angle, double duration) {
+ nativeSetAngle(mNativeMapViewPtr, angle, duration);
+ }
+
+ public void setAngle(double angle, double cx, double cy) {
+ nativeSetAngle(mNativeMapViewPtr, angle, cx, cy);
+ }
+
+ public double getAngle() {
+ return nativeGetAngle(mNativeMapViewPtr);
+ }
+
+ public void resetNorth() {
+ nativeResetNorth(mNativeMapViewPtr);
+ }
+
+ public void startRotating() {
+ nativeStartRotating(mNativeMapViewPtr);
+ }
+
+ public void stopRotating() {
+ nativeStopRotating(mNativeMapViewPtr);
+ }
+
+ public boolean canRotate() {
+ return nativeCanRotate(mNativeMapViewPtr);
+ }
+
+ public void setDebug(boolean debug) {
+ nativeSetDebug(mNativeMapViewPtr, debug);
+ }
+
+ public void toggleDebug() {
+ nativeToggleDebug(mNativeMapViewPtr);
+ }
+
+ public boolean getDebug() {
+ return nativeGetDebug(mNativeMapViewPtr);
+ }
+
+ //
+ // Callbacks
+ //
+
+ protected void onMapChanged() {
+ Log.v(TAG, "onMapChanged");
+ mMapView.onMapChanged();
+ }
+
+ //
+ // JNI methods
+ //
+
+ @Override
+ protected void finalize() throws Throwable {
+ nativeDestroy(mNativeMapViewPtr);
+ mNativeMapViewPtr = 0;
+ super.finalize();
+ }
+
+ private native long nativeCreate(String defaultStyleJSON);
+
+ private native void nativeDestroy(long nativeMapViewPtr);
+
+ private native void nativeInitializeDisplay(long nativeMapViewPtr);
+
+ private native void nativeTerminateDisplay(long nativeMapViewPtr);
+
+ private native void nativeInitializeContext(long nativeMapViewPtr);
+
+ private native void nativeTerminateContext(long nativeMapViewPtr);
+
+ private native void nativeCreateSurface(long nativeMapViewPtr,
+ Surface surface);
+
+ private native void nativeDestroySurface(long nativeMapViewPtr);
+
+ private native void nativeStart(long nativeMapViewPtr);
+
+ private native void nativeStop(long nativeMapViewPtr);
+
+ private native void nativeRerender(long nativeMapViewPtr);
+
+ private native void nativeUpdate(long nativeMapViewPtr);
+
+ private native void nativeCleanup(long nativeMapViewPtr);
+
+ private native void nativeAddDefaultSource(long nativeMapViewPtr);
+
+ private native void nativeRemoveDefaultSource(long nativeMapViewPtr);
+
+ private native void nativeAddSource(long nativeMapViewPtr, String name,
+ String url);
+
+ private native void nativeRemoveSource(long nativeMapViewPtr, String name);
+
+ private native void nativeResize(long nativeMapViewPtr, int width,
+ int height, float ratio);
+
+ private native void nativeResize(long nativeMapViewPtr, int width,
+ int height, float ratio, int fbWidth, int fbHeight);
+
+ private native void nativeSetAppliedClasses(long nativeMapViewPtr,
+ Set appliedClasses);
+
+ private native Set nativeGetAppliedClasses(long nativeMapViewPtr);
+
+ private native void nativeSetDefaultTransitionDuration(
+ long nativeMapViewPtr, long durationMilliseconds);
+
+ private native void nativeSetStyleJSON(long nativeMapViewPtr,
+ String newStyleJSON);
+
+ private native String nativeGetStyleJSON(long nativeMapViewPtr);
+
+ private native void nativeCancelTransitions(long nativeMapViewPtr);
+
+ private native void nativeMoveBy(long nativeMapViewPtr, double dx,
+ double dy, double duration);
+
+ private native void nativeSetLonLat(long nativeMapViewPtr, LonLat lonLat,
+ double duration);
+
+ private native LonLat nativeGetLonLat(long nativeMapViewPtr);
+
+ private native void nativeStartPanning(long nativeMapViewPtr);
+
+ private native void nativeStopPanning(long nativeMapViewPtr);
+
+ private native void nativeResetPosition(long nativeMapViewPtr);
+
+ private native void nativeScaleBy(long nativeMapViewPtr, double ds,
+ double cx, double cy, double duration);
+
+ private native void nativeSetScale(long nativeMapViewPtr, double scale,
+ double cx, double cy, double duration);
+
+ private native double nativeGetScale(long nativeMapViewPtr);
+
+ private native void nativeSetZoom(long nativeMapViewPtr, double zoom,
+ double duration);
+
+ private native double nativeGetZoom(long nativeMapViewPtr);
+
+ private native void nativeSetLonLatZoom(long nativeMapViewPtr,
+ LonLatZoom lonLatZoom, double duration);
+
+ private native LonLatZoom nativeGetLonLatZoom(long nativeMapViewPtr);
+
+ private native void nativeResetZoom(long nativeMapViewPtr);
+
+ private native void nativeStartScaling(long nativeMapViewPtr);
+
+ private native void nativeStopScaling(long nativeMapViewPtr);
+
+ private native double nativeGetMinZoom(long nativeMapViewPtr);
+
+ private native double nativeGetMaxZoom(long nativeMapViewPtr);
+
+ private native void nativeRotateBy(long nativeMapViewPtr, double sx,
+ double sy, double ex, double ey, double duration);
+
+ private native void nativeSetAngle(long nativeMapViewPtr, double angle,
+ double duration);
+
+ private native void nativeSetAngle(long nativeMapViewPtr, double angle,
+ double cx, double cy);
+
+ private native double nativeGetAngle(long nativeMapViewPtr);
+
+ private native void nativeResetNorth(long nativeMapViewPtr);
+
+ private native void nativeStartRotating(long nativeMapViewPtr);
+
+ private native void nativeStopRotating(long nativeMapViewPtr);
+
+ private native boolean nativeCanRotate(long nativeMapViewPtr);
+
+ private native void nativeSetDebug(long nativeMapViewPtr, boolean debug);
+
+ private native void nativeToggleDebug(long nativeMapViewPtr);
+
+ private native boolean nativeGetDebug(long nativeMapViewPtr);
+}
diff --git a/android/java/lib/src/main/res/raw/style.min.js b/android/java/lib/src/main/res/raw/style.min.js
new file mode 100644
index 00000000000..906bec14f31
--- /dev/null
+++ b/android/java/lib/src/main/res/raw/style.min.js
@@ -0,0 +1 @@
+{"buckets":{"admin_maritime":{"source":"outdoors","layer":"admin","field":"maritime","value":1,"join":"round","cap":"round","type":"line"},"admin_l2":{"source":"outdoors","layer":"admin","filter":["and",{"field":"admin_level","value":2},{"field":"maritime","operator":"not","value":1}],"join":"round","cap":"round","type":"line"},"admin_l3":{"source":"outdoors","layer":"admin","filter":["and",{"field":"admin_level","value":[3,4,5]},{"field":"maritime","operator":"not","value":1}],"join":"round","type":"line"},"landcover_wood":{"source":"outdoors","layer":"landcover","field":"class","value":"wood","type":"fill"},"landcover_scrub":{"source":"outdoors","layer":"landcover","field":"class","value":"scrub","type":"fill"},"landcover_grass":{"source":"outdoors","layer":"landcover","field":"class","value":"grass","type":"fill"},"landcover_crop":{"source":"outdoors","layer":"landcover","field":"class","value":"crop","type":"fill"},"landcover_snow":{"source":"outdoors","layer":"landcover","field":"class","value":"snow","type":"fill"},"water":{"source":"outdoors","layer":"water","type":"fill"},"waterway_other":{"source":"outdoors","layer":"waterway","field":"type","value":["ditch","drain"],"cap":"round","type":"line"},"waterway_river_canal":{"source":"outdoors","layer":"waterway","field":"type","value":["river","canal"],"cap":"round","type":"line"},"waterway_stream":{"source":"outdoors","layer":"waterway","field":"type","value":"stream","cap":"round","type":"line"},"landuse_park":{"source":"outdoors","layer":"landuse","field":"class","value":"park","type":"fill"},"landuse_pitch":{"source":"outdoors","layer":"landuse","field":"class","value":"pitch","type":"fill"},"landuse_cemetery":{"source":"outdoors","layer":"landuse","field":"class","value":"cemetery","type":"fill"},"landuse_hospital":{"source":"outdoors","layer":"landuse","field":"class","value":"hospital","type":"fill"},"landuse_industrial":{"source":"outdoors","layer":"landuse","type":"fill","field":"class","value":"industrial"},"landuse_school":{"source":"outdoors","layer":"landuse","field":"class","value":"school","type":"fill"},"landuse_wood":{"source":"outdoors","layer":"landuse","field":"class","value":"wood","type":"fill"},"landuse_scrub":{"source":"outdoors","layer":"landuse","field":"class","value":"scrub","type":"fill"},"landuse_grass":{"source":"outdoors","layer":"landuse","field":"class","value":"grass","type":"fill"},"landuse_crop":{"source":"outdoors","layer":"landuse","field":"class","value":"crop","type":"fill"},"landuse_sand":{"source":"outdoors","layer":"landuse","field":"class","value":"sand","type":"fill"},"landuse_rock":{"source":"outdoors","layer":"landuse","field":"class","value":"rock","type":"fill"},"landuse_snow":{"source":"outdoors","layer":"landuse","field":"class","value":"snow","type":"fill"},"overlay_wetland":{"source":"outdoors","layer":"landuse_overlay","field":"class","value":["wetland","wetland_noveg"],"type":"fill"},"overlay_breakwater_pier":{"source":"outdoors","layer":"landuse_overlay","field":"class","value":["breakwater","pier"],"type":"fill"},"hillshade_full_shadow":{"source":"outdoors","layer":"hillshade","field":"class","value":"full_shadow","type":"fill"},"hillshade_medium_shadow":{"source":"outdoors","layer":"hillshade","field":"class","value":"medium_shadow","type":"fill"},"hillshade_medium_highlight":{"source":"outdoors","layer":"hillshade","field":"class","value":"medium_highlight","type":"fill"},"hillshade_full_highlight":{"source":"outdoors","layer":"hillshade","field":"class","value":"full_highlight","type":"fill"},"contour_line_5":{"source":"outdoors","layer":"contour","field":"index","join":"round","value":5,"type":"line"},"contour_line_10":{"source":"outdoors","layer":"contour","field":"index","join":"round","value":10,"type":"line"},"contour_line_other":{"source":"outdoors","layer":"contour","join":"round","type":"line"},"contour_label":{"source":"outdoors","layer":"contour","filter":["and",{"field":"index","value":[5,10]},{"field":"ele","operator":"not","value":0}],"path":"curve","text_field":"{{ele}} m","font":"Open Sans Regular, Arial Unicode MS Regular","fontSize":10,"feature_type":"line","type":"text","maxAngleDelta":0.5},"building":{"source":"outdoors","layer":"building","type":"fill"},"barrier_line_gate":{"source":"outdoors","layer":"barrier_line","field":"class","value":"gate","type":"line"},"barrier_line_fence":{"source":"outdoors","layer":"barrier_line","field":"class","value":"fence","type":"line"},"barrier_line_hedge":{"source":"outdoors","layer":"barrier_line","field":"class","value":"hedge","type":"line"},"barrier_line_land":{"source":"outdoors","layer":"barrier_line","field":"class","value":"land","type":"line"},"barrier_line_land_fill":{"source":"outdoors","layer":"barrier_line","field":"class","value":"land","type":"fill"},"barrier_line_cliff":{"source":"outdoors","layer":"barrier_line","field":"class","value":"cliff","type":"line"},"aeroway_fill":{"source":"outdoors","layer":"aeroway","type":"fill","enabled":12},"aeroway_runway":{"source":"outdoors","layer":"aeroway","field":"type","value":"runway","type":"line","enabled":12},"aeroway_taxiway":{"source":"outdoors","layer":"aeroway","field":"type","value":"taxiway","type":"line","enabled":12},"motorway":{"source":"outdoors","layer":"road","field":"class","value":"motorway","join":"round","cap":"round","type":"line"},"motorway_link":{"source":"outdoors","layer":"road","field":"class","value":"motorway_link","join":"round","cap":"round","type":"line"},"main":{"source":"outdoors","layer":"road","field":"class","value":"main","join":"round","cap":"round","type":"line"},"street":{"source":"outdoors","layer":"road","field":"class","value":["street","street_limited"],"join":"round","cap":"round","type":"line","enabled":12},"service":{"source":"outdoors","layer":"road","field":"class","value":"service","join":"round","cap":"round","type":"line","enabled":15},"path":{"source":"outdoors","layer":"road","field":"class","value":"path","type":"line"},"path_footway":{"source":"outdoors","layer":"road","field":"type","value":"footway","type":"line"},"path_path":{"source":"outdoors","layer":"road","field":"type","value":"path","type":"line"},"path_cycleway":{"source":"outdoors","layer":"road","field":"type","value":"cycleway","type":"line"},"path_mtb":{"source":"outdoors","layer":"road","field":"type","value":"mtb","type":"line"},"path_steps":{"source":"outdoors","layer":"road","field":"type","value":"steps","type":"line"},"path_piste":{"source":"outdoors","layer":"road","field":"type","value":"piste","type":"line"},"major_rail":{"source":"outdoors","layer":"road","field":"class","value":"major_rail","type":"line"},"tunnel_motorway":{"source":"outdoors","layer":"tunnel","field":"class","value":"motorway","type":"line"},"tunnel_motorway_link":{"source":"outdoors","layer":"tunnel","field":"class","value":"motorway_link","type":"line"},"tunnel_main":{"source":"outdoors","layer":"tunnel","field":"class","value":"main","type":"line"},"tunnel_street":{"source":"outdoors","layer":"tunnel","field":"class","value":["street","street_limited"],"type":"line","enabled":12},"tunnel_service":{"source":"outdoors","layer":"tunnel","field":"class","value":"service","type":"line","enabled":15},"tunnel_path":{"source":"outdoors","layer":"tunnel","field":"class","value":"path","type":"line"},"tunnel_path_footway":{"source":"outdoors","layer":"tunnel","field":"type","value":"footway","type":"line"},"tunnel_path_path":{"source":"outdoors","layer":"tunnel","field":"type","value":"path","type":"line"},"tunnel_path_cycleway":{"source":"outdoors","layer":"tunnel","field":"type","value":"cycleway","type":"line"},"tunnel_path_mtb":{"source":"outdoors","layer":"tunnel","field":"type","value":"mtb","type":"line"},"tunnel_path_steps":{"source":"outdoors","layer":"tunnel","field":"type","value":"steps","type":"line"},"tunnel_path_piste":{"source":"outdoors","layer":"tunnel","field":"type","value":"piste","type":"line"},"tunnel_major_rail":{"source":"outdoors","layer":"tunnel","field":"class","value":"major_rail","type":"line"},"bridge_motorway":{"source":"outdoors","layer":"bridge","field":"class","value":"motorway","type":"line"},"bridge_motorway_link":{"source":"outdoors","layer":"bridge","field":"class","value":"motorway_link","type":"line"},"bridge_main":{"source":"outdoors","layer":"bridge","field":"class","value":"main","type":"line"},"bridge_street":{"source":"outdoors","layer":"bridge","field":"class","value":["street","street_limited"],"type":"line","enabled":12},"bridge_service":{"source":"outdoors","layer":"bridge","field":"class","value":"service","type":"line","enabled":15},"bridge_path":{"source":"outdoors","layer":"bridge","type":"line"},"bridge_path_footway":{"source":"outdoors","layer":"bridge","field":"type","value":"footway","type":"line"},"bridge_path_path":{"source":"outdoors","layer":"bridge","field":"type","value":"path","type":"line"},"bridge_path_cycleway":{"source":"outdoors","layer":"bridge","field":"type","value":"cycleway","type":"line"},"bridge_path_mtb":{"source":"outdoors","layer":"bridge","field":"type","value":"mtb","type":"line"},"bridge_path_steps":{"source":"outdoors","layer":"bridge","field":"type","value":"steps","type":"line"},"bridge_path_piste":{"source":"outdoors","layer":"bridge","field":"type","value":"piste","type":"line"},"bridge_major_rail":{"source":"outdoors","layer":"bridge","field":"class","value":"major_rail","type":"line"},"bridge_aerialway":{"source":"outdoors","layer":"bridge","field":"class","value":"aerialway","type":"line"},"country_label":{"source":"outdoors","layer":"country_label","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":24,"feature_type":"point","type":"text","maxWidth":5},"country_label_line":{"source":"outdoors","layer":"country_label_line","type":"line"},"marine_label_line_1":{"source":"outdoors","layer":"marine_label","feature_type":"line","type":"text","field":"labelrank","value":1,"text_field":"{{name_en}}","path":"curve","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":30,"letterSpacing":0.4,"maxAngleDelta":0.5},"marine_label_line_2":{"source":"outdoors","layer":"marine_label","feature_type":"line","field":"labelrank","value":2,"type":"text","text_field":"{{name_en}}","path":"curve","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":24,"letterSpacing":0.2,"maxAngleDelta":0.5},"marine_label_line_3":{"source":"outdoors","layer":"marine_label","feature_type":"line","type":"text","field":"labelrank","value":3,"text_field":"{{name_en}}","path":"curve","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":18,"letterSpacing":0.1,"maxAngleDelta":0.5},"marine_label_line_other":{"source":"outdoors","layer":"marine_label","feature_type":"line","type":"text","field":"labelrank","value":[4,5,6],"text_field":"{{name_en}}","path":"curve","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":16,"letterSpacing":0.1,"maxAngleDelta":0.5},"marine_label_point_1":{"source":"outdoors","layer":"marine_label","feature_type":"point","type":"text","text_field":"{{name_en}}","path":"horizontal","field":"labelrank","value":1,"font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":30,"maxWidth":8,"letterSpacing":0.4,"lineHeight":2},"marine_label_point_2":{"source":"outdoors","layer":"marine_label","feature_type":"point","type":"text","text_field":"{{name_en}}","path":"horizontal","field":"labelrank","value":2,"font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":24,"maxWidth":8,"letterSpacing":0.2,"lineHeight":1.5},"marine_label_point_3":{"source":"outdoors","layer":"marine_label","feature_type":"point","type":"text","text_field":"{{name_en}}","path":"horizontal","field":"labelrank","value":3,"font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":18,"maxWidth":8,"letterSpacing":0.1,"lineHeight":1.3},"marine_label_point_other":{"source":"outdoors","layer":"marine_label","feature_type":"point","type":"text","text_field":"{{name_en}}","path":"horizontal","field":"labelrank","value":[4,5,6],"font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":16,"maxWidth":8,"letterSpacing":0.1,"lineHeight":1.2},"state_label":{"source":"outdoors","layer":"state_label","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Regular, Arial Unicode MS Regular","fontSize":16,"feature_type":"point","type":"text","enabled":4,"maxWidth":8},"place_label_city":{"source":"outdoors","layer":"place_label","field":"type","value":"city","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":20,"feature_type":"point","type":"text","maxWidth":8},"place_label_town":{"source":"outdoors","layer":"place_label","field":"type","value":"town","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":24,"feature_type":"point","type":"text","maxWidth":8},"place_label_village":{"source":"outdoors","layer":"place_label","field":"type","value":"village","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":22,"feature_type":"point","type":"text","maxWidth":8},"place_label_other":{"source":"outdoors","layer":"place_label","field":"type","value":["hamlet","suburb","neighbourhood"],"text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":18,"maxWidth":6,"feature_type":"point","type":"text"},"road_label_1":{"source":"outdoors","layer":"road_label","field":"class","value":["motorway","main"],"text_field":"{{name_en}}","path":"curve","padding":2,"font":"Open Sans Regular, Arial Unicode MS Regular","fontSize":18,"feature_type":"line","type":"text","maxAngleDelta":0.5},"road_label_2":{"source":"outdoors","layer":"road_label","field":"class","value":["street","street_limited"],"text_field":"{{name_en}}","path":"curve","padding":2,"font":"Open Sans Regular, Arial Unicode MS Regular","fontSize":16,"feature_type":"line","type":"text","maxAngleDelta":0.5},"road_label_3":{"source":"outdoors","layer":"road_label","field":"class","value":["service","driveway","path"],"text_field":"{{name_en}}","path":"curve","padding":2,"font":"Open Sans Regular, Arial Unicode MS Regular","fontSize":14,"feature_type":"line","type":"text","maxAngleDelta":0.5},"water_label":{"source":"outdoors","layer":"water_label","text_field":"{{name_en}}","path":"horizontal","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":12,"feature_type":"point","type":"text","maxWidth":8},"waterway_label":{"source":"outdoors","layer":"waterway_label","text_field":"{{name_en}}","path":"curve","font":"Open Sans Semibold Italic, Arial Unicode MS Bold","fontSize":12,"feature_type":"line","type":"text","maxAngleDelta":0.5},"poi":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","field":"scalerank","value":[1,2],"size":12,"type":"point"},"poi_3":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","field":"scalerank","value":3,"size":12,"type":"point"},"poi_4_4":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","filter":["and",{"field":"scalerank","value":4},{"field":"localrank","operator":"<=","value":4}],"size":12,"type":"point"},"poi_4_16":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","filter":["and",{"field":"scalerank","value":4},{"field":"localrank","operator":"<=","value":16},{"field":"localrank","operator":">","value":4}],"size":12,"type":"point"},"poi_4_all":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","field":"scalerank","value":4,"size":12,"type":"point"},"poi_aerodrome":{"source":"outdoors","layer":"poi_label","icon":"{{maki}}","field":"maki","value":"airport","size":24,"type":"point"},"poi_label_1-2":{"source":"outdoors","layer":"poi_label","field":"scalerank","value":[1,2],"text_field":"{{name_en}}","path":"horizontal","padding":2,"maxWidth":10,"verticalAlignment":"top","translate":[0,-1],"font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":12,"feature_type":"point","type":"text"},"poi_label_3":{"source":"outdoors","layer":"poi_label","field":"scalerank","value":3,"text_field":"{{name_en}}","path":"horizontal","padding":2,"maxWidth":8,"verticalAlignment":"top","translate":[0,-1],"font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":11,"feature_type":"point","type":"text"},"poi_label_4_4":{"source":"outdoors","layer":"poi_label","filter":["and",{"field":"scalerank","value":4},{"field":"localrank","operator":"<=","value":4}],"text_field":"{{name_en}}","path":"horizontal","padding":2,"maxWidth":8,"verticalAlignment":"top","translate":[0,-1],"font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":10,"feature_type":"point","type":"text"},"poi_label_4_16":{"source":"outdoors","layer":"poi_label","filter":["and",{"field":"scalerank","value":4},{"field":"localrank","operator":"<=","value":16}],"text_field":"{{name_en}}","path":"horizontal","padding":2,"maxWidth":8,"verticalAlignment":"top","translate":[0,-1],"font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":10,"feature_type":"point","type":"text"},"poi_label_4_all":{"source":"outdoors","layer":"poi_label","field":"scalerank","value":4,"text_field":"{{name_en}}","path":"horizontal","padding":2,"maxWidth":8,"verticalAlignment":"top","translate":[0,-1],"font":"Open Sans Semibold, Arial Unicode MS Bold","fontSize":10,"feature_type":"point","type":"text"}},"structure":[{"name":"background","bucket":"background"},{"name":"landcover_snow","bucket":"landcover_snow"},{"name":"landcover_crop","bucket":"landcover_crop"},{"name":"landcover_grass","bucket":"landcover_grass"},{"name":"landcover_scrub","bucket":"landcover_scrub"},{"name":"landcover_wood","bucket":"landcover_wood"},{"name":"landuse_wood","bucket":"landuse_wood"},{"name":"landuse_school","bucket":"landuse_school"},{"name":"landuse_sand","bucket":"landuse_sand"},{"name":"landuse_pitch","bucket":"landuse_pitch"},{"name":"landuse_park","bucket":"landuse_park"},{"name":"landuse_industrial","bucket":"landuse_industrial"},{"name":"landuse_scrub","bucket":"landuse_scrub"},{"name":"landuse_grass","bucket":"landuse_grass"},{"name":"landuse_crop","bucket":"landuse_crop"},{"name":"landuse_rock","bucket":"landuse_rock"},{"name":"landuse_snow","bucket":"landuse_snow"},{"name":"landuse_hospital","bucket":"landuse_hospital"},{"name":"landuse_cemetery","bucket":"landuse_cemetery"},{"name":"overlay_wetland","bucket":"overlay_wetland"},{"name":"overlay_breakwater_pier","bucket":"overlay_breakwater_pier"},{"name":"waterway_river_canal","bucket":"waterway_river_canal"},{"name":"waterway_stream","bucket":"waterway_stream"},{"name":"building_shadow","bucket":"building"},{"name":"building","bucket":"building"},{"name":"building_wall","bucket":"building"},{"name":"hillshade_full_highlight","bucket":"hillshade_full_highlight"},{"name":"hillshade_medium_highlight","bucket":"hillshade_medium_highlight"},{"name":"hillshade_medium_shadow","bucket":"hillshade_medium_shadow"},{"name":"hillshade_full_shadow","bucket":"hillshade_full_shadow"},{"name":"contour_line_loud","bucket":"contour_line_10"},{"name":"contour_line_loud","bucket":"contour_line_5"},{"name":"contour_line_regular","bucket":"contour_line_other"},{"name":"barrier_line_gate","bucket":"barrier_line_gate"},{"name":"barrier_line_fence","bucket":"barrier_line_fence"},{"name":"barrier_line_hedge","bucket":"barrier_line_hedge"},{"name":"barrier_line_land","bucket":"barrier_line_land"},{"name":"barrier_line_land_fill","bucket":"barrier_line_land_fill"},{"name":"barrier_line_cliff","bucket":"barrier_line_cliff"},{"name":"water","bucket":"water"},{"name":"aeroway_fill","bucket":"aeroway_fill"},{"name":"aeroway_runway","bucket":"aeroway_runway"},{"name":"aeroway_taxiway","bucket":"aeroway_taxiway"},{"name":"tunnel_motorway_link_casing","bucket":"tunnel_motorway_link"},{"name":"tunnel_service_casing","bucket":"tunnel_service"},{"name":"tunnel_main_casing","bucket":"tunnel_main"},{"name":"tunnel_street_casing","bucket":"tunnel_street"},{"name":"tunnel_motorway_link","bucket":"tunnel_motorway_link"},{"name":"tunnel_service","bucket":"tunnel_service"},{"name":"tunnel_street","bucket":"tunnel_street"},{"name":"tunnel_main","bucket":"tunnel_main"},{"name":"tunnel_motorway_casing","bucket":"tunnel_motorway"},{"name":"tunnel_motorway","bucket":"tunnel_motorway"},{"name":"road_path_case","bucket":"tunnel_path"},{"name":"road_path_footway","bucket":"tunnel_path_footway"},{"name":"road_path_path","bucket":"tunnel_path_path"},{"name":"road_path_cycleway","bucket":"tunnel_path_cycleway"},{"name":"road_path_mtb","bucket":"tunnel_path_mtb"},{"name":"road_path_piste","bucket":"tunnel_path_piste"},{"name":"road_path_steps","bucket":"tunnel_path_steps"},{"name":"road_major_rail","bucket":"tunnel_major_rail"},{"name":"road_major_rail_hatching","bucket":"tunnel_major_rail"},{"name":"road_motorway_link_casing","bucket":"motorway_link"},{"name":"road_service_casing","bucket":"service"},{"name":"road_main_casing","bucket":"main"},{"name":"road_street_casing","bucket":"street"},{"name":"road_motorway_casing","bucket":"motorway"},{"name":"road_motorway_link","bucket":"motorway_link"},{"name":"road_service","bucket":"service"},{"name":"road_street","bucket":"street"},{"name":"road_main","bucket":"main"},{"name":"road_motorway","bucket":"motorway"},{"name":"road_path_case","bucket":"path"},{"name":"road_path_footway","bucket":"path_footway"},{"name":"road_path_path","bucket":"path_path"},{"name":"road_path_cycleway","bucket":"path_cycleway"},{"name":"road_path_mtb","bucket":"path_mtb"},{"name":"road_path_piste","bucket":"path_piste"},{"name":"road_path_steps","bucket":"path_steps"},{"name":"road_major_rail","bucket":"major_rail"},{"name":"road_major_rail_hatching","bucket":"major_rail"},{"name":"bridge_motorway_link_casing","bucket":"bridge_motorway_link"},{"name":"bridge_service_casing","bucket":"bridge_service"},{"name":"bridge_main_casing","bucket":"bridge_main"},{"name":"bridge_street_casing","bucket":"bridge_street"},{"name":"bridge_motorway_link","bucket":"bridge_motorway_link"},{"name":"bridge_service","bucket":"bridge_service"},{"name":"bridge_street","bucket":"bridge_street"},{"name":"bridge_main","bucket":"bridge_main"},{"name":"bridge_motorway_casing","bucket":"bridge_motorway"},{"name":"bridge_motorway","bucket":"bridge_motorway"},{"name":"road_path_footway","bucket":"bridge_path_footway"},{"name":"road_path_path","bucket":"bridge_path_path"},{"name":"road_path_cycleway","bucket":"bridge_path_cycleway"},{"name":"road_path_mtb","bucket":"bridge_path_mtb"},{"name":"road_path_piste","bucket":"bridge_path_piste"},{"name":"road_path_steps","bucket":"bridge_path_steps"},{"name":"bridge_aerialway_casing","bucket":"bridge_aerialway"},{"name":"bridge_aerialway","bucket":"bridge_aerialway"},{"name":"road_major_rail","bucket":"bridge_major_rail"},{"name":"road_major_rail_hatching","bucket":"bridge_major_rail"},{"name":"admin_l3","bucket":"admin_l3"},{"name":"admin_l2","bucket":"admin_l2"},{"name":"admin_maritime_cover","bucket":"admin_maritime"},{"name":"admin_maritime","bucket":"admin_maritime"},{"name":"country_label_line","bucket":"country_label_line"},{"name":"country_label","bucket":"country_label"},{"name":"marine_label_line_1","bucket":"marine_label_line_1"},{"name":"marine_label_line_2","bucket":"marine_label_line_2"},{"name":"marine_label_line_3","bucket":"marine_label_line_3"},{"name":"marine_label_line_other","bucket":"marine_label_line_other"},{"name":"marine_label_point_1","bucket":"marine_label_point_1"},{"name":"marine_label_point_2","bucket":"marine_label_point_2"},{"name":"marine_label_point_3","bucket":"marine_label_point_3"},{"name":"marine_label_point_other","bucket":"marine_label_point_other"},{"name":"state_label","bucket":"state_label"},{"name":"place_label_city","bucket":"place_label_city"},{"name":"place_label_town","bucket":"place_label_town"},{"name":"place_label_village","bucket":"place_label_village"},{"name":"place_label_other","bucket":"place_label_other"},{"name":"water_label","bucket":"water_label"},{"name":"poi_aerodrome","bucket":"poi_aerodrome"},{"name":"poi","bucket":"poi"},{"name":"poi_label_1-2","bucket":"poi_label_1-2"},{"name":"road_label_1","bucket":"road_label_1"},{"name":"poi_3","bucket":"poi_3"},{"name":"poi_label_3","bucket":"poi_label_3"},{"name":"road_label_2","bucket":"road_label_2"},{"name":"road_label_3","bucket":"road_label_3"},{"name":"contour_label","bucket":"contour_label"},{"name":"waterway_label","bucket":"waterway_label"},{"name":"poi_4_4","bucket":"poi_4_4"},{"name":"poi_label_4_4","bucket":"poi_label_4_4"},{"name":"poi_4_16","bucket":"poi_4_16"},{"name":"poi_label_4_16","bucket":"poi_label_4_16"},{"name":"poi_4_all","bucket":"poi_4_all"},{"name":"poi_label_4_all","bucket":"poi_label_4_all"}],"constants":{"land":"rgb(244,239,225)","water":"#cdd","water_dark":"#185869","crop":"#eeeed4","grass":"#e6e6cc","scrub":"#dfe5c8","wood":"#cee2bd","snow":"#f4f8ff","rock":"#ddd","sand":"#ffd","cemetery":"#edf4ed","pitch":"#fff","park":"#d4e4bc","piste":"blue","school":"#e8dfe0","hospital":"#f8eee0","builtup":"#f6faff","case":"#fff","motorway":"#cda0a0","main":"#ddc0b9","street":"#fff","text":"#666","text_stroke":"rgba(255,255,255,0.8)","country_text":"#222","marine_text":"#a0bdc0","water_text":"#185869","land_night":"#017293","water_night":"#103","water_dark_night":"#003366","crop_night":"#178d96","grass_night":"#23948a","scrub_night":"#31a186","wood_night":"#45b581","park_night":"#51bd8b","snow_night":"#5ad9fe","rock_night":"#999","sand_night":"#437162","cemetery_night":"#218c96","pitch_night":"rgba(255,255,255,0.2)","school_night":"#01536a","hospital_night":"#015e7a","builtup_night":"#014b60","admin_night":"#ffb680","text_night":"#fff","text_water_night":"#2a5b8a","text_stroke_night":"#103","text2_stroke_night":"rgba(1,69,89,0.8)","case_night":"#015e7a","street_case_night":"#015b76","motorway_night":"#bbdde7","main_night":"#64b2c9","street_night":"#0186ac","contour_night":"#ffff80","river_canal_width":["stops",{"z":11,"val":0.5},{"z":12,"val":1},{"z":14,"val":2},{"z":16,"val":3}],"stream_width":["stops",{"z":13,"val":0.25},{"z":14,"val":0.5},{"z":16,"val":1.5},{"z":18,"val":2}],"motorway_width":["stops",{"z":5,"val":0},{"z":6,"val":0.5},{"z":8,"val":0.8},{"z":10,"val":1},{"z":11,"val":1.2},{"z":12,"val":2},{"z":13,"val":3},{"z":14,"val":4},{"z":15,"val":6},{"z":16,"val":9},{"z":17,"val":12},{"z":18,"val":14}],"motorway_casing_width":["stops",{"z":7.5,"val":0.6},{"z":8,"val":0.8},{"z":10,"val":2.8},{"z":11,"val":3},{"z":12,"val":4},{"z":13,"val":5},{"z":14,"val":6.5},{"z":15,"val":9},{"z":16,"val":12},{"z":17,"val":15},{"z":18,"val":17}],"motorway_link_width":["stops",{"z":12,"val":1.2},{"z":14,"val":2},{"z":16,"val":3},{"z":18,"val":4}],"motorway_link_casing_width":["stops",{"z":12,"val":2.8},{"z":14,"val":3.5},{"z":16,"val":5},{"z":18,"val":6}],"main_width":["stops",{"z":5,"val":1},{"z":12,"val":1},{"z":13,"val":1.5},{"z":14,"val":2},{"z":15,"val":3},{"z":16,"val":6},{"z":17,"val":10},{"z":18,"val":12}],"main_casing_width":["stops",{"z":9,"val":2.9},{"z":12,"val":2.9},{"z":13,"val":3.5},{"z":14,"val":4},{"z":15,"val":5.5},{"z":16,"val":9},{"z":17,"val":12},{"z":18,"val":14}],"street_width":["stops",{"z":14.5,"val":0},{"z":15,"val":1.5},{"z":16,"val":3},{"z":17,"val":8}],"street_casing_width":["stops",{"z":13,"val":0.4},{"z":14,"val":1},{"z":15,"val":2.5},{"z":16,"val":4},{"z":17,"val":10}],"street_casing_opacity":["stops",{"z":14,"val":0},{"z":14.5,"val":1}],"service_casing_width":["stops",{"z":14,"val":0.5},{"z":15,"val":3},{"z":16,"val":3.5},{"z":17,"val":4},{"z":18,"val":5},{"z":19,"val":6}],"runway_width":["stops",{"z":10,"val":1},{"z":11,"val":2},{"z":12,"val":3},{"z":13,"val":5},{"z":14,"val":7},{"z":15,"val":11},{"z":16,"val":15},{"z":17,"val":19},{"z":18,"val":23}],"taxiway_width":["stops",{"z":10,"val":0.2},{"z":12,"val":0.2},{"z":13,"val":1},{"z":14,"val":1.5},{"z":15,"val":2},{"z":16,"val":3},{"z":17,"val":4},{"z":18,"val":5}],"aerialway_width":["stops",{"z":13.5,"val":0.8},{"z":14,"val":1.4},{"z":15,"val":1.6},{"z":16,"val":2},{"z":17,"val":2.4},{"z":18,"val":3}],"aerialway_casing_width":["stops",{"z":13.5,"val":2},{"z":14,"val":2.5},{"z":15,"val":3},{"z":16,"val":3.5},{"z":17,"val":4},{"z":22,"val":5}],"path_width":["stops",{"z":14,"val":1.2},{"z":15,"val":1.5},{"z":16,"val":1.8}],"admin_l2_width":["stops",{"z":2,"val":0.5},{"z":3,"val":0.7},{"z":4,"val":0.7},{"z":5,"val":0.8},{"z":6,"val":1},{"z":8,"val":2},{"z":10,"val":3}],"admin_l3_width":["stops",{"z":6,"val":0.6},{"z":8,"val":1},{"z":12,"val":2}],"road_label_1_size":["stops",{"z":13,"val":11},{"z":14,"val":12},{"z":15,"val":13},{"z":16,"val":14},{"z":17,"val":16},{"z":18,"val":18}],"road_label_2_size":["stops",{"z":13,"val":11},{"z":14,"val":12},{"z":16,"val":14},{"z":18,"val":16}],"road_label_3_size":["stops",{"z":15,"val":10},{"z":16,"val":12},{"z":18,"val":14}],"fence_width":["stops",{"z":17,"val":0.6},{"z":19,"val":1}],"hedge_width":["stops",{"z":16,"val":0.6},{"z":17,"val":1.2},{"z":19,"val":1.6}],"barrier_line_land_width":["stops",{"z":14,"val":0.4},{"z":15,"val":0.75},{"z":16,"val":1.5},{"z":17,"val":3},{"z":18,"val":6},{"z":19,"val":12},{"z":20,"val":24},{"z":21,"val":48}],"country_label_size":["stops",{"z":1,"val":14},{"z":12,"val":24}],"poi_label_1-2_size":["stops",{"z":15,"val":10},{"z":16,"val":11},{"z":17,"val":12}],"poi_label_3_size":["stops",{"z":16,"val":10},{"z":17,"val":11}],"hillshade_prerender":["stops",{"z":11,"val":0},{"z":12,"val":1}],"hillshade_prerender_size":["stops",{"z":11,"val":1056},{"z":12,"val":512},{"z":13,"val":256}]},"classes":[{"name":"default","layers":{"background":{"color":"land"},"admin_maritime_cover":{"color":"water","width":5},"admin_maritime":{"color":"#c0d6d6","width":["stops",{"z":6,"val":1},{"z":8,"val":2},{"z":12,"val":3}]},"admin_l2":{"color":"#88a","width":"admin_l2_width"},"admin_l3":{"color":"#88a","dasharray":[60,20],"opacity":["stops",{"z":4,"val":0},{"z":6,"val":1}],"width":"admin_l3_width"},"waterway_river_canal":{"color":"#87abaf","width":"river_canal_width"},"waterway_stream":{"color":"#87abaf","width":"stream_width"},"barrier_line_gate":{"width":2.5,"color":"#aab"},"barrier_line_fence":{"color":"#aeada3","width":"fence_width"},"barrier_line_hedge":{"color":"#8de99b","width":"hedge_width"},"barrier_line_land":{"color":"land","width":"barrier_line_land_width"},"barrier_line_land_fill":{"color":"land"},"barrier_line_cliff":{"color":"#987","width":4},"landcover_wood":{"color":"wood","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_scrub":{"color":"scrub","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_grass":{"color":"grass","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_crop":{"color":"crop"},"landcover_snow":{"color":"snow"},"landuse_wood":{"color":"wood"},"landuse_scrub":{"color":"scrub"},"landuse_grass":{"color":"grass"},"landuse_crop":{"color":"crop"},"landuse_snow":{"color":"snow"},"landuse_rock":{"color":"rock"},"landuse_sand":{"color":"sand"},"landuse_park":{"color":"park"},"landuse_cemetery":{"color":"cemetery"},"landuse_hospital":{"color":"hospital"},"landuse_school":{"color":"school"},"landuse_pitch":{"color":"rgba(255,255,255,0.5)","stroke":"pitch"},"landuse_industrial":{"color":"rgba(246,250,255,0.5)"},"overlay_wetland":{"color":"rgba(210,225,225,0.2)","image":"wetland_noveg_64"},"overlay_breakwater_pier":{"color":"land"},"hillshade_full_shadow":{"color":"#103","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":15,"val":0.08},{"z":16,"val":0.075},{"z":17,"val":0.05},{"z":18,"val":0.05},{"z":19,"val":0.025}]},"hillshade_medium_shadow":{"color":"#206","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":15,"val":0.08},{"z":16,"val":0.075},{"z":17,"val":0.05},{"z":18,"val":0.05},{"z":19,"val":0.025}]},"hillshade_full_highlight":{"color":"#fffff3","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":15,"val":0.3},{"z":16,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.2},{"z":19,"val":0.1}]},"hillshade_medium_highlight":{"color":"#ffd","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":15,"val":0.3},{"z":16,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.2},{"z":19,"val":0.1}]},"contour_line_loud":{"color":"#008","width":0.9,"opacity":["stops",{"z":12,"val":0.05},{"z":13,"val":0.11}]},"contour_line_regular":{"color":"#008","width":0.5,"opacity":["stops",{"z":12,"val":0.05},{"z":13,"val":0.11}]},"contour_label":{"color":"text","stroke":"land","strokeWidth":0.3,"strokeBlur":3,"size":10},"water":{"color":"water","stroke":"#a2bdc0"},"aeroway_fill":{"color":"#ddd"},"aeroway_runway":{"color":"#ddd","width":"runway_width"},"aeroway_taxiway":{"color":"#ddd","width":"taxiway_width"},"building":{"color":"#ebe7db"},"building_wall":{"color":"#ebe7db","stroke":"#d5d1c6","opacity":["stops",{"z":16.5,"val":0},{"z":17,"val":0.7}]},"building_shadow":{"color":"#d5d1c6","stroke":"#d5d1c6","translate":[1,1],"opacity":["stops",{"z":16.5,"val":0},{"z":17,"val":1}]},"tunnel_motorway_casing":{"color":"case","dasharray":[6,6],"width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"tunnel_motorway":{"color":"#e6cec7","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"tunnel_main_casing":{"color":"case","dasharray":[6,6],"width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"tunnel_main":{"color":"#e6cec7","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"tunnel_motorway_link_casing":{"color":"case","dasharray":[6,6],"width":"motorway_link_casing_width"},"tunnel_motorway_link":{"color":"#e6cec7","width":"motorway_link_width"},"tunnel_street_casing":{"color":"#d9d5c6","width":"street_casing_width","opacity":"street_casing_opacity"},"tunnel_street":{"color":"#d9d5c6","width":"street_width"},"tunnel_service_casing":{"color":"#000","opacity":0.04,"dasharray":[6,6],"width":"service_casing_width"},"tunnel_service":{"color":"#e6cec7","width":2},"road_motorway_casing":{"color":"case","width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"road_motorway":{"color":"motorway","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"road_main_casing":{"color":"case","width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"road_main":{"color":"main","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"road_motorway_link_casing":{"color":"case","width":"motorway_link_casing_width"},"road_motorway_link":{"color":"motorway","width":"motorway_link_width"},"road_street_casing":{"color":"#d9d5c6","width":"street_casing_width","opacity":"street_casing_opacity"},"road_street":{"color":"street","width":"street_width"},"road_service_casing":{"color":"#000","opacity":0.04,"width":"service_casing_width"},"road_service":{"color":"street","width":2},"road_path_case":{"color":"#ffd","opacity":0.4,"width":["stops",{"z":15,"val":3},{"z":16,"val":4}]},"road_path_footway":{"color":"#bba","dasharray":[10,4],"width":"path_width"},"road_path_path":{"color":"#987","dasharray":[10,4],"opacity":0.8,"width":["stops",{"z":14,"val":0.8},{"z":15,"val":0.9},{"z":16,"val":1.2}]},"road_path_cycleway":{"color":"#488","dasharray":[10,4],"width":"path_width"},"road_path_mtb":{"color":"#488","dasharray":[12,4],"width":"path_width"},"road_path_piste":{"color":"#87b","dasharray":[8,4],"width":"path_width"},"road_path_steps":{"color":"#bba","dasharray":[10,4],"width":4},"road_major_rail":{"color":"#c8c4c0","width":0.8},"road_major_rail_hatching":{"color":"#c8c4c0","dasharray":[2,31],"width":5},"bridge_motorway_casing":{"color":"case","width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"bridge_motorway":{"color":"motorway","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"bridge_main_casing":{"color":"case","width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"bridge_main":{"color":"main","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"bridge_motorway_link_casing":{"color":"case","width":"motorway_link_casing_width"},"bridge_motorway_link":{"color":"motorway","width":"motorway_link_width"},"bridge_street_casing":{"color":"#d9d5c6","width":"street_casing_width","opacity":"street_casing_opacity"},"bridge_street":{"color":"street","width":"street_width"},"bridge_service_casing":{"color":"#000","opacity":0.04,"width":"service_casing_width"},"bridge_service":{"color":"street","width":2},"bridge_aerialway_casing":{"color":"white","opacity":0.5,"width":"aerialway_casing_width"},"bridge_aerialway":{"color":"#876","opacity":0.5,"width":"aerialway_width"},"country_label":{"color":"country_text","stroke":"rgba(255,255,255,0.5)","strokeWidth":0.5,"size":"country_label_size"},"country_label_line":{"color":"country_text","width":0.5,"opacity":0.5},"marine_label_line_1":{"color":"marine_text","size":["stops",{"z":3,"val":20},{"z":4,"val":25},{"z":5,"val":30},{"z":22,"val":30}],"stroke":"water"},"marine_label_line_2":{"color":"marine_text","size":["stops",{"z":3,"val":13},{"z":4,"val":14},{"z":5,"val":20},{"z":6,"val":24},{"z":22,"val":24}],"stroke":"water"},"marine_label_line_3":{"color":"marine_text","size":["stops",{"z":3,"val":12},{"z":4,"val":13},{"z":5,"val":15},{"z":6,"val":18},{"z":22,"val":18}],"stroke":"water"},"marine_label_line_other":{"color":"marine_text","size":["stops",{"z":4,"val":12},{"z":5,"val":14},{"z":6,"val":16},{"z":22,"val":16}],"stroke":"water"},"marine_label_point_1":{"color":"marine_text","size":["stops",{"z":3,"val":20},{"z":4,"val":25},{"z":5,"val":30},{"z":22,"val":30}],"stroke":"water"},"marine_label_point_2":{"color":"marine_text","size":["stops",{"z":3,"val":13},{"z":4,"val":14},{"z":5,"val":20},{"z":6,"val":24},{"z":22,"val":24}],"stroke":"water"},"marine_label_point_3":{"color":"marine_text","size":["stops",{"z":3,"val":12},{"z":4,"val":13},{"z":5,"val":15},{"z":6,"val":18},{"z":22,"val":18}],"stroke":"water"},"marine_label_point_other":{"color":"marine_text","size":["stops",{"z":4,"val":12},{"z":5,"val":14},{"z":6,"val":16},{"z":22,"val":16}],"stroke":"water"},"state_label":{"color":"#333","strokeWidth":0.4,"strokeBlur":1,"stroke":"rgba(244,239,225,0.8)","size":["stops",{"z":3.99,"val":0},{"z":4,"val":10},{"z":9.99,"val":16},{"z":10,"val":0}]},"place_label_city":{"color":"#444","strokeWidth":0.4,"stroke":"text_stroke","size":["stops",{"z":3.99,"val":0},{"z":4,"val":10},{"z":7,"val":14},{"z":14.99,"val":20},{"z":15,"val":0}]},"place_label_town":{"color":"#716656","strokeWidth":0.3,"strokeBlur":2,"stroke":"text_stroke","size":["stops",{"z":9,"val":10},{"z":12,"val":13},{"z":14,"val":17},{"z":16,"val":22}]},"place_label_village":{"color":"#635644","strokeWidth":0.3,"strokeBlur":2,"stroke":"text_stroke","size":["stops",{"z":9,"val":8},{"z":12,"val":10},{"z":14,"val":14},{"z":16,"val":16},{"z":17,"val":20}]},"place_label_other":{"color":"#7d6c55","stroke":"text_stroke","size":["stops",{"z":13,"val":11},{"z":14,"val":12},{"z":16,"val":14},{"z":18,"val":18}]},"road_label_1":{"color":"#585042","stroke":"land","strokeWidth":0.6,"strokeBlur":2,"size":"road_label_1_size"},"road_label_2":{"color":"#585042","stroke":"land","strokeWidth":0.6,"strokeBlur":2,"size":"road_label_2_size"},"road_label_3":{"color":"#585042","stroke":"land","strokeWidth":0.6,"strokeBlur":2,"size":"road_label_3_size"},"water_label":{"color":"water_dark","stroke":"rgba(255,255,255,0.75)"},"waterway_label":{"color":"water_dark","strokeWidth":0.4,"strokeBlur":2,"stroke":"text_stroke"},"poi":{"antialias":false},"poi_3":{"antialias":false,"opacity":["stops",{"z":17.5,"val":0},{"z":17.75,"val":1}]},"poi_4_4":{"antialias":false,"opacity":["stops",{"z":18,"val":0},{"z":18.25,"val":1}]},"poi_4_16":{"antialias":false,"opacity":["stops",{"z":18.5,"val":0},{"z":18.75,"val":1}]},"poi_4_all":{"antialias":false,"opacity":["stops",{"z":18.75,"val":0},{"z":19,"val":1}]},"poi_label_1-2":{"color":"#444","size":"poi_label_1-2_size","stroke":"land","strokeWidth":0.3,"strokeBlur":1},"poi_label_3":{"color":"#444","size":"poi_label_3_size","stroke":"land","strokeWidth":0.3,"strokeBlur":1,"opacity":["stops",{"z":17.5,"val":0},{"z":17.75,"val":1}]},"poi_label_4_4":{"color":"#444","size":10,"opacity":["stops",{"z":18,"val":0},{"z":17.25,"val":1}],"stroke":"land","strokeWidth":0.3,"strokeBlur":1},"poi_label_4_16":{"color":"#444","size":10,"opacity":["stops",{"z":18.25,"val":0},{"z":18.75,"val":1}],"stroke":"land","strokeWidth":0.3,"strokeBlur":1},"poi_label_4_all":{"color":"#444","size":10,"opacity":["stops",{"z":18.75,"val":0},{"z":19,"val":1}],"stroke":"land","strokeWidth":0.3,"strokeBlur":1},"poi_aerodrome":{"opacity":["stops",{"z":13,"val":0},{"z":13.25,"val":1}],"antialias":false}}},{"name":"night","layers":{"background":{"color":"land_night"},"admin_maritime_cover":{"color":"water_night","width":5},"admin_maritime":{"color":"#0a1347","width":["stops",{"z":6,"val":1},{"z":8,"val":2},{"z":12,"val":3}]},"admin_l2":{"color":"admin_night","width":"admin_l2_width"},"admin_l3":{"color":"admin_night","dasharray":[60,20],"opacity":["stops",{"z":4,"val":0},{"z":6,"val":1}],"width":"admin_l3_width"},"waterway_river_canal":{"color":"rgb(10,20,71)","width":"river_canal_width"},"waterway_stream":{"color":"rgb(10,20,71)","width":"stream_width"},"barrier_line_gate":{"width":2.5,"color":"#59596f"},"barrier_line_fence":{"color":"#014b61","width":"fence_width"},"barrier_line_hedge":{"color":"#2e7a57","width":"hedge_width"},"barrier_line_land":{"color":"land_night","width":"barrier_line_land_width"},"barrier_line_land_fill":{"color":"land_night"},"barrier_line_cliff":{"color":"#63574b","width":4},"landcover_wood":{"color":"wood_night","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_scrub":{"color":"scrub_night","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_grass":{"color":"grass_night","opacity":["stops",{"z":13,"val":1},{"z":14,"val":0.8},{"z":17,"val":0.2}]},"landcover_crop":{"color":"crop_night"},"landcover_snow":{"color":"snow_night"},"landuse_wood":{"color":"wood_night","opacity":0.8},"landuse_scrub":{"color":"scrub_night","opacity":0.8},"landuse_grass":{"color":"grass_night","opacity":0.8},"landuse_crop":{"color":"crop_night","opacity":0.8},"landuse_snow":{"color":"snow_night","opacity":0.8},"landuse_rock":{"color":"rock_night","opacity":0.8},"landuse_sand":{"color":"sand_night","opacity":0.8},"landuse_park":{"color":"park_night"},"landuse_cemetery":{"color":"cemetery_night"},"landuse_hospital":{"color":"hospital_night"},"landuse_school":{"color":"school_night"},"landuse_pitch":{"color":"pitch_night","stroke":"pitch"},"landuse_industrial":{"color":"builtup_night"},"overlay_wetland":{"color":"rgba(210,225,225,0.2)","image":"wetland_noveg_64"},"overlay_breakwater_pier":{"color":"land_night"},"hillshade_full_shadow":{"color":"#103","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":16,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.1},{"z":19,"val":0.05}]},"hillshade_medium_shadow":{"color":"#206","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":16,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.1},{"z":19,"val":0.05}]},"hillshade_full_highlight":{"color":"#fdfdad","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":14,"val":0.4},{"z":15,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.1},{"z":19,"val":0.05}]},"hillshade_medium_highlight":{"color":"#ffe1b7","antialias":false,"prerender":"hillshade_prerender","prerender-size":"hillshade_prerender_size","prerender-blur":1,"opacity":["stops",{"z":15,"val":0.3},{"z":17,"val":0.2},{"z":18,"val":0.15},{"z":19,"val":0.05}]},"contour_line_loud":{"color":"contour_night","width":0.9,"opacity":["stops",{"z":12,"val":0.1},{"z":13,"val":0.2}]},"contour_line_regular":{"color":"contour_night","width":0.5,"opacity":["stops",{"z":12,"val":0.1},{"z":13,"val":0.4}]},"contour_label":{"color":"contour_night","stroke":"land_night","strokeWidth":0.3,"strokeBlur":3,"size":10},"water":{"color":"water_night","stroke":"water_dark_night"},"aeroway_fill":{"color":"#367"},"aeroway_runway":{"color":"#367","width":"runway_width"},"aeroway_taxiway":{"color":"#367","width":"taxiway_width"},"building":{"color":"#027797"},"building_wall":{"color":"#027797","stroke":"#026688","opacity":["stops",{"z":16.5,"val":0},{"z":17,"val":0.7}]},"building_shadow":{"color":"#026688","stroke":"#026688","translate":[1,1],"opacity":["stops",{"z":16.5,"val":0},{"z":17,"val":1}]},"tunnel_motorway_casing":{"color":"case_night","dasharray":[6,6],"width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"tunnel_motorway":{"color":"#78b0c1","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"tunnel_main_casing":{"color":"case_night","dasharray":[6,6],"width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"tunnel_main":{"color":"#78b0c1","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"tunnel_motorway_link_casing":{"color":"case_night","dasharray":[6,6],"width":"motorway_link_casing_width"},"tunnel_motorway_link":{"color":"#78b0c1","width":"motorway_link_width"},"tunnel_street_casing":{"color":"street_case_night","width":"street_casing_width","opacity":"street_casing_opacity"},"tunnel_street":{"color":"street_night","width":"street_width"},"tunnel_service_casing":{"color":"#000","opacity":0.04,"dasharray":[6,6],"width":"service_casing_width"},"tunnel_service":{"color":"#017ca0","width":2},"road_motorway_casing":{"color":"case_night","width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"road_motorway":{"color":"motorway_night","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"road_main_casing":{"color":"case_night","width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"road_main":{"color":"main_night","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"road_motorway_link_casing":{"color":"case_night","width":"motorway_link_casing_width"},"road_motorway_link":{"color":"motorway_night","width":"motorway_link_width"},"road_street_casing":{"color":"street_case_night","width":"street_casing_width","opacity":"street_casing_opacity"},"road_street":{"color":"street_night","width":"street_width"},"road_service_casing":{"color":"#000","opacity":0.04,"width":"service_casing_width"},"road_service":{"color":"street_night","width":2},"road_path_case":{"color":"land_night","opacity":0.2},"road_path_footway":{"color":"#fff","dasharray":[10,4],"width":"path_width"},"road_path_path":{"color":"#fff","dasharray":[10,4],"opacity":0.8,"width":["stops",{"z":14,"val":0.8},{"z":15,"val":0.9},{"z":16,"val":1.2}]},"road_path_cycleway":{"color":"#94e6ff","dasharray":[10,4],"width":"path_width"},"road_path_mtb":{"color":"#94e6ff","dasharray":[12,4],"width":"path_width"},"road_path_piste":{"color":"#715dae","dasharray":[8,4],"width":"path_width"},"road_path_steps":{"color":"#016684","dasharray":[10,4],"opacity":0.3,"width":6},"road_major_rail":{"color":"#c8c4c0","width":0.8},"road_major_rail_hatching":{"color":"#c8c4c0","dasharray":[2,31],"width":5},"bridge_motorway_casing":{"color":"case_night","width":"motorway_casing_width","opacity":["stops",{"z":9.5,"val":0},{"z":10,"val":1}]},"bridge_motorway":{"color":"motorway_night","width":"motorway_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"bridge_main_casing":{"color":"case_night","width":"main_casing_width","opacity":["stops",{"z":9,"val":0},{"z":10,"val":1}]},"bridge_main":{"color":"main_night","width":"main_width","opacity":["stops",{"z":6.5,"val":0},{"z":7,"val":1}]},"bridge_motorway_link_casing":{"color":"case_night","width":"motorway_link_casing_width"},"bridge_motorway_link":{"color":"motorway_night","width":"motorway_link_width"},"bridge_street_casing":{"color":"street_case_night","width":"street_casing_width","opacity":"street_casing_opacity"},"bridge_street":{"color":"street_night","width":"street_width"},"bridge_service_casing":{"color":"#000","opacity":0.04,"width":"service_casing_width"},"bridge_service":{"color":"street_night","width":2},"bridge_aerialway_casing":{"color":"white","opacity":0.5,"width":"aerialway_casing_width"},"bridge_aerialway":{"color":"#876","opacity":0.5,"width":"aerialway_width"},"country_label":{"color":"text_night","stroke":"text2_stroke_night","strokeWidth":0.4,"strokeBlur":2,"size":"country_label_size"},"country_label_line":{"color":"text_night","width":0.5,"opacity":0.5},"marine_label_line_1":{"color":"water_dark_night","size":["stops",{"z":3,"val":20},{"z":4,"val":25},{"z":5,"val":30},{"z":22,"val":30}],"stroke":"water_night"},"marine_label_line_2":{"color":"water_dark_night","size":["stops",{"z":3,"val":13},{"z":4,"val":14},{"z":5,"val":20},{"z":6,"val":24},{"z":22,"val":24}],"stroke":"water_night"},"marine_label_line_3":{"color":"water_dark_night","size":["stops",{"z":3,"val":12},{"z":4,"val":13},{"z":5,"val":15},{"z":6,"val":18},{"z":22,"val":18}],"stroke":"water_night"},"marine_label_line_other":{"color":"water_dark_night","size":["stops",{"z":4,"val":12},{"z":5,"val":14},{"z":6,"val":16},{"z":22,"val":16}],"stroke":"water_night"},"marine_label_point_1":{"color":"water_dark_night","size":["stops",{"z":3,"val":20},{"z":4,"val":25},{"z":5,"val":30},{"z":22,"val":30}],"stroke":"water_night"},"marine_label_point_2":{"color":"water_dark_night","size":["stops",{"z":3,"val":13},{"z":4,"val":14},{"z":5,"val":20},{"z":6,"val":24},{"z":22,"val":24}],"stroke":"water_night"},"marine_label_point_3":{"color":"water_dark_night","size":["stops",{"z":3,"val":12},{"z":4,"val":13},{"z":5,"val":15},{"z":6,"val":18},{"z":22,"val":18}],"stroke":"water_night"},"marine_label_point_other":{"color":"water_dark_night","size":["stops",{"z":4,"val":12},{"z":5,"val":14},{"z":6,"val":16},{"z":22,"val":16}],"stroke":"water_night"},"state_label":{"color":"#fff","strokeWidth":0.4,"strokeBlur":1,"stroke":"land_night","size":["stops",{"z":3.99,"val":0},{"z":4,"val":10},{"z":9.99,"val":16},{"z":10,"val":0}]},"place_label_city":{"color":"#fff","strokeWidth":0.4,"stroke":"text2_stroke_night","size":["stops",{"z":3.99,"val":0},{"z":4,"val":10},{"z":7,"val":14},{"z":14.99,"val":20},{"z":15,"val":0}]},"place_label_town":{"color":"text_night","strokeWidth":0.3,"strokeBlur":2,"stroke":"text2_stroke_night","size":["stops",{"z":9,"val":10},{"z":12,"val":13},{"z":14,"val":17},{"z":16,"val":22}]},"place_label_village":{"color":"text_night","strokeWidth":0.3,"strokeBlur":2,"stroke":"text2_stroke_night","size":["stops",{"z":9,"val":8},{"z":12,"val":10},{"z":14,"val":14},{"z":16,"val":16},{"z":17,"val":20}]},"place_label_other":{"color":"text_night","stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":2,"size":["stops",{"z":13,"val":11},{"z":14,"val":12},{"z":16,"val":14},{"z":18,"val":18}]},"road_label_1":{"color":"text_night","stroke":"text2_stroke_night","strokeWidth":0.5,"strokeBlur":3,"size":"road_label_1_size"},"road_label_2":{"color":"text_night","stroke":"text2_stroke_night","strokeWidth":0.5,"strokeBlur":3,"size":"road_label_2_size"},"road_label_3":{"color":"text_night","stroke":"text2_stroke_night","strokeWidth":0.5,"strokeBlur":3,"size":"road_label_3_size"},"water_label":{"color":"text_water_night","stroke":"water_night"},"waterway_label":{"color":"text_water_night","stroke":"water_night"},"poi":{"color":"white","antialias":false},"poi_3":{"antialias":false,"opacity":["stops",{"z":17.5,"val":0},{"z":17.75,"val":1}]},"poi_4_4":{"antialias":false,"opacity":["stops",{"z":18,"val":0},{"z":17.25,"val":1}]},"poi_4_16":{"antialias":false,"opacity":["stops",{"z":18.5,"val":0},{"z":18.75,"val":1}]},"poi_4_all":{"antialias":false,"opacity":["stops",{"z":18.75,"val":0},{"z":19,"val":1}]},"poi_label_1-2":{"color":"#fff","size":"poi_label_1-2_size","stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":1},"poi_label_3":{"color":"#fff","size":"poi_label_3_size","stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":1,"opacity":["stops",{"z":17.5,"val":0},{"z":17.75,"val":1}]},"poi_label_4_4":{"color":"#fff","size":10,"opacity":["stops",{"z":18,"val":0},{"z":18.25,"val":1}],"stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":1},"poi_label_4_16":{"color":"#fff","size":10,"opacity":["stops",{"z":18.5,"val":0},{"z":18.75,"val":1}],"stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":1},"poi_label_4_all":{"color":"#fff","size":10,"opacity":["stops",{"z":18.75,"val":0},{"z":19,"val":1}],"stroke":"text2_stroke_night","strokeWidth":0.3,"strokeBlur":1},"poi_aerodrome":{"opacity":["stops",{"z":13,"val":0},{"z":13.25,"val":1}],"antialias":false}}}],"sprite":"/img/maki-sprite"}
\ No newline at end of file
diff --git a/android/java/lib/src/main/res/values/attrs.xml b/android/java/lib/src/main/res/values/attrs.xml
new file mode 100644
index 00000000000..b42f729c929
--- /dev/null
+++ b/android/java/lib/src/main/res/values/attrs.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/java/lib/src/main/res/values/strings.xml b/android/java/lib/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..f11f7450a82
--- /dev/null
+++ b/android/java/lib/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/android/java/settings.gradle b/android/java/settings.gradle
new file mode 100644
index 00000000000..0959cbb33ec
--- /dev/null
+++ b/android/java/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+include ':lib'