Bimba.git

commit 83b430a2f77d5ecb4d829f50a00d70e6c31c14cf

Author: Adam Pioterek <adam.pioterek@protonmail.ch>

suggestions from VM without offline timetable

 app/build.gradle | 2 
 app/src/main/AndroidManifest.xml | 2 
 app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt | 57 
 app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt | 65 
 app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt | 10 
 app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt | 95 
 app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt | 47 
 app/src/main/res/drawable/icon_dev.xml | 17 


diff --git a/app/build.gradle b/app/build.gradle
index ceec2719de9cdc6a9df162cb04300385ca2912b3..d20a839f1360318e2dce6b81ef8d8c2fdb0a44aa 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,7 +6,7 @@ android {
     compileSdkVersion 27
     buildToolsVersion "28.0.1"
     defaultConfig {
-        applicationId "ml.adamsprogs.bimba"
+        applicationId "ml.adamsprogs.bimba.scheduleless"
         minSdkVersion 19
         targetSdkVersion 27
         versionCode 14




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5f4681fca49b83a45a0610ee2e176da39900d4ec..40d8d758b7391a5f9ee273336c2185a987c9c309 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,7 @@     
 
     <application
         android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
+        android:icon="@drawable/icon_dev"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">




diff --git a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9f8e1300ab3a7d260f9feaf34af0d674e668ed85
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
@@ -0,0 +1,57 @@
+package ml.adamsprogs.bimba
+
+import android.content.Context
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.*
+import ml.adamsprogs.bimba.datasources.VmStopsClient
+import ml.adamsprogs.bimba.models.Timetable
+import ml.adamsprogs.bimba.models.suggestions.*
+
+class ProviderProxy(context: Context) {
+    private val vmStopsClient = VmStopsClient.getVmStopClient()
+    private val timetable: Timetable = Timetable.getTimetable(context)
+    private var suggestions = emptyList<GtfsSuggestion>()
+
+    fun getSuggestions(query: String = "", callback: (List<GtfsSuggestion>) -> Unit) {
+        launch(UI) {
+            suggestions = withContext(DefaultDispatcher) {
+                getStopSuggestions(query) //+ getLineSuggestions(query) //todo<p:v+1> + bike stations, train stations, &c
+            }
+            callback(filterSuggestions(query))
+        }
+    }
+
+    private suspend fun getStopSuggestions(query: String): List<StopSuggestion> {
+        val vmSuggestions = withContext(DefaultDispatcher) {
+            vmStopsClient.getStops(query)
+        }
+
+        return if (vmSuggestions.isEmpty()) {
+            if (timetable.isEmpty())
+                emptyList()
+            else
+                timetable.getStopSuggestions()
+        } else {
+            vmSuggestions
+        }
+    }
+
+    private fun filterSuggestions(query: String): List<GtfsSuggestion> {
+        return suggestions.filter {
+            deAccent(it.name).contains(deAccent(query), true)
+        }
+    }
+
+    private fun deAccent(str: String): String {
+        var result = str.replace('ę', 'e', true)
+        result = result.replace('ó', 'o', true)
+        result = result.replace('ą', 'a', true)
+        result = result.replace('ś', 's', true)
+        result = result.replace('ł', 'l', true)
+        result = result.replace('ż', 'z', true)
+        result = result.replace('ź', 'z', true)
+        result = result.replace('ć', 'c', true)
+        result = result.replace('ń', 'n', true)
+        return result
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
index b3389a44b0e630c67900e4fd810e80964521eae8..e66054a84ced3c7d34d383ff10b4e251925955cc 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -3,8 +3,8 @@
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.*
-import android.database.sqlite.SQLiteException
 import android.os.*
+import android.preference.PreferenceManager.getDefaultSharedPreferences
 import android.support.design.widget.*
 import android.support.v4.widget.*
 import android.support.v7.widget.*
@@ -12,7 +12,6 @@ import android.support.v7.app.*
 import android.text.Html
 import android.view.*
 import android.view.inputmethod.InputMethodManager
-import kotlin.concurrent.thread
 import kotlin.collections.ArrayList
 import kotlinx.android.synthetic.main.activity_dash.*
 import java.util.*
@@ -34,7 +33,7 @@         FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener,
         FavouritesAdapter.ViewHolder.OnClickListener {
     val context: Context = this
     private val receiver = MessageReceiver.getMessageReceiver()
-    var timetable: Timetable? = null
+    private lateinit var timetable: Timetable
     private var suggestions: List<GtfsSuggestion>? = null
     private lateinit var drawerLayout: DrawerLayout
     private lateinit var drawerView: NavigationView
@@ -45,6 +44,7 @@     private lateinit var adapter: FavouritesAdapter
     private val actionModeCallback = ActionModeCallback()
     private var actionMode: ActionMode? = null
     private var isWarned = false
+    private lateinit var providerProxy: ProviderProxy
 
     companion object {
         const val REQUEST_EDIT_FAVOURITE = 1
@@ -56,11 +56,8 @@         setContentView(R.layout.activity_dash)
 
         setSupportActionBar(toolbar)
 
-        timetable = try {
-            Timetable.getTimetable(this)
-        } catch (e: SQLiteException) {
-            null
-        }
+        providerProxy = ProviderProxy(this)
+        timetable = Timetable.getTimetable()
 
         getSuggestions()
 
@@ -99,7 +96,9 @@
         searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener {
             override fun onFocus() {
                 favouritesList.visibility = View.GONE
-                filterSuggestions(searchView.query)
+                providerProxy.getSuggestions(searchView.query) {
+                    searchView.swapSuggestions(it)
+                }
             }
 
             override fun onFocusCleared() {
@@ -110,7 +109,9 @@
         searchView.setOnQueryChangeListener { oldQuery, newQuery ->
             if (oldQuery != "" && newQuery == "")
                 searchView.clearSuggestions()
-            filterSuggestions(newQuery)
+            providerProxy.getSuggestions(newQuery) {
+                searchView.swapSuggestions(it)
+            }
         }
 
         searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener {
@@ -152,40 +153,33 @@         searchView.attachNavigationDrawerToMenuButton(drawer_layout as DrawerLayout)
     }
 
     private fun showValidityInDrawer() {
-        if (timetable == null) {
+        if (timetable.isEmpty()) {
             drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.validity_offline_unavailable)
         } else {
             val formatter = DateFormat.getDateInstance(DateFormat.SHORT)
-            var calendar = calendarFromIsoD(timetable!!.getValidSince())
+            var calendar = calendarFromIsoD(timetable.getValidSince())
             formatter.timeZone = calendar.timeZone
             drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, formatter.format(calendar.time))
-            calendar = calendarFromIsoD(timetable!!.getValidTill())
+            calendar = calendarFromIsoD(timetable.getValidTill())
             formatter.timeZone = calendar.timeZone
             drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, formatter.format(calendar.time))
         }
     }
 
-    private fun filterSuggestions(newQuery: String) {
-        thread {
-            val newStops = suggestions!!.filter { deAccent(it.name).contains(deAccent(newQuery), true) } //todo<p:2> sorted by similarity
-            runOnUiThread { searchView.swapSuggestions(newStops) }
-        }
-    }
-
     private fun warnTimetableValidity() {
         if (isWarned)
             return
         isWarned = true
-        if (timetable == null)
+        if (timetable.isEmpty())
             return
-        val validTill = timetable!!.getValidTill()
+        val validTill = timetable.getValidTill()
         val today = Calendar.getInstance().toIsoDate()
         val tomorrow = Calendar.getInstance().apply {
             this.add(Calendar.DAY_OF_MONTH, 1)
         }.toIsoDate()
 
         try {
-            timetable!!.getServiceForToday()
+            timetable.getServiceForToday()
             if (today > validTill) {
                 notifyTimetableValidity(-1)
                 suggestions = ArrayList()
@@ -202,7 +196,7 @@             return
         }
 
         try {
-            timetable!!.getServiceForTomorrow()
+            timetable.getServiceForTomorrow()
             if (tomorrow == validTill) {
                 notifyTimetableValidity(1)
                 return
@@ -246,10 +240,9 @@         favouritesList.adapter.notifyDataSetChanged()
     }
 
     private fun getSuggestions() {
-        suggestions = if (timetable != null)
-            (timetable!!.getStopSuggestions(context)).sorted() //+ timetable.getLineSuggestions()).sorted() //todo<p:v+1> + bike stations, train stations, &c
-        else
-            emptyList()
+        providerProxy.getSuggestions {
+            searchView.swapSuggestions(it)
+        }
     }
 
     private fun prepareListeners() {
@@ -262,7 +255,8 @@         favourites.registerOnVm(receiver, context)
     }
 
     private fun startDownloaderService() {
-        startService(Intent(context, TimetableDownloader::class.java))
+        if (getDefaultSharedPreferences(this).getBoolean("automatic timetable updates", false))
+            startService(Intent(context, TimetableDownloader::class.java))
     }
 
     override fun onBackPressed() {
@@ -286,19 +280,6 @@         super.onDestroy()
         receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
         favourites.deregisterOnVm(receiver, context)
         unregisterReceiver(receiver)
-    }
-
-    private fun deAccent(str: String): String {
-        var result = str.replace('ę', 'e', true)
-        result = result.replace('ó', 'o', true)
-        result = result.replace('ą', 'a', true)
-        result = result.replace('ś', 's', true)
-        result = result.replace('ł', 'l', true)
-        result = result.replace('ż', 'z', true)
-        result = result.replace('ź', 'z', true)
-        result = result.replace('ć', 'c', true)
-        result = result.replace('ń', 'n', true)
-        return result
     }
 
     override fun onTimetableDownload(result: String?) {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
index 5a2d486aa09d33eae44c5d3d1686830a8adcbb2d..e7c3e0085a8ad06f180703cd533e0848579b9752 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
@@ -14,15 +14,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO)
-        try {
-            val timetable = Timetable.getTimetable(this)
-            if (timetable.isEmpty())
-                startActivity(Intent(this, NoDbActivity::class.java))
-            else
-                startActivity(Intent(this, DashActivity::class.java))
-        } catch (e:Exception) {
-            startActivity(Intent(this, NoDbActivity::class.java))
-        }
+        startActivity(Intent(this, DashActivity::class.java))
         finish()
     }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a2d47d4e31c9d91aada6130f5b58a2dbcb0303b1
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt
@@ -0,0 +1,95 @@
+package ml.adamsprogs.bimba.datasources
+
+import com.google.gson.*
+import kotlinx.coroutines.experimental.*
+import ml.adamsprogs.bimba.models.Plate
+import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
+import ml.adamsprogs.bimba.models.suggestions.*
+import okhttp3.*
+import java.io.IOException
+import java.util.*
+import kotlin.collections.HashMap
+import kotlin.collections.HashSet
+
+class VmStopsClient {
+    companion object {
+        private var vmStopsClient: VmStopsClient? = null
+
+        fun getVmStopClient(): VmStopsClient {
+            if (vmStopsClient == null)
+                vmStopsClient = VmStopsClient()
+            return vmStopsClient!!
+        }
+    }
+
+    /*suspend fun getBollardsByStopPoint(name: String): Map<String, Set<String>> {
+        val response = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""")
+        println("asked for $name and got $response")
+        val rootObject = response["success"].asJsonObject["bollards"].asJsonArray
+        val result = HashMap<String, Set<String>>()
+        rootObject.forEach {
+            val code = it.asJsonObject["bollard"].asJsonObject["tag"].asString
+            result[code] = it.asJsonObject["directions"].asJsonArray.map {
+                """${it.asJsonObject["lineName"].asString} → ${it.asJsonObject["direction"].asString}"""
+            }.toSet()
+        }
+        return result
+    }
+
+    suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? {
+        val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""")
+        val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString
+
+        val bollards = getBollardsByStopPoint(name)
+        return bollards.filter {
+            it.key == code
+        }.values.flatMap {
+            it.map {
+                val (line, headsign) = it.split(" → ")
+                Plate.ID(AgencyAndId(line), AgencyAndId(code), headsign)
+            }
+        }.toSet()
+    }*/
+
+    suspend fun getStops(pattern: String): List<StopSuggestion> {
+        val response = withContext(DefaultDispatcher) {
+            makeRequest("getStopPoints", """{"pattern": "$pattern"}""")
+        }
+
+        val points = response["success"].asJsonArray.map { it.asJsonObject }
+
+        val names = HashSet<String>()
+
+        points.forEach {
+            val name = it["name"].asString
+            names.add(name)
+        }
+
+        return names.map { StopSuggestion(it, emptySet(), "", "") }
+    }
+
+    private suspend fun makeRequest(method: String, data: String): JsonObject {
+        val client = OkHttpClient()
+        val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}"
+        val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"),
+                "method=$method&p0=$data")
+        val request = okhttp3.Request.Builder()
+                .url(url)
+                .post(body)
+                .build()
+        println("makeRequest: $request")
+
+
+        val responseBody: String?
+        try {
+            responseBody = withContext(CommonPool) {
+                client.newCall(request).execute().body()?.string()
+            }
+        } catch (e: IOException) {
+            return JsonObject()
+        }
+
+
+        return Gson().fromJson(responseBody, JsonObject::class.java)
+    }
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
index ceed419ddda0ed2bca34443399301c56f5818792..4f5762e92ce634e5e4275ef8558da5615a6a1ec9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.database.*
 import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteException
 import ml.adamsprogs.bimba.*
 import ml.adamsprogs.bimba.models.gtfs.*
 import ml.adamsprogs.bimba.models.suggestions.*
@@ -37,25 +38,29 @@         private fun constructTimetable(context: Context) {
             val timetable = Timetable()
             val filesDir = context.getSecondaryExternalFilesDir()
             val dbFile = File(filesDir, "timetable.db")
-            timetable.db = SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY) // fixme can be null
+            timetable.db = try {
+                SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
+            } catch(e: SQLiteException) {
+                null
+            }
             this.timetable = timetable
         }
     }
 
-    private lateinit var db: SQLiteDatabase
+    private var db: SQLiteDatabase? = null
     private var _stops: List<StopSuggestion>? = null
 
     fun refresh() {
     }
 
-    fun getStopSuggestions(context: Context, force: Boolean = false): List<StopSuggestion> {
+    fun getStopSuggestions(/*context: Context, */force: Boolean = false): List<StopSuggestion> {
         if (_stops != null && !force)
             return _stops!!
 
         val ids = HashMap<String, HashSet<AgencyAndId>>()
         val zones = HashMap<String, String>()
 
-        val cursor = db.rawQuery("select stop_name, stop_id, zone_id from stops", null)
+        val cursor = db!!.rawQuery("select stop_name, stop_id, zone_id from stops", null)
 
         while (cursor.moveToNext()) {
             val name = cursor.getString(0)
@@ -70,20 +75,22 @@
         cursor.close()
 
         _stops = ids.map {
+            /*todo
             val colour = when (zones[it.key]) {
                 "A" -> "#${getColour(R.color.zoneA, context).toString(16)}"
                 "B" -> "#${getColour(R.color.zoneB, context).toString(16)}"
                 "C" -> "#${getColour(R.color.zoneC, context).toString(16)}"
                 else -> "#000000"
             }
-            StopSuggestion(it.key, it.value, zones[it.key]!!, colour)
+            */
+            StopSuggestion(it.key, it.value, zones[it.key]!!, "#000000")
         }.sorted()
         return _stops!!
     }
 
     fun getLineSuggestions(): List<LineSuggestion> {
         val routes = ArrayList<LineSuggestion>()
-        val cursor = db.rawQuery("select * from routes", null)
+        val cursor = db!!.rawQuery("select * from routes", null)
 
         while (cursor.moveToNext()) {
             val routeId = cursor.getString(0)
@@ -100,7 +107,7 @@         val headsigns = HashMap>>()
 
         val stopsIndex = HashMap<Int, String>()
         val where = stops.joinToString(" or ", "where ") { "stop_id = ?" }
-        var cursor = db.rawQuery("select stop_id, stop_code from stops $where", stops.map { it.toString() }.toTypedArray())
+        var cursor = db!!.rawQuery("select stop_id, stop_code from stops $where", stops.map { it.toString() }.toTypedArray())
 
         while (cursor.moveToNext()) {
             stopsIndex[cursor.getInt(0)] = cursor.getString(1)
@@ -108,7 +115,7 @@         }
 
         cursor.close()
 
-        cursor = db.rawQuery("select stop_id, route_id, trip_headsign " +
+        cursor = db!!.rawQuery("select stop_id, route_id, trip_headsign " +
                 "from stop_times natural join trips " +
                 where, stops.map { it.toString() }.toTypedArray())
 
@@ -138,7 +145,7 @@         */
     }
 
     fun getStopName(stopId: AgencyAndId): String {
-        val cursor = db.rawQuery("select stop_name from stops where stop_id = ?",
+        val cursor = db!!.rawQuery("select stop_name from stops where stop_id = ?",
                 arrayOf(stopId.id))
         cursor.moveToNext()
         val name = cursor.getString(0)
@@ -148,7 +155,7 @@         return name
     }
 
     fun getStopCode(stopId: AgencyAndId): String {
-        val cursor = db.rawQuery("select stop_code from stops where stop_id = ?",
+        val cursor = db!!.rawQuery("select stop_code from stops where stop_id = ?",
                 arrayOf(stopId.id))
         cursor.moveToNext()
         val code = cursor.getString(0)
@@ -159,7 +166,7 @@     }
 
     fun getStopDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> {
         val map = HashMap<AgencyAndId, ArrayList<Departure>>()
-        val cursor = db.rawQuery("select route_id, service_id, departure_time, " +
+        val cursor = db!!.rawQuery("select route_id, service_id, departure_time, " +
                 "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " +
                 "from stop_times natural join trips natural join routes where stop_id = ?",
                 arrayOf(stopId.id))
@@ -197,7 +204,7 @@                 "(stop_id = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')"
             } ?: listOf()
         }.joinToString(" or ")
 
-        val cursor = db.rawQuery("select route_id, service_id, departure_time, " +
+        val cursor = db!!.rawQuery("select route_id, service_id, departure_time, " +
                 "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " +
                 "from stop_times natural join trips natural join routes where $wheres", null)
 
@@ -245,7 +252,7 @@     }
 
     fun calendarToMode(serviceId: AgencyAndId): List<Int> {
         val days = ArrayList<Int>()
-        val cursor = db.rawQuery("select * from calendar where service_id = ?",
+        val cursor = db!!.rawQuery("select * from calendar where service_id = ?",
                 arrayOf(serviceId.id))
 
         cursor.moveToNext()
@@ -308,10 +315,12 @@     }
 
     @SuppressLint("Recycle")
     fun isEmpty(): Boolean {
+        if (db == null)
+            return true
         var result: Boolean
         var cursor: Cursor? = null
         try {
-            cursor = db.rawQuery("select * from feed_info", null)
+            cursor = db!!.rawQuery("select * from feed_info", null)
             result = !cursor.moveToNext()
         } catch (e: Exception) {
             result = true
@@ -322,7 +331,7 @@         return result
     }
 
     fun getValidSince(): String {
-        val cursor = db.rawQuery("select feed_start_date from feed_info", null)
+        val cursor = db!!.rawQuery("select feed_start_date from feed_info", null)
 
         cursor.moveToNext()
         val validTill = cursor.getString(0)
@@ -332,7 +341,7 @@         return validTill
     }
 
     fun getValidTill(): String {
-        val cursor = db.rawQuery("select feed_end_date from feed_info", null)
+        val cursor = db!!.rawQuery("select feed_end_date from feed_info", null)
 
         cursor.moveToNext()
         val validTill = cursor.getString(0)
@@ -355,7 +364,7 @@     }
 
     fun getServiceFor(day: Int): AgencyAndId {
         val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day + 5) % 7)]
-        val cursor = db.rawQuery("select service_id from calendar where $dayColumn = 1", null)
+        val cursor = db!!.rawQuery("select service_id from calendar where $dayColumn = 1", null)
 
         val service: Int
         cursor.moveToNext()
@@ -370,7 +379,7 @@     }
 
     fun getPlatesForStop(stop: AgencyAndId): Set<Plate.ID> {
         val plates = HashSet<Plate.ID>()
-        val cursor = db.rawQuery("select route_id, trip_headsign " +
+        val cursor = db!!.rawQuery("select route_id, trip_headsign " +
                 "from stop_times natural join trips where stop_id = ? " +
                 "group by route_id, trip_headsign", arrayOf(stop.id))
 
@@ -387,7 +396,7 @@
     fun getTripGraphs(id: AgencyAndId): Array<TripGraph> {
         val graphs = arrayOf(TripGraph(), TripGraph())
 
-        val cursor = db.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " +
+        val cursor = db!!.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " +
                 "stop_sequence, pickup_type, stop_name, zone_id " +
                 "from stop_times natural join trips natural join stops" +
                 "where route_id = ?", arrayOf(id.id))




diff --git a/app/src/main/res/drawable/icon_dev.xml b/app/src/main/res/drawable/icon_dev.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a4b238053d6fdefaa6c74cfe50a206f56b769aff
--- /dev/null
+++ b/app/src/main/res/drawable/icon_dev.xml
@@ -0,0 +1,17 @@
+<vector android:height="24dp" android:viewportHeight="293"
+    android:viewportWidth="298" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m62.925,292.677v0c-3.69,-0.897 -5.684,-3.789 -4.288,-6.482L90.846,223.472c1.296,-2.593 5.485,-3.989 9.174,-2.992v0c3.69,0.897 5.684,3.789 4.288,6.482l-32.209,62.723c-1.296,2.593 -5.385,3.989 -9.174,2.992z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m229.156,292.677v0c3.69,-0.897 5.684,-3.789 4.288,-6.482l-32.209,-62.723c-1.296,-2.593 -5.485,-3.989 -9.174,-2.992v0c-3.69,0.897 -5.684,3.789 -4.288,6.482l32.209,62.723c1.296,2.593 5.485,3.989 9.174,2.992z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m151.076,37.797 l-0.598,0.199c-1.795,0.698 -3.789,-0.299 -4.487,-2.094L136.418,9.277c-0.698,-1.795 0.299,-3.789 2.094,-4.487l0.598,-0.199c1.795,-0.698 3.789,0.299 4.487,2.094l9.573,26.625c0.698,1.895 -0.299,3.889 -2.094,4.487z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m180.294,4.79v0c0,2.094 -1.695,3.789 -3.789,3.789h-60.828c-2.094,0 -3.789,-1.695 -3.789,-3.789v0c0,-2.094 1.695,-3.789 3.789,-3.789h60.828c2.094,0 3.789,1.695 3.789,3.789z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="M218.286,237.034H73.795c-13.263,0 -23.932,-10.77 -23.932,-23.932V105.406c0,-41.184 33.406,-74.689 74.689,-74.689h43.178c41.184,0 74.689,33.406 74.689,74.689v107.696c-0.1,13.163 -10.869,23.932 -24.132,23.932z" android:strokeWidth="0.99718279"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M212.104,146.789L79.977,146.789c-5.584,0 -10.171,-4.487 -10.171,-10.171l0,-34.802c0,-16.852 13.661,-30.514 30.514,-30.514L191.761,71.302c16.852,0 30.514,13.661 30.514,30.514l0,34.802c-0.1,5.684 -4.587,10.171 -10.171,10.171z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M161.148,56.344L130.933,56.344c-3.191,0 -5.684,-2.593 -5.684,-5.684l0,0c0,-3.191 2.593,-5.684 5.684,-5.684l30.215,0c3.191,0 5.684,2.593 5.684,5.684l0,0c0,3.091 -2.593,5.684 -5.684,5.684z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M86.957,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M205.223,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/>
+</vector>