--- /dev/null
+*.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
--- /dev/null
+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'
+}
--- /dev/null
+# 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
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="net.jcornell.osmandgeocoder"
+>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application
+ android:name=".Application"
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ >
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+ <!--
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ -->
+</manifest>
--- /dev/null
+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<Result<JSONArray>> results = new LinkedBlockingQueue<>();
+}
--- /dev/null
+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<JSONArray> result) {
+ if (result instanceof Result.Ok) {
+ JSONArray data = ((Result.Ok<JSONArray>) 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<Entry> 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<Entry> 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<JSONArray> 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<JSONArray> 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) {}
+ }
+ }
+}
--- /dev/null
+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<A> {
+ public static class Ok<B> implements Result<B> {
+ 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<Byte> 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;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:layout_height = "match_parent"
+ android:layout_width = "match_parent"
+ android:orientation = "vertical"
+ >
+ <EditText
+ android:id = "@+id/addr_entry"
+ android:layout_width = "match_parent"
+ android:layout_height = "wrap_content"
+ android:lines = "3"
+ android:hint = "9303 Lyon Drive, Hill Valley, CA"
+ />
+ <Button
+ android:id = "@+id/go_button"
+ android:layout_width = "match_parent"
+ android:layout_height = "wrap_content"
+ android:text = "search"
+ />
+ <TextView
+ android:id = "@+id/status_text"
+ android:layout_width = "match_parent"
+ android:layout_height = "wrap_content"
+ android:gravity = "center"
+ />
+ <ProgressBar
+ android:id = "@+id/progress"
+ android:layout_width = "match_parent"
+ android:layout_height = "wrap_content"
+ android:gravity = "center"
+ android:visibility = "invisible"
+ />
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
--- /dev/null
+<?xml version = "1.0" encoding = "utf-8" ?>
+<LinearLayout
+ xmlns:android = "http://schemas.android.com/apk/res/android"
+ android:layout_width = "wrap_content"
+ android:layout_height = "wrap_content"
+>
+ <ListView
+ android:id = "@+id/results_list"
+ android:layout_width = "wrap_content"
+ android:layout_height = "fill_parent"
+ />
+</LinearLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
--- /dev/null
+<resources>
+ <string name="app_name">OsmAnd Geocoder</string>
+</resources>
--- /dev/null
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
--- /dev/null
+// 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
+}
--- /dev/null
+# 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.
--- /dev/null
+include ':app'
+rootProject.name='OsmAnd Geocoder'