Initial commit master
authorJakob Cornell <_ecjqzpqwlt@jcornell.net>
Mon, 30 Sep 2019 03:33:03 +0000 (03:33 +0000)
committerJakob Cornell <_ecjqzpqwlt@jcornell.net>
Mon, 30 Sep 2019 06:10:14 +0000 (06:10 +0000)
16 files changed:
.gitignore [new file with mode: 0644]
app/.gitignore [new file with mode: 0644]
app/build.gradle [new file with mode: 0644]
app/proguard-rules.pro [new file with mode: 0644]
app/src/main/AndroidManifest.xml [new file with mode: 0644]
app/src/main/java/net/jcornell/osmandgeocoder/Application.java [new file with mode: 0644]
app/src/main/java/net/jcornell/osmandgeocoder/MainActivity.java [new file with mode: 0644]
app/src/main/java/net/jcornell/osmandgeocoder/Util.java [new file with mode: 0644]
app/src/main/res/layout/activity_main.xml [new file with mode: 0644]
app/src/main/res/layout/results_dialog.xml [new file with mode: 0644]
app/src/main/res/values/colors.xml [new file with mode: 0644]
app/src/main/res/values/strings.xml [new file with mode: 0644]
app/src/main/res/values/styles.xml [new file with mode: 0644]
build.gradle [new file with mode: 0644]
readme.md [new file with mode: 0644]
settings.gradle [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..69e8586
--- /dev/null
@@ -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 (file)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644 (file)
index 0000000..e8ceec2
--- /dev/null
@@ -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 (file)
index 0000000..f1b4245
--- /dev/null
@@ -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 (file)
index 0000000..5308b80
--- /dev/null
@@ -0,0 +1,25 @@
+<?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>
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 (file)
index 0000000..5ca4242
--- /dev/null
@@ -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<Result<JSONArray>> 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 (file)
index 0000000..2388078
--- /dev/null
@@ -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<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) {}
+               }
+       }
+}
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 (file)
index 0000000..31d7be1
--- /dev/null
@@ -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<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;
+       }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..3ec886a
--- /dev/null
@@ -0,0 +1,41 @@
+<?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>
diff --git a/app/src/main/res/layout/results_dialog.xml b/app/src/main/res/layout/results_dialog.xml
new file mode 100644 (file)
index 0000000..ebd5f5c
--- /dev/null
@@ -0,0 +1,12 @@
+<?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>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..69b2233
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..c9330cc
--- /dev/null
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">OsmAnd Geocoder</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644 (file)
index 0000000..5885930
--- /dev/null
@@ -0,0 +1,11 @@
+<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>
diff --git a/build.gradle b/build.gradle
new file mode 100644 (file)
index 0000000..f5fb2cc
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..cb9df49
--- /dev/null
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='OsmAnd Geocoder'