diff --git a/OsmAndCore-sample/.gitignore b/OsmAndCore-sample/.gitignore
new file mode 100644
index 0000000000..9d3ed1dd63
--- /dev/null
+++ b/OsmAndCore-sample/.gitignore
@@ -0,0 +1,13 @@
+# Android Studio
+/.idea
+*.iml
+
+# Gradle
+.gradle
+/local.properties
+
+# MacOSX
+.DS_Store
+
+# Output
+/build
diff --git a/OsmAndCore-sample/AndroidManifest.xml b/OsmAndCore-sample/AndroidManifest.xml
new file mode 100644
index 0000000000..da3ce99223
--- /dev/null
+++ b/OsmAndCore-sample/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OsmAndCore-sample/build.gradle b/OsmAndCore-sample/build.gradle
new file mode 100644
index 0000000000..8e537e3355
--- /dev/null
+++ b/OsmAndCore-sample/build.gradle
@@ -0,0 +1,92 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.3"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ // This is from OsmAndCore_android.aar - for some reason it's not inherited
+ aaptOptions {
+ // Don't compress any embedded resources
+ noCompress "qz"
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile "AndroidManifest.xml"
+ jni.srcDirs = []
+ jniLibs.srcDirs = ["libs"]
+ java.srcDirs = ["src"]
+ resources.srcDirs = ["src"]
+ renderscript.srcDirs = ["src"]
+ res.srcDirs = ["res"]
+ assets.srcDirs = ["assets"]
+ }
+ }
+
+ productFlavors {
+ x86 {
+ ndk {
+ abiFilter "x86"
+ }
+ }
+ mips {
+ ndk {
+ abiFilter "mips"
+ }
+ }
+ armv7 {
+ ndk {
+ abiFilter "armeabi-v7a"
+ }
+ }
+ armv5 {
+ ndk {
+ abiFilter "armeabi"
+ }
+ }
+ fat
+ }
+
+ buildTypes {
+ debug {
+ signingConfig android.signingConfigs.debug
+ }
+ nativeDebug {
+ signingConfig android.signingConfigs.debug
+ }
+ release {
+ signingConfig android.signingConfigs.debug
+ }
+ }
+}
+
+repositories {
+ ivy {
+ name = "OsmAndBinariesIvy"
+ url = "http://builder.osmand.net"
+ layout "pattern" , {
+ artifact "ivy/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"
+ }
+ }
+}
+
+dependencies {
+ compile project(path: ':OsmAnd-java', configuration: 'android')
+ compile 'com.android.support:appcompat-v7:23.3.0'
+ compile fileTree(dir: "libs", include: ["*.jar"])
+ compile "net.osmand:OsmAndCore_android:0.1-SNAPSHOT@aar"
+ debugCompile "net.osmand:OsmAndCore_androidNativeRelease:0.1-SNAPSHOT@aar"
+ nativeDebugCompile "net.osmand:OsmAndCore_androidNativeDebug:0.1-SNAPSHOT@aar"
+ releaseCompile "net.osmand:OsmAndCore_androidNativeRelease:0.1-SNAPSHOT@aar"
+}
diff --git a/OsmAndCore-sample/res/drawable-hdpi/bg_contextmenu_shadow.9.png b/OsmAndCore-sample/res/drawable-hdpi/bg_contextmenu_shadow.9.png
new file mode 100644
index 0000000000..0356c4a541
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/bg_contextmenu_shadow.9.png differ
diff --git a/OsmAndCore-sample/res/drawable-hdpi/ic_action_remove_dark.png b/OsmAndCore-sample/res/drawable-hdpi/ic_action_remove_dark.png
new file mode 100644
index 0000000000..718053c411
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/ic_action_remove_dark.png differ
diff --git a/OsmAndCore-sample/res/drawable-hdpi/map_bt_round_1_shadow.png b/OsmAndCore-sample/res/drawable-hdpi/map_bt_round_1_shadow.png
new file mode 100644
index 0000000000..159d4c08b5
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/map_bt_round_1_shadow.png differ
diff --git a/OsmAndCore-sample/res/drawable-hdpi/map_compass_niu.png b/OsmAndCore-sample/res/drawable-hdpi/map_compass_niu.png
new file mode 100644
index 0000000000..3ae3157990
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/map_compass_niu.png differ
diff --git a/OsmAndCore-sample/res/drawable-hdpi/map_zoom_in.png b/OsmAndCore-sample/res/drawable-hdpi/map_zoom_in.png
new file mode 100644
index 0000000000..d37a733a9b
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/map_zoom_in.png differ
diff --git a/OsmAndCore-sample/res/drawable-hdpi/map_zoom_out.png b/OsmAndCore-sample/res/drawable-hdpi/map_zoom_out.png
new file mode 100644
index 0000000000..36a88d3536
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-hdpi/map_zoom_out.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/bg_contextmenu_shadow.9.png b/OsmAndCore-sample/res/drawable-mdpi/bg_contextmenu_shadow.9.png
new file mode 100644
index 0000000000..f5984c27ce
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/bg_contextmenu_shadow.9.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/ic_action_remove_dark.png b/OsmAndCore-sample/res/drawable-mdpi/ic_action_remove_dark.png
new file mode 100644
index 0000000000..939f58b193
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/ic_action_remove_dark.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/map_bt_round_1_shadow.png b/OsmAndCore-sample/res/drawable-mdpi/map_bt_round_1_shadow.png
new file mode 100644
index 0000000000..586ba999a4
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/map_bt_round_1_shadow.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/map_compass_niu.png b/OsmAndCore-sample/res/drawable-mdpi/map_compass_niu.png
new file mode 100644
index 0000000000..5207161498
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/map_compass_niu.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/map_zoom_in.png b/OsmAndCore-sample/res/drawable-mdpi/map_zoom_in.png
new file mode 100644
index 0000000000..938a4d4805
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/map_zoom_in.png differ
diff --git a/OsmAndCore-sample/res/drawable-mdpi/map_zoom_out.png b/OsmAndCore-sample/res/drawable-mdpi/map_zoom_out.png
new file mode 100644
index 0000000000..2051dbd1a6
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-mdpi/map_zoom_out.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/bg_contextmenu_shadow.9.png b/OsmAndCore-sample/res/drawable-xhdpi/bg_contextmenu_shadow.9.png
new file mode 100644
index 0000000000..b40ce9b5b7
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/bg_contextmenu_shadow.9.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/ic_action_remove_dark.png b/OsmAndCore-sample/res/drawable-xhdpi/ic_action_remove_dark.png
new file mode 100644
index 0000000000..7f2de6194e
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/ic_action_remove_dark.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/map_bt_round_1_shadow.png b/OsmAndCore-sample/res/drawable-xhdpi/map_bt_round_1_shadow.png
new file mode 100644
index 0000000000..2459bd66ee
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/map_bt_round_1_shadow.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/map_compass_niu.png b/OsmAndCore-sample/res/drawable-xhdpi/map_compass_niu.png
new file mode 100644
index 0000000000..05f8351357
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/map_compass_niu.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_in.png b/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_in.png
new file mode 100644
index 0000000000..eeb0b1a9cc
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_in.png differ
diff --git a/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_out.png b/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_out.png
new file mode 100644
index 0000000000..8e62ba2a84
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xhdpi/map_zoom_out.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/bg_contextmenu_shadow.9.png b/OsmAndCore-sample/res/drawable-xxhdpi/bg_contextmenu_shadow.9.png
new file mode 100644
index 0000000000..89b8ffc35e
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/bg_contextmenu_shadow.9.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/ic_action_remove_dark.png b/OsmAndCore-sample/res/drawable-xxhdpi/ic_action_remove_dark.png
new file mode 100644
index 0000000000..ea01fe05e2
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/ic_action_remove_dark.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/map_bt_round_1_shadow.png b/OsmAndCore-sample/res/drawable-xxhdpi/map_bt_round_1_shadow.png
new file mode 100644
index 0000000000..04833b06d2
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/map_bt_round_1_shadow.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/map_compass_niu.png b/OsmAndCore-sample/res/drawable-xxhdpi/map_compass_niu.png
new file mode 100644
index 0000000000..1e039c7f47
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/map_compass_niu.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_in.png b/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_in.png
new file mode 100644
index 0000000000..363037676d
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_in.png differ
diff --git a/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_out.png b/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_out.png
new file mode 100644
index 0000000000..10f88a6623
Binary files /dev/null and b/OsmAndCore-sample/res/drawable-xxhdpi/map_zoom_out.png differ
diff --git a/OsmAndCore-sample/res/drawable/btn_circle.xml b/OsmAndCore-sample/res/drawable/btn_circle.xml
new file mode 100644
index 0000000000..96603af448
--- /dev/null
+++ b/OsmAndCore-sample/res/drawable/btn_circle.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/OsmAndCore-sample/res/drawable/btn_circle_n.xml b/OsmAndCore-sample/res/drawable/btn_circle_n.xml
new file mode 100644
index 0000000000..dfbe7a2326
--- /dev/null
+++ b/OsmAndCore-sample/res/drawable/btn_circle_n.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OsmAndCore-sample/res/drawable/btn_circle_p.xml b/OsmAndCore-sample/res/drawable/btn_circle_p.xml
new file mode 100644
index 0000000000..9574b2cbd5
--- /dev/null
+++ b/OsmAndCore-sample/res/drawable/btn_circle_p.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OsmAndCore-sample/res/layout/activity_main.xml b/OsmAndCore-sample/res/layout/activity_main.xml
new file mode 100755
index 0000000000..5fb4343512
--- /dev/null
+++ b/OsmAndCore-sample/res/layout/activity_main.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OsmAndCore-sample/res/layout/search_list_item.xml b/OsmAndCore-sample/res/layout/search_list_item.xml
new file mode 100644
index 0000000000..81d63628f9
--- /dev/null
+++ b/OsmAndCore-sample/res/layout/search_list_item.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OsmAndCore-sample/res/mipmap-hdpi/sample_app.png b/OsmAndCore-sample/res/mipmap-hdpi/sample_app.png
new file mode 100755
index 0000000000..80663eef15
Binary files /dev/null and b/OsmAndCore-sample/res/mipmap-hdpi/sample_app.png differ
diff --git a/OsmAndCore-sample/res/mipmap-mdpi/sample_app.png b/OsmAndCore-sample/res/mipmap-mdpi/sample_app.png
new file mode 100755
index 0000000000..0d1df64202
Binary files /dev/null and b/OsmAndCore-sample/res/mipmap-mdpi/sample_app.png differ
diff --git a/OsmAndCore-sample/res/mipmap-xhdpi/sample_app.png b/OsmAndCore-sample/res/mipmap-xhdpi/sample_app.png
new file mode 100755
index 0000000000..614165ae18
Binary files /dev/null and b/OsmAndCore-sample/res/mipmap-xhdpi/sample_app.png differ
diff --git a/OsmAndCore-sample/res/mipmap-xxhdpi/sample_app.png b/OsmAndCore-sample/res/mipmap-xxhdpi/sample_app.png
new file mode 100755
index 0000000000..21e771e448
Binary files /dev/null and b/OsmAndCore-sample/res/mipmap-xxhdpi/sample_app.png differ
diff --git a/OsmAndCore-sample/res/mipmap-xxxhdpi/sample_app.png b/OsmAndCore-sample/res/mipmap-xxxhdpi/sample_app.png
new file mode 100755
index 0000000000..20e83f11d8
Binary files /dev/null and b/OsmAndCore-sample/res/mipmap-xxxhdpi/sample_app.png differ
diff --git a/OsmAndCore-sample/res/values-w820dp/dimens.xml b/OsmAndCore-sample/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000000..63fc816444
--- /dev/null
+++ b/OsmAndCore-sample/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/OsmAndCore-sample/res/values/colors.xml b/OsmAndCore-sample/res/values/colors.xml
new file mode 100644
index 0000000000..695c2bbee3
--- /dev/null
+++ b/OsmAndCore-sample/res/values/colors.xml
@@ -0,0 +1,17 @@
+
+
+ #ff8f00
+ #e68200
+ #FF4081
+
+ #F0F0F0
+ #EAEAEA
+ #FFF
+ #727272
+ #2f7af5
+
+ #EAEAEA
+ #FFF
+ #000
+
+
diff --git a/OsmAndCore-sample/res/values/dimens.xml b/OsmAndCore-sample/res/values/dimens.xml
new file mode 100644
index 0000000000..91dec05887
--- /dev/null
+++ b/OsmAndCore-sample/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 16dp
+ 16dp
+ 16dp
+
diff --git a/OsmAndCore-sample/res/values/strings.xml b/OsmAndCore-sample/res/values/strings.xml
new file mode 100644
index 0000000000..0586d71b0e
--- /dev/null
+++ b/OsmAndCore-sample/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+ OsmAnd Core API sample 1 for Android OS
+ OsmAnd Core Sample 1
+ Zoom in
+ Zoom out
+ Azimuth to North
+ Type and Search
+ Close
+
diff --git a/OsmAndCore-sample/res/values/styles.xml b/OsmAndCore-sample/res/values/styles.xml
new file mode 100644
index 0000000000..26dd38c319
--- /dev/null
+++ b/OsmAndCore-sample/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MainActivity.java b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MainActivity.java
new file mode 100644
index 0000000000..a845426429
--- /dev/null
+++ b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MainActivity.java
@@ -0,0 +1,546 @@
+package net.osmand.core.samples.android.sample1;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorListener;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import net.osmand.core.android.AtlasMapRendererView;
+import net.osmand.core.android.CoreResourcesFromAndroidAssets;
+import net.osmand.core.android.NativeCore;
+import net.osmand.core.jni.AreaI;
+import net.osmand.core.jni.IMapLayerProvider;
+import net.osmand.core.jni.IMapStylesCollection;
+import net.osmand.core.jni.LatLon;
+import net.osmand.core.jni.LogSeverityLevel;
+import net.osmand.core.jni.Logger;
+import net.osmand.core.jni.MapObjectsSymbolsProvider;
+import net.osmand.core.jni.MapPresentationEnvironment;
+import net.osmand.core.jni.MapPrimitivesProvider;
+import net.osmand.core.jni.MapPrimitiviser;
+import net.osmand.core.jni.MapRasterLayerProvider_Software;
+import net.osmand.core.jni.MapStylesCollection;
+import net.osmand.core.jni.ObfMapObjectsProvider;
+import net.osmand.core.jni.ObfsCollection;
+import net.osmand.core.jni.PointI;
+import net.osmand.core.jni.QIODeviceLogSink;
+import net.osmand.core.jni.ResolvedMapStyle;
+import net.osmand.core.jni.Utilities;
+import net.osmand.core.samples.android.sample1.MultiTouchSupport.MultiTouchZoomListener;
+import net.osmand.core.samples.android.sample1.SearchAPI.SearchAPICallback;
+import net.osmand.core.samples.android.sample1.SearchAPI.SearchItem;
+import net.osmand.core.samples.android.sample1.SearchUIHelper.SearchListAdapter;
+import net.osmand.core.samples.android.sample1.SearchUIHelper.SearchRow;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class MainActivity extends Activity {
+ private static final String TAG = "OsmAndCoreSample";
+
+ private float displayDensityFactor;
+ private int referenceTileSize;
+ private int rasterTileSize;
+ private IMapStylesCollection mapStylesCollection;
+ private ResolvedMapStyle mapStyle;
+ private ObfsCollection obfsCollection;
+ private MapPresentationEnvironment mapPresentationEnvironment;
+ private MapPrimitiviser mapPrimitiviser;
+ private ObfMapObjectsProvider obfMapObjectsProvider;
+ private MapPrimitivesProvider mapPrimitivesProvider;
+ private MapObjectsSymbolsProvider mapObjectsSymbolsProvider;
+ private IMapLayerProvider mapLayerProvider0;
+ private IMapLayerProvider mapLayerProvider1;
+ private QIODeviceLogSink fileLogSink;
+
+ private AtlasMapRendererView mapView;
+ private TextView textZoom;
+ private ImageButton azimuthNorthButton;
+
+ private GestureDetector gestureDetector;
+ private PointI target31;
+ private float zoom;
+ private float azimuth;
+ private float elevationAngle;
+ private MultiTouchSupport multiTouchSupport;
+
+ private SearchAPI searchAPI;
+ private ListView searchListView;
+ private SearchListAdapter adapter;
+ private final static int MAX_SEARCH_RESULTS = 50;
+
+ // Germany
+ private final static float INIT_LAT = 49.353953f;
+ private final static float INIT_LON = 11.214384f;
+ // Kyiv
+ //private final static float INIT_LAT = 50.450117f;
+ //private final static float INIT_LON = 30.524142f;
+ private final static float INIT_ZOOM = 6.0f;
+ private final static float INIT_AZIMUTH = 0.0f;
+ private final static float INIT_ELEVATION_ANGLE = 90.0f;
+ private final static int MIN_ZOOM_LEVEL = 2;
+ private final static int MAX_ZOOM_LEVEL = 22;
+
+ private static final String PREF_MAP_CENTER_LAT = "MAP_CENTER_LAT";
+ private static final String PREF_MAP_CENTER_LON = "MAP_CENTER_LON";
+ private static final String PREF_MAP_AZIMUTH = "MAP_AZIMUTH";
+ private static final String PREF_MAP_ZOOM = "MAP_ZOOM";
+ private static final String PREF_MAP_ELEVATION_ANGLE = "MAP_ELEVATION_ANGLE";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ gestureDetector = new GestureDetector(this, new MapViewOnGestureListener());
+ multiTouchSupport = new MultiTouchSupport(this, new MapViewMultiTouchZoomListener());
+
+ long startTime = System.currentTimeMillis();
+
+ // Initialize native core prior (if needed)
+ if (NativeCore.isAvailable() && !NativeCore.isLoaded())
+ NativeCore.load(CoreResourcesFromAndroidAssets.loadFromCurrentApplication(this));
+
+ Logger.get().setSeverityLevelThreshold(LogSeverityLevel.Debug);
+
+ // Inflate views
+ setContentView(R.layout.activity_main);
+
+ // Get map view
+ mapView = (AtlasMapRendererView) findViewById(R.id.mapRendererView);
+
+ textZoom = (TextView) findViewById(R.id.text_zoom);
+ azimuthNorthButton = (ImageButton) findViewById(R.id.map_azimuth_north_button);
+
+ azimuthNorthButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setAzimuth(0f);
+ }
+ });
+
+ findViewById(R.id.map_zoom_in_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setZoom(zoom + 1f);
+ }
+ });
+
+ findViewById(R.id.map_zoom_out_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setZoom(zoom - 1f);
+ }
+ });
+
+ // Additional log sink
+ fileLogSink = QIODeviceLogSink.createFileLogSink(
+ Environment.getExternalStorageDirectory() + "/osmand/osmandcore.log");
+ Logger.get().addLogSink(fileLogSink);
+
+ // Get device display density factor
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ displayDensityFactor = displayMetrics.densityDpi / 160.0f;
+ referenceTileSize = (int)(256 * displayDensityFactor);
+ rasterTileSize = Integer.highestOneBit(referenceTileSize - 1) * 2;
+ Log.i(TAG, "displayDensityFactor = " + displayDensityFactor);
+ Log.i(TAG, "referenceTileSize = " + referenceTileSize);
+ Log.i(TAG, "rasterTileSize = " + rasterTileSize);
+
+ Log.i(TAG, "Going to resolve default embedded style...");
+ mapStylesCollection = new MapStylesCollection();
+ mapStyle = mapStylesCollection.getResolvedStyleByName("default");
+ if (mapStyle == null)
+ {
+ Log.e(TAG, "Failed to resolve style 'default'");
+ System.exit(0);
+ }
+
+ Log.i(TAG, "Going to prepare OBFs collection");
+ obfsCollection = new ObfsCollection();
+ Log.i(TAG, "Will load OBFs from " + Environment.getExternalStorageDirectory() + "/osmand");
+ obfsCollection.addDirectory(Environment.getExternalStorageDirectory() + "/osmand", false);
+
+ Log.i(TAG, "Going to prepare all resources for renderer");
+ mapPresentationEnvironment = new MapPresentationEnvironment(
+ mapStyle,
+ displayDensityFactor,
+ 1.0f,
+ 1.0f,
+ MapUtils.LANGUAGE);
+ //mapPresentationEnvironment->setSettings(configuration.styleSettings);
+ mapPrimitiviser = new MapPrimitiviser(
+ mapPresentationEnvironment);
+ obfMapObjectsProvider = new ObfMapObjectsProvider(
+ obfsCollection);
+ mapPrimitivesProvider = new MapPrimitivesProvider(
+ obfMapObjectsProvider,
+ mapPrimitiviser,
+ rasterTileSize);
+ mapObjectsSymbolsProvider = new MapObjectsSymbolsProvider(
+ mapPrimitivesProvider,
+ rasterTileSize);
+
+ mapView.setReferenceTileSizeOnScreenInPixels(referenceTileSize);
+ mapView.addSymbolsProvider(mapObjectsSymbolsProvider);
+
+ restoreMapState();
+
+ mapLayerProvider0 = new MapRasterLayerProvider_Software(mapPrimitivesProvider);
+ mapView.setMapLayerProvider(0, mapLayerProvider0);
+
+ System.out.println("NATIVE_INITIALIZED = " + (System.currentTimeMillis() - startTime) / 1000f);
+
+ //Setup search
+
+ searchAPI = new SearchAPI(obfsCollection);
+
+ final EditText searchEditText = (EditText) findViewById(R.id.searchEditText);
+ searchEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (s.length() > 2) {
+ runSearch(getScreenBounds31(), s.toString());
+ }
+ }
+ });
+ searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus && adapter.getCount() > 0 && isSearchListHidden()) {
+ showSearchList();
+ LatLon latLon = Utilities.convert31ToLatLon(target31);
+ adapter.updateDistance(latLon.getLatitude(), latLon.getLongitude());
+ adapter.notifyDataSetChanged();
+ }
+ }
+ });
+
+ ImageButton clearButton = (ImageButton) findViewById(R.id.clearButton);
+ clearButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ searchEditText.setText("");
+ adapter.clear();
+ adapter.notifyDataSetChanged();
+ hideSearchList();
+ }
+ });
+
+ searchListView = (ListView) findViewById(android.R.id.list);
+ adapter = new SearchListAdapter(this);
+ searchListView.setAdapter(adapter);
+ searchListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ hideSearchList();
+ mapView.requestFocus();
+ SearchRow item = adapter.getItem(position);
+ PointI target = Utilities.convertLatLonTo31(new LatLon(item.getLatitude(), item.getLongitude()));
+ setTarget(target);
+ setZoom(17f);
+ }
+ });
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ mapView.handleOnResume();
+ }
+
+ @Override
+ protected void onPause() {
+ saveMapState();
+ mapView.handleOnPause();
+
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mapView.handleOnDestroy();
+
+ super.onDestroy();
+ }
+
+ private AreaI getScreenBounds31() {
+ PointI topLeftPoint = new PointI();
+ PointI bottomRightPoint = new PointI();
+ mapView.getLocationFromScreenPoint(new PointI(0, 0), topLeftPoint);
+ mapView.getLocationFromScreenPoint(new PointI(mapView.getWidth(), mapView.getHeight()), bottomRightPoint);
+ return new AreaI(topLeftPoint, bottomRightPoint);
+ }
+
+ private boolean isSearchListHidden() {
+ return searchListView.getVisibility() != View.VISIBLE;
+ }
+
+ private void showSearchList() {
+ if (isSearchListHidden()) {
+ ViewCompat.setAlpha(searchListView, 0f);
+ searchListView.setVisibility(View.VISIBLE);
+ ViewCompat.animate(searchListView).alpha(1f).setListener(null);
+ }
+ }
+
+ private void hideSearchList() {
+ ViewCompat.animate(searchListView).alpha(0f).setListener(new ViewPropertyAnimatorListener() {
+ @Override
+ public void onAnimationStart(View view) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ searchListView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(View view) {
+ searchListView.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ public void saveMapState() {
+ SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+ Editor edit = prefs.edit();
+ LatLon latLon = Utilities.convert31ToLatLon(target31);
+ edit.putFloat(PREF_MAP_CENTER_LAT, (float)latLon.getLatitude());
+ edit.putFloat(PREF_MAP_CENTER_LON, (float)latLon.getLongitude());
+ edit.putFloat(PREF_MAP_AZIMUTH, azimuth);
+ edit.putFloat(PREF_MAP_ZOOM, zoom);
+ edit.putFloat(PREF_MAP_ELEVATION_ANGLE, elevationAngle);
+ edit.commit();
+ }
+
+ public void restoreMapState() {
+ SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+ float prefLat = prefs.getFloat(PREF_MAP_CENTER_LAT, INIT_LAT);
+ float prefLon = prefs.getFloat(PREF_MAP_CENTER_LON, INIT_LON);
+ float prefAzimuth = prefs.getFloat(PREF_MAP_AZIMUTH, INIT_AZIMUTH);
+ float prefZoom = prefs.getFloat(PREF_MAP_ZOOM, INIT_ZOOM);
+ float prefElevationAngle = prefs.getFloat(PREF_MAP_ELEVATION_ANGLE, INIT_ELEVATION_ANGLE);
+
+ setAzimuth(prefAzimuth);
+ setElevationAngle(prefElevationAngle);
+ setTarget(Utilities.convertLatLonTo31(new LatLon(prefLat, prefLon)));
+ setZoom(prefZoom);
+ }
+
+ public boolean setTarget(PointI pointI) {
+ target31 = pointI;
+ return mapView.setTarget(pointI);
+ }
+
+ @SuppressLint("DefaultLocale")
+ public boolean setZoom(float zoom) {
+ if (zoom < MIN_ZOOM_LEVEL) {
+ zoom = MIN_ZOOM_LEVEL;
+ } else if (zoom > MAX_ZOOM_LEVEL) {
+ zoom = MAX_ZOOM_LEVEL;
+ }
+ this.zoom = zoom;
+ textZoom.setText(String.format("%.0f", zoom));
+ return mapView.setZoom(zoom);
+ }
+
+ public void setAzimuth(float angle) {
+ angle = MapUtils.unifyRotationTo360(angle);
+ this.azimuth = angle;
+ mapView.setAzimuth(angle);
+
+ if (angle == 0f && azimuthNorthButton.getVisibility() == View.VISIBLE) {
+ azimuthNorthButton.setVisibility(View.INVISIBLE);
+ } else if (angle != 0f && azimuthNorthButton.getVisibility() == View.INVISIBLE) {
+ azimuthNorthButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setElevationAngle(float angle) {
+ if (angle < 35f) {
+ angle = 35f;
+ } else if (angle > 90f) {
+ angle = 90f;
+ }
+ this.elevationAngle = angle;
+ mapView.setElevationAngle(angle);
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ return multiTouchSupport.onTouchEvent(event)
+ || gestureDetector.onTouchEvent(event);
+ }
+
+ private void runSearch(AreaI bounds31, String keyword) {
+
+ searchAPI.setObfAreaFilter(bounds31);
+ searchAPI.startSearch(keyword, MAX_SEARCH_RESULTS, new SearchAPICallback() {
+ @Override
+ public void onSearchFinished(List searchItems, boolean cancelled) {
+ if (searchItems != null && !cancelled) {
+ LatLon latLon = Utilities.convert31ToLatLon(target31);
+ List rows = new ArrayList<>();
+ for (SearchItem item : searchItems) {
+ SearchRow row = new SearchRow(item);
+ rows.add(row);
+ }
+
+ adapter.clear();
+ adapter.addAll(rows);
+ adapter.updateDistance(latLon.getLatitude(), latLon.getLongitude());
+ adapter.sort(new Comparator() {
+ @Override
+ public int compare(SearchRow lhs, SearchRow rhs) {
+ int res = Double.compare(lhs.getDistance(), rhs.getDistance());
+ if (res == 0) {
+ return lhs.getSearchItem().getLocalizedName().compareToIgnoreCase(rhs.getSearchItem().getLocalizedName());
+ } else {
+ return res;
+ }
+ }
+ });
+ adapter.notifyDataSetChanged();
+ if (adapter.getCount() > 0) {
+ searchListView.setSelection(0);
+ }
+
+ showSearchList();
+ }
+ }
+ });
+ }
+
+ private class MapViewOnGestureListener extends SimpleOnGestureListener {
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ mapView.requestFocus();
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ float fromX = e2.getX() + distanceX;
+ float fromY = e2.getY() + distanceY;
+ float toX = e2.getX();
+ float toY = e2.getY();
+
+ float dx = (fromX - toX);
+ float dy = (fromY - toY);
+
+ PointI newTarget = new PointI();
+ mapView.getLocationFromScreenPoint(new PointI(mapView.getWidth() / 2 + (int)dx, mapView.getHeight() / 2 + (int)dy), newTarget);
+
+ setTarget(newTarget);
+
+ mapView.requestFocus();
+ return true;
+ }
+ }
+
+ private class MapViewMultiTouchZoomListener implements MultiTouchZoomListener {
+
+ private float initialZoom;
+ private float initialAzimuth;
+ private float initialElevation;
+ private PointF centerPoint;
+
+ @Override
+ public void onGestureFinished(float scale, float rotation) {
+ }
+
+ @Override
+ public void onGestureInit(float x1, float y1, float x2, float y2) {
+ }
+
+ @Override
+ public void onZoomStarted(PointF centerPoint) {
+ initialZoom = zoom;
+ initialAzimuth = azimuth;
+ this.centerPoint = centerPoint;
+ }
+
+ @Override
+ public void onZoomingOrRotating(float scale, float rotation) {
+
+ PointI centerLocationBefore = new PointI();
+ mapView.getLocationFromScreenPoint(
+ new PointI((int)centerPoint.x, (int)centerPoint.y), centerLocationBefore);
+
+ // Change zoom
+ setZoom(initialZoom + (float)(Math.log(scale) / Math.log(2)));
+
+ // Adjust current target position to keep touch center the same
+ PointI centerLocationAfter = new PointI();
+ mapView.getLocationFromScreenPoint(
+ new PointI((int)centerPoint.x, (int)centerPoint.y), centerLocationAfter);
+ PointI centerLocationDelta = new PointI(
+ centerLocationAfter.getX() - centerLocationBefore.getX(),
+ centerLocationAfter.getY() - centerLocationBefore.getY());
+
+ setTarget(new PointI(target31.getX() - centerLocationDelta.getX(), target31.getY() - centerLocationDelta.getY()));
+
+ /*
+ // Convert point from screen to location
+ PointI centerLocation = new PointI();
+ mapView.getLocationFromScreenPoint(
+ new PointI((int)centerPoint.x, (int)centerPoint.y), centerLocation);
+
+ // Rotate current target around center location
+ PointI target = new PointI(xI - centerLocation.getX(), yI - centerLocation.getY());
+ double cosAngle = Math.cos(-Math.toRadians(rotation));
+ double sinAngle = Math.sin(-Math.toRadians(rotation));
+
+ PointI newTarget = new PointI(
+ (int)(target.getX() * cosAngle - target.getY() * sinAngle + centerLocation.getX()),
+ (int)(target.getX() * sinAngle + target.getY() * cosAngle + centerLocation.getY()));
+
+ setTarget(newTarget);
+ */
+
+ // Set rotation
+ setAzimuth(initialAzimuth - rotation);
+ }
+
+ @Override
+ public void onChangeViewAngleStarted() {
+ initialElevation = elevationAngle;
+ }
+
+ @Override
+ public void onChangingViewAngle(float angle) {
+ setElevationAngle(initialElevation - angle);
+ }
+ }
+}
diff --git a/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MapUtils.java b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MapUtils.java
new file mode 100644
index 0000000000..eac4454d46
--- /dev/null
+++ b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MapUtils.java
@@ -0,0 +1,30 @@
+package net.osmand.core.samples.android.sample1;
+
+import java.util.Locale;
+
+public class MapUtils {
+
+ public static final String LANGUAGE;
+
+ static {
+ LANGUAGE = getLanguage();
+ }
+
+ public static float unifyRotationTo360(float rotate) {
+ while (rotate < -180) {
+ rotate += 360;
+ }
+ while (rotate > +180) {
+ rotate -= 360;
+ }
+ return rotate;
+ }
+
+ private static String getLanguage() {
+ String langCode = Locale.getDefault().getLanguage();
+ if (langCode.isEmpty()) {
+ langCode = "en";
+ }
+ return langCode;
+ }
+}
diff --git a/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MultiTouchSupport.java b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MultiTouchSupport.java
new file mode 100644
index 0000000000..9f0dbd350b
--- /dev/null
+++ b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/MultiTouchSupport.java
@@ -0,0 +1,202 @@
+package net.osmand.core.samples.android.sample1;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.lang.reflect.Method;
+
+public class MultiTouchSupport {
+
+ private static final String TAG = "MultiTouchSupport";
+
+ public static final int ACTION_MASK = 255;
+ protected final Context ctx;
+ private final MultiTouchZoomListener listener;
+ protected Method getPointerCount;
+ protected Method getX;
+ protected Method getY;
+ protected Method getPointerId;
+
+ private float initialAngle;
+ private float rotation;
+ private static final float ROTATION_THRESHOLD_DEG = 15.0f;
+ private boolean isRotating;
+
+ private boolean multiTouchAPISupported = false;
+
+ private boolean inTiltMode = false;
+ private PointF firstFingerStart = new PointF();
+ private PointF secondFingerStart = new PointF();
+ private static final int TILT_X_THRESHOLD_PX = 40;
+ private static final int TILT_Y_THRESHOLD_PX = 40;
+ private static final int TILT_DY_THRESHOLD_PX = 40;
+
+ private boolean inZoomMode = false;
+ private float initialDistance = 100;
+ private float scale = 1;
+ private PointF centerPoint = new PointF();
+
+ private boolean multiTouch;
+
+ public MultiTouchSupport(Context ctx, MultiTouchZoomListener listener) {
+ this.ctx = ctx;
+ this.listener = listener;
+ initMethods();
+ }
+
+ public boolean isMultiTouchSupported(){
+ return multiTouchAPISupported;
+ }
+
+ public boolean isInZoomMode(){
+ return inZoomMode;
+ }
+
+ public boolean isInTiltMode() {
+ return inTiltMode;
+ }
+
+ private void initMethods() {
+ try {
+ getPointerCount = MotionEvent.class.getMethod("getPointerCount"); //$NON-NLS-1$
+ getPointerId = MotionEvent.class.getMethod("getPointerId", Integer.TYPE); //$NON-NLS-1$
+ getX = MotionEvent.class.getMethod("getX", Integer.TYPE); //$NON-NLS-1$
+ getY = MotionEvent.class.getMethod("getY", Integer.TYPE); //$NON-NLS-1$
+ multiTouchAPISupported = true;
+ } catch (Exception e) {
+ multiTouchAPISupported = false;
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent event){
+ if(!isMultiTouchSupported()){
+ return false;
+ }
+ int actionCode = event.getAction() & ACTION_MASK;
+ try {
+ if (actionCode == MotionEvent.ACTION_UP || actionCode == MotionEvent.ACTION_CANCEL) {
+ multiTouch = false;
+ }
+ Integer pointCount = (Integer) getPointerCount.invoke(event);
+ if (pointCount < 2) {
+ if (inZoomMode || inTiltMode) {
+ listener.onGestureFinished(scale, rotation);
+ inZoomMode = false;
+ inTiltMode = false;
+ }
+ return multiTouch;
+ } else {
+ multiTouch = true;
+ }
+
+ Float x1 = (Float) getX.invoke(event, 0);
+ Float x2 = (Float) getX.invoke(event, 1);
+ Float y1 = (Float) getY.invoke(event, 0);
+ Float y2 = (Float) getY.invoke(event, 1);
+ float distance = (float) Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
+ float angle = 0;
+ boolean angleDefined = false;
+ if (x1.floatValue() != x2.floatValue() || y1.floatValue() != y2.floatValue()) {
+ angleDefined = true;
+ angle = (float) (Math.atan2(y2 - y1, x2 -x1) * 180 / Math.PI);
+ }
+
+ switch (actionCode) {
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+
+ centerPoint = new PointF((x1 + x2) / 2, (y1 + y2) / 2);
+ firstFingerStart = new PointF(x1, y1);
+ secondFingerStart = new PointF(x2, y2);
+ listener.onGestureInit(x1, y1, x2, y2);
+ return true;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+
+ if (inZoomMode || inTiltMode) {
+ listener.onGestureFinished(scale, rotation);
+ inZoomMode = false;
+ inTiltMode = false;
+ }
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE: {
+
+ if (inZoomMode) {
+ if (angleDefined) {
+ float a = MapUtils.unifyRotationTo360(angle - initialAngle);
+ if (!isRotating && Math.abs(a) > ROTATION_THRESHOLD_DEG) {
+ isRotating = true;
+ initialAngle = angle;
+ } else if (isRotating) {
+ rotation = a;
+ }
+ }
+ scale = distance / initialDistance;
+
+ listener.onZoomingOrRotating(scale, rotation);
+ return true;
+
+ } else if (inTiltMode) {
+ float dy2 = secondFingerStart.y - y2;
+ float viewAngle = dy2 / 8f;
+ listener.onChangingViewAngle(viewAngle);
+
+ } else {
+ float dx1 = Math.abs(firstFingerStart.x - x1);
+ float dx2 = Math.abs(secondFingerStart.x - x2);
+ float dy1 = Math.abs(firstFingerStart.y - y1);
+ float dy2 = Math.abs(secondFingerStart.y - y2);
+ float startDy = Math.abs(secondFingerStart.y - firstFingerStart.y);
+ if (dx1 < TILT_X_THRESHOLD_PX && dx2 < TILT_X_THRESHOLD_PX
+ && dy1 > TILT_Y_THRESHOLD_PX && dy2 > TILT_Y_THRESHOLD_PX
+ && startDy < TILT_Y_THRESHOLD_PX * 6
+ && Math.abs(dy2 - dy1) < TILT_DY_THRESHOLD_PX) {
+ listener.onChangeViewAngleStarted();
+ inTiltMode = true;
+
+ } else if (dx1 > TILT_X_THRESHOLD_PX || dx2 > TILT_X_THRESHOLD_PX
+ || Math.abs(dy2 - dy1) > TILT_DY_THRESHOLD_PX
+ || Math.abs(dy1 - dy2) > TILT_DY_THRESHOLD_PX) {
+ listener.onZoomStarted(centerPoint);
+ initialDistance = distance;
+ initialAngle = angle;
+ rotation = 0;
+ scale = 0;
+ isRotating = false;
+ inZoomMode = true;
+ }
+ }
+ }
+ default:
+ break;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Multi touch exception" , e); //$NON-NLS-1$
+ }
+ return true;
+ }
+
+ public PointF getCenterPoint() {
+ return centerPoint;
+ }
+
+ public interface MultiTouchZoomListener {
+
+ void onZoomStarted(PointF centerPoint);
+
+ void onZoomingOrRotating(float scale, float rotation);
+
+ void onChangeViewAngleStarted();
+
+ void onChangingViewAngle(float angle);
+
+ void onGestureFinished(float scale, float rotation);
+
+ void onGestureInit(float x1, float y1, float x2, float y2);
+
+ }
+}
diff --git a/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchAPI.java b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchAPI.java
new file mode 100644
index 0000000000..b00b677a90
--- /dev/null
+++ b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchAPI.java
@@ -0,0 +1,306 @@
+package net.osmand.core.samples.android.sample1;
+
+import android.os.AsyncTask;
+
+import net.osmand.core.jni.AmenitiesByNameSearch;
+import net.osmand.core.jni.Amenity;
+import net.osmand.core.jni.Amenity.DecodedCategory;
+import net.osmand.core.jni.AreaI;
+import net.osmand.core.jni.DecodedCategoryList;
+import net.osmand.core.jni.IObfsCollection;
+import net.osmand.core.jni.IQueryController;
+import net.osmand.core.jni.ISearch;
+import net.osmand.core.jni.LatLon;
+import net.osmand.core.jni.NullableAreaI;
+import net.osmand.core.jni.ObfInfo;
+import net.osmand.core.jni.ObfsCollection;
+import net.osmand.core.jni.PointI;
+import net.osmand.core.jni.QStringStringHash;
+import net.osmand.core.jni.Utilities;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchAPI {
+
+ private ObfsCollection obfsCollection;
+ private AreaI searchableArea;
+ private AreaI obfAreaFilter;
+ private SearchRequestExecutor executor;
+
+ interface SearchAPICallback {
+ void onSearchFinished(List searchItems, boolean cancelled);
+ }
+
+ public SearchAPI(ObfsCollection obfsCollection) {
+ this.obfsCollection = obfsCollection;
+ executor = new SearchRequestExecutor();
+ }
+
+ public AreaI getSearchableArea() {
+ return searchableArea;
+ }
+
+ public void setSearchableArea(AreaI searchableArea) {
+ this.searchableArea = searchableArea;
+ }
+
+ public AreaI getObfAreaFilter() {
+ return obfAreaFilter;
+ }
+
+ public void setObfAreaFilter(AreaI obfAreaFilter) {
+ this.obfAreaFilter = obfAreaFilter;
+ }
+
+ public void startSearch(String keyword, int maxSearchResults, SearchAPICallback apiCallback) {
+ executor.run(new SearchRequest(keyword, maxSearchResults, apiCallback), true);
+ }
+
+ public void cancelSearch() {
+ executor.cancel();
+ }
+
+
+ public class SearchRequestExecutor {
+
+ private SearchRequest ongoingSearchRequest;
+ private SearchRequest nextSearchRequest;
+
+ public void run(SearchRequest searchRequest, boolean cancelCurrentRequest) {
+ if (ongoingSearchRequest != null) {
+ nextSearchRequest = searchRequest;
+ if (cancelCurrentRequest) {
+ ongoingSearchRequest.cancel();
+ }
+ } else {
+ ongoingSearchRequest = searchRequest;
+ nextSearchRequest = null;
+ searchRequest.setOnFinishedCallback(new Runnable() {
+ @Override
+ public void run() {
+ operationFinished();
+ }
+ });
+ searchRequest.run();
+ }
+ }
+
+ public void cancel() {
+ if (nextSearchRequest != null) {
+ nextSearchRequest = null;
+ }
+ if (ongoingSearchRequest != null) {
+ ongoingSearchRequest.cancel();
+ }
+ }
+
+ private void operationFinished() {
+ ongoingSearchRequest = null;
+ if (nextSearchRequest != null) {
+ run(nextSearchRequest, false);
+ }
+ }
+ }
+
+ public class SearchRequest {
+ private String keyword;
+ private int maxSearchResults;
+ private Runnable onFinished;
+ private SearchAPICallback apiCallback;
+
+ private boolean cancelled;
+ private int resCount;
+
+ public SearchRequest(String keyword, int maxSearchResults, SearchAPICallback apiCallback) {
+ this.keyword = keyword;
+ this.maxSearchResults = maxSearchResults;
+ this.apiCallback = apiCallback;
+ }
+
+ public void run() {
+
+ new AsyncTask>() {
+ @Override
+ protected List doInBackground(String... params) {
+ return doSearch(params[0]);
+ }
+
+ @Override
+ protected void onPostExecute(List searchItems) {
+
+ if (onFinished != null) {
+ onFinished.run();
+ }
+
+ if (apiCallback != null) {
+ apiCallback.onSearchFinished(searchItems, cancelled);
+ }
+ }
+ }.execute(keyword);
+ }
+
+ private List doSearch(String keyword) {
+ System.out.println("=== Start search");
+ resCount = 0;
+
+ final List searchItems = new ArrayList<>();
+
+ AmenitiesByNameSearch byNameSearch = new AmenitiesByNameSearch(obfsCollection);
+ AmenitiesByNameSearch.Criteria criteria = new AmenitiesByNameSearch.Criteria();
+ criteria.setName(keyword);
+ if (obfAreaFilter != null) {
+ criteria.setObfInfoAreaFilter(new NullableAreaI(new AreaI(obfAreaFilter)));
+ }
+
+ ISearch.INewResultEntryCallback newResultEntryCallback = new ISearch.INewResultEntryCallback() {
+ @Override
+ public void method(ISearch.Criteria criteria, ISearch.IResultEntry resultEntry) {
+ Amenity amenity = new ResultEntry(resultEntry).getAmenity();
+ searchItems.add(new AmenitySearchItem(amenity));
+ System.out.println("Poi found === " + amenity.getNativeName());
+ resCount++;
+ /*
+ QStringStringHash locNames = amenity.getLocalizedNames();
+ if (locNames.size() > 0) {
+ QStringList keys = locNames.keys();
+ StringBuilder sb = new StringBuilder("=== Localized names: ");
+ for (int i = 0; i < keys.size(); i++) {
+ String key = keys.get(i);
+ sb.append(key).append("=").append(locNames.get(key)).append(" | ");
+ }
+ System.out.println(sb.toString());
+ }
+ */
+ }
+ };
+
+ byNameSearch.performSearch(criteria, newResultEntryCallback.getBinding(), new IQueryController() {
+ @Override
+ public boolean isAborted() {
+ return resCount >= maxSearchResults || cancelled;
+ }
+ });
+
+ System.out.println("=== Finish search");
+
+ return searchItems;
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+
+ public void setOnFinishedCallback(Runnable onFinished) {
+ this.onFinished = onFinished;
+ }
+ }
+
+ private static class ResultEntry extends AmenitiesByNameSearch.ResultEntry {
+ protected ResultEntry(ISearch.IResultEntry resultEntry) {
+ super(ISearch.IResultEntry.getCPtr(resultEntry), false);
+ }
+ }
+
+ public static abstract class SearchItem {
+
+ protected double latitude;
+ protected double longitude;
+
+ public SearchItem(double latitude, double longitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ public SearchItem(PointI location31) {
+ LatLon latLon = Utilities.convert31ToLatLon(location31);
+ latitude = latLon.getLatitude();
+ longitude = latLon.getLongitude();
+ }
+
+ public abstract String getNativeName();
+
+ public abstract String getLocalizedName();
+
+ public abstract String getTypeName();
+
+ public abstract String getSubTypeName();
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ @Override
+ public String toString() {
+ return getNativeName() + " {lat:" + getLatitude() + " lon: " + getLongitude() + "}";
+ }
+ }
+
+ public static class AmenitySearchItem extends SearchItem {
+
+ private String nativeName;
+ private String localizedName;
+ private String category;
+ private String subcategory;
+
+ public AmenitySearchItem(Amenity amenity) {
+ super(amenity.getPosition31());
+
+ nativeName = amenity.getNativeName();
+ QStringStringHash locNames = amenity.getLocalizedNames();
+ if (locNames.has_key(MapUtils.LANGUAGE)) {
+ localizedName = locNames.get(MapUtils.LANGUAGE);
+ }
+ /*
+ if (locNames.size() > 0) {
+ QStringList keys = locNames.keys();
+ for (int i = 0; i < keys.size(); i++) {
+ String key = keys.get(i);
+ localizedNamesMap.put(key, locNames.get(key));
+ }
+ }
+ */
+
+ DecodedCategoryList catList = amenity.getDecodedCategories();
+ if (catList.size() > 0) {
+ DecodedCategory decodedCategory = catList.get(0);
+ category = decodedCategory.getCategory();
+ subcategory = decodedCategory.getSubcategory();
+ }
+ }
+
+ @Override
+ public String getNativeName() {
+ return nativeName;
+ }
+
+ @Override
+ public String getLocalizedName() {
+ return localizedName != null ? localizedName : nativeName;
+ }
+
+ @Override
+ public String getTypeName() {
+ return category;
+ }
+
+ @Override
+ public String getSubTypeName() {
+ return subcategory;
+ }
+
+ @Override
+ public double getLatitude() {
+ return latitude;
+ }
+
+ @Override
+ public double getLongitude() {
+ return longitude;
+ }
+ }
+}
diff --git a/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchUIHelper.java b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchUIHelper.java
new file mode 100644
index 0000000000..ff82491271
--- /dev/null
+++ b/OsmAndCore-sample/src/net/osmand/core/samples/android/sample1/SearchUIHelper.java
@@ -0,0 +1,139 @@
+package net.osmand.core.samples.android.sample1;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.osmand.core.jni.Utilities;
+import net.osmand.core.samples.android.sample1.SearchAPI.SearchItem;
+
+import java.text.MessageFormat;
+
+public class SearchUIHelper {
+
+ public static Drawable getIcon(Context ctx, SearchItem item) {
+ return null;
+ }
+
+ public static class SearchRow {
+
+ private SearchItem searchItem;
+ private double distance;
+
+ public SearchRow(SearchItem searchItem) {
+ this.searchItem = searchItem;
+ }
+
+ public SearchItem getSearchItem() {
+ return searchItem;
+ }
+
+ public double getLatitude() {
+ return searchItem.getLatitude();
+ }
+
+ public double getLongitude() {
+ return searchItem.getLongitude();
+ }
+
+ public double getDistance() {
+ return distance;
+ }
+
+ public void setDistance(double distance) {
+ this.distance = distance;
+ }
+ }
+
+ public static class SearchListAdapter extends ArrayAdapter {
+
+ private Context ctx;
+
+ public SearchListAdapter(Context ctx) {
+ super(ctx, R.layout.search_list_item);
+ this.ctx = ctx;
+ }
+
+ public void updateDistance(double latitude, double longitude) {
+ for (int i = 0; i < getCount(); i++) {
+ SearchRow item = getItem(i);
+ item.setDistance(Utilities.distance(
+ longitude, latitude,
+ item.getLongitude(), item.getLatitude()));
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SearchRow item = getItem(position);
+
+ LinearLayout view;
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) ctx
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (LinearLayout) inflater.inflate(
+ R.layout.search_list_item, null);
+ } else {
+ view = (LinearLayout) convertView;
+ }
+
+ TextView title = (TextView) view.findViewById(R.id.title);
+ TextView subtitle = (TextView) view.findViewById(R.id.subtitle);
+ TextView distance = (TextView) view.findViewById(R.id.distance);
+ title.setText(item.searchItem.getLocalizedName());
+ StringBuilder sb = new StringBuilder();
+ if (!item.searchItem.getSubTypeName().isEmpty()) {
+ sb.append(getNiceString(item.searchItem.getSubTypeName()));
+ }
+ if (!item.searchItem.getTypeName().isEmpty()) {
+ if (sb.length() > 0) {
+ sb.append(" — ");
+ }
+ sb.append(getNiceString(item.searchItem.getTypeName()));
+ }
+ subtitle.setText(sb.toString());
+ if (item.getDistance() == 0) {
+ distance.setText("");
+ } else {
+ distance.setText(getFormattedDistance(item.getDistance()));
+ }
+
+ //text1.setTextColor(ctx.getResources().getColor(R.color.listTextColor));
+ //view.setCompoundDrawablesWithIntrinsicBounds(getIcon(ctx, item), null, null, null);
+ //view.setCompoundDrawablePadding(ctx.getResources().getDimensionPixelSize(R.dimen.list_content_padding));
+ return view;
+ }
+ }
+
+ public static String capitalizeFirstLetterAndLowercase(String s) {
+ if (s != null && s.length() > 1) {
+ // not very efficient algorithm
+ return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
+ } else {
+ return s;
+ }
+ }
+
+ public static String getNiceString(String s) {
+ return capitalizeFirstLetterAndLowercase(s.replaceAll("_", " "));
+ }
+
+ public static String getFormattedDistance(double meters) {
+ double mainUnitInMeters = 1000;
+ String mainUnitStr = "km";
+ if (meters >= 100 * mainUnitInMeters) {
+ return (int) (meters / mainUnitInMeters + 0.5) + " " + mainUnitStr;
+ } else if (meters > 9.99f * mainUnitInMeters) {
+ return MessageFormat.format("{0,number,#.#} " + mainUnitStr, ((float) meters) / mainUnitInMeters).replace('\n', ' ');
+ } else if (meters > 0.999f * mainUnitInMeters) {
+ return MessageFormat.format("{0,number,#.##} " + mainUnitStr, ((float) meters) / mainUnitInMeters).replace('\n', ' ');
+ } else {
+ return ((int) (meters + 0.5)) + " m";
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 4c604d0ff3..234ab0eb96 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,4 @@
-include ':OsmAnd-java'
+include ':OsmAnd-java', ':OsmAndCore-sample'
include ':OsmAnd'
include ':plugins:OsmAnd-AddressPlugin'
include ':plugins:Osmand-ParkingPlugin'