From: Jakob Cornell <_ecjqzpqwlt@jcornell.net>
Date: Mon, 30 Sep 2019 03:33:03 +0000 (+0000)
Subject: Initial commit
X-Git-Url: https://jcornell.net/gitweb/gitweb.cgi?a=commitdiff_plain;h=HEAD;p=osmand-geocoder.git
Initial commit
---
c9cfcf7a750060a97ffffcc784a6288d38aeb60b
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..69e8586
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+
+/gradle/
+/gradlew
+/gradlew.bat
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..e8ceec2
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.2"
+ defaultConfig {
+ applicationId "net.jcornell.osmandgeocoder"
+ minSdkVersion 15
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5308b80
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/net/jcornell/osmandgeocoder/Application.java b/app/src/main/java/net/jcornell/osmandgeocoder/Application.java
new file mode 100644
index 0000000..5ca4242
--- /dev/null
+++ b/app/src/main/java/net/jcornell/osmandgeocoder/Application.java
@@ -0,0 +1,12 @@
+package net.jcornell.osmandgeocoder;
+
+import net.jcornell.osmandgeocoder.Util.Result;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.json.JSONArray;
+
+public class Application extends android.app.Application {
+ public final BlockingQueue> results = new LinkedBlockingQueue<>();
+}
diff --git a/app/src/main/java/net/jcornell/osmandgeocoder/MainActivity.java b/app/src/main/java/net/jcornell/osmandgeocoder/MainActivity.java
new file mode 100644
index 0000000..2388078
--- /dev/null
+++ b/app/src/main/java/net/jcornell/osmandgeocoder/MainActivity.java
@@ -0,0 +1,209 @@
+package net.jcornell.osmandgeocoder;
+
+import net.jcornell.osmandgeocoder.Util.Result;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import androidx.appcompat.app.AppCompatActivity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+public class MainActivity extends AppCompatActivity {
+ protected Application app;
+ protected ExecutorService jobExecutor, resultProcExecutor;
+
+ protected void initResultProcThread() {
+ resultProcExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ final Button goButton = findViewById(R.id.go_button);
+ goButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ handleSubmit();
+ }
+ });
+
+ app = (Application) getApplication();
+
+ // I had problems using plain threads for some reason
+ jobExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ initResultProcThread();
+ resultProcExecutor.execute(new ResultProcessor());
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ resultProcExecutor.shutdownNow();
+ }
+
+ protected static class UiStatus {
+ public final boolean loading;
+ public final String statusMessage;
+
+ public UiStatus(boolean loading, String statusMessage) {
+ this.loading = loading;
+ this.statusMessage = statusMessage;
+ }
+ }
+
+ protected void updateUiStatus(UiStatus status) {
+ ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);
+ TextView statusText = (TextView) findViewById(R.id.status_text);
+
+ progressBar.setVisibility(status.loading ? View.VISIBLE : View.GONE);
+ if (status.statusMessage == null) {
+ statusText.setVisibility(View.GONE);
+ } else {
+ statusText.setText(status.statusMessage);
+ statusText.setVisibility(View.VISIBLE);
+ }
+ }
+
+ protected void handleSubmit() {
+ final EditText addrBox = findViewById(R.id.addr_entry);
+ final String addr = addrBox.getText().toString();
+ jobExecutor.execute(new Runner(addr));
+ updateUiStatus(new UiStatus(true, null));
+ }
+
+ protected void onJobComplete(Result result) {
+ if (result instanceof Result.Ok) {
+ JSONArray data = ((Result.Ok) result).value;
+ if (data.length() == 0) {
+ updateUiStatus(new UiStatus(false, "No results."));
+ }
+ else {
+ updateUiStatus(new UiStatus(false, null));
+
+ final Dialog resultsDialog = new Dialog(this);
+ resultsDialog.setContentView(R.layout.results_dialog);
+ resultsDialog.setTitle("Results");
+
+ class Entry {
+ public final String name, latitude, longitude;
+ public Entry(String name, String latitude, String longitude) {
+ this.name = name;
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+ public String toString() { return name; }
+ }
+
+ final List entries = new ArrayList<>();
+ for (int i = 0; i < data.length(); i += 1) {
+ try {
+ JSONObject entry = data.getJSONObject(i);
+ entries.add(new Entry(
+ entry.getString("display_name"),
+ entry.getString("lat"),
+ entry.getString("lon")
+ ));
+ } catch (JSONException e) {}
+ }
+ ArrayAdapter adapter = new ArrayAdapter<>(
+ this,
+ android.R.layout.simple_list_item_1,
+ entries
+ );
+
+ ListView list = (ListView) resultsDialog.findViewById(R.id.results_list);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ resultsDialog.dismiss();
+ Entry e = entries.get(position);
+ Uri uri = Uri.parse(String.format("geo:%s,%s", e.latitude, e.longitude));
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ }
+ });
+ list.setAdapter(adapter);
+ resultsDialog.show();
+ }
+ } else {
+ Throwable t = ((Result.Error) result).error;
+ String message = String.format("%s: %s", t.getClass().getSimpleName(), t.getMessage());
+ updateUiStatus(new UiStatus(false, message));
+ }
+ }
+
+ protected class ResultProcessor implements Runnable {
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ final Result result = app.results.take();
+ runOnUiThread(new Runnable() {
+ @Override public void run() {
+ onJobComplete(result);
+ }
+ });
+ }
+ } catch (InterruptedException e) {
+ // terminate thread
+ }
+ }
+ }
+
+ protected class Runner implements Runnable {
+ protected final String API_URL = "https://nominatim.openstreetmap.org/search";
+
+ protected final String address;
+
+ public Runner(String address) {
+ this.address = address;
+ }
+
+ @Override
+ public void run() {
+ Result result;
+ try {
+ String queryParam = URLEncoder.encode(
+ address.replace('\n', ';'), // Nominatim doesn't like these
+ "UTF-8"
+ );
+ URL url = new URL(API_URL + "?format=json&limit=10&q=" + queryParam);
+ InputStream stream = url.openConnection().getInputStream();
+ String body = new String(Util.readAll(stream), "UTF-8");
+ result = new Result.Ok<>(new JSONArray(body));
+ } catch (IOException | JSONException e) {
+ result = new Result.Error(e);
+ }
+
+ try {
+ app.results.put(result);
+ } catch (InterruptedException e) {}
+ }
+ }
+}
diff --git a/app/src/main/java/net/jcornell/osmandgeocoder/Util.java b/app/src/main/java/net/jcornell/osmandgeocoder/Util.java
new file mode 100644
index 0000000..31d7be1
--- /dev/null
+++ b/app/src/main/java/net/jcornell/osmandgeocoder/Util.java
@@ -0,0 +1,41 @@
+package net.jcornell.osmandgeocoder;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+
+public class Util {
+ public interface Result {
+ public static class Ok implements Result {
+ public final B value;
+ public Ok(B value) {
+ this.value = value;
+ }
+ }
+ public static class Error implements Result {
+ public final Throwable error;
+ public Error(Throwable error) {
+ this.error = error;
+ }
+ }
+ }
+
+ protected static byte[] readAll(InputStream in) throws IOException {
+ // two byte-by-byte copies brought to you by the Java 1.7 standard library
+ List storage = new ArrayList<>();
+ byte[] buffer = new byte[1024];
+ int ret = in.read(buffer);
+ while (ret != -1) {
+ for (int i = 0; i < ret; i += 1) {
+ storage.add(buffer[i]);
+ }
+ ret = in.read(buffer);
+ }
+ byte[] out = new byte[storage.size()];
+ for (int i = 0; i < storage.size(); i += 1) {
+ out[i] = storage.get(i);
+ }
+ return out;
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3ec886a
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/results_dialog.xml b/app/src/main/res/layout/results_dialog.xml
new file mode 100644
index 0000000..ebd5f5c
--- /dev/null
+++ b/app/src/main/res/layout/results_dialog.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..69b2233
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c9330cc
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ OsmAnd Geocoder
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..f5fb2cc
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..ff3feff
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,17 @@
+# Geocoder for OsmAnd
+
+This is a simple Android app to support the use of the [Nominatim](https://nominatim.org) geocoding API with [OsmAnd](https://osmand.net).
+It accepts a string and queries the Nominatim API to find matches in the OpenStreetMap data.
+The app shows a list of the results returned (a maximum of 10).
+When the user selects an entry from the list, the app sends an intent with a [`geo` URI](https://en.wikipedia.org/wiki/Geo_URI_scheme) specifying the latitude and longitude of the place, which is received by OsmAnd (or perhaps another mapping app).
+
+## Input
+
+I'm not sure exactly what sorts of search strings this application will work with.
+It uses the "free-form" query feature documented [here](https://nominatim.org/release-docs/latest/api/Search/#parameters).
+Looks like it tends to work very well with addresses in the typical format humans use.
+
+## Nominatim API
+
+Please be aware that the app makes requests to the nominatim.org API without rate limiting.
+See [Nominatim's usage policy](https://operations.osmfoundation.org/policies/nominatim) for more information.
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..cb9df49
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='OsmAnd Geocoder'