Bimba.git

commit e77e181b5fee1ec6c48eda701e2e7a0b262ee85c

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

Better cache

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/.idea/misc.xml b/.idea/misc.xml
index 635999df1e86791ad3787e455b4524e4d8879b93..ba7052b8197ddf8ba8756022d905d03055c7ad60 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -24,7 +24,7 @@         
       </value>
     </option>
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">




diff --git a/app/src/main/java/ml/adamsprogs/bimba/CacheManager.kt b/app/src/main/java/ml/adamsprogs/bimba/CacheManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..15832219c47408de967b6edf2b94b02992de65ae
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/CacheManager.kt
@@ -0,0 +1,111 @@
+package ml.adamsprogs.bimba
+
+import android.content.Context
+import android.content.SharedPreferences
+import ml.adamsprogs.bimba.models.Plate
+
+class CacheManager private constructor(context: Context) {
+    companion object {
+        private var manager: CacheManager? = null
+        fun getCacheManager(context: Context) : CacheManager {
+            return if (manager == null) {
+                manager = CacheManager(context)
+                manager!!
+            } else
+                manager!!
+        }
+    }
+
+    private var cachePreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cache", Context.MODE_PRIVATE)
+    private var cacheHitsPreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cacheHits", Context.MODE_PRIVATE)
+
+    private var cache: HashMap<String, Plate> = HashMap()
+    private var cacheHits: HashMap<String, Int> = HashMap()
+
+    fun hasAll(plates: HashSet<Plate>): Boolean {
+        plates
+                .filterNot { has(it) }
+                .forEach { return false }
+        return true
+    }
+
+    fun hasAny(plates: HashSet<Plate>): Boolean {
+        plates
+                .filter { has(it) }
+                .forEach { return true }
+        return false
+    }
+
+    fun has(plate: Plate): Boolean {
+        val key = "${plate.line}@${plate.stop}"
+        return cache.containsKey(key)
+    }
+
+    fun push(plates: HashSet<Plate>) {
+        val editor = cachePreferences.edit()
+        for (plate in plates) {
+            val key = "${plate.line}@${plate.stop}"
+            cache[key] = plate
+            editor.putString(key, cache[key].toString())
+        }
+        editor.apply()
+    }
+
+    fun push(plate: Plate) {
+        val editorCache = cachePreferences.edit()
+        val editorCacheHits = cacheHitsPreferences.edit()
+        if (cacheHits.size == 40) { //todo size?
+            val key = cacheHits.minBy { it.value }?.key
+            cache.remove(key)
+            editorCache.remove(key)
+            cacheHits.remove(key)
+            editorCacheHits.remove(key)
+        }
+        val key = "${plate.line}@${plate.stop}"
+        cache[key] = plate
+        cacheHits[key] = 0
+        editorCache.putString(key, plate.toString())
+        editorCacheHits.putInt(key, 0)
+        editorCache.apply()
+        editorCacheHits.apply()
+    }
+
+    fun get(plates: HashSet<Plate>): HashSet<Plate> {
+        val result = HashSet<Plate>()
+        for (plate in plates) {
+            val value = get(plate)
+            if (value == null)
+                result.add(plate)
+            else
+                result.add(get(plate)!!)
+        }
+        return result
+    }
+
+    fun get(plate: Plate): Plate? {
+        if (!has(plate))
+            return null
+        val key = "${plate.line}@${plate.stop}"
+        val hits = cacheHits[key]
+        if (hits != null)
+            cacheHits[key] = hits + 1
+        return cache[key]
+    }
+
+    fun recreate() {
+        TODO()
+    }
+
+    init {
+        cache = cacheFromString(cachePreferences.all)
+        cacheHits = cacheHitsPreferences.all as HashMap<String, Int>
+    }
+
+    private fun cacheFromString(preferences: Map<String, *>): HashMap<String, Plate> {
+        val result = HashMap<String, Plate>()
+        for ((key, value) in preferences.entries) {
+            result[key] = Plate.fromString(value as String)
+        }
+        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 1f2257b90b00f26960d288968e9383a2ae580418..2ed04860296ff1106e3e912c14de22bc2c2c50e3 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -19,7 +19,6 @@ import android.view.inputmethod.InputMethodManager
 import ml.adamsprogs.bimba.*
 import java.util.*
 import android.os.Bundle
-import android.util.Log
 import kotlinx.android.synthetic.main.activity_dash.*
 
 class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener,
@@ -74,7 +73,7 @@             super.onOptionsItemSelected(item)
         }
 
         val validity = timetable.getValidity()
-        drawerView.menu.findItem(R.id.drawer_validity).title = getString(R.string.valid_through, validity)
+        drawerView.menu.findItem(R.id.drawer_validity).title = getString(R.string.valid_since, validity)
 
         searchView = search_view
 
@@ -114,7 +113,6 @@                 searchSuggestion as StopSuggestion
                 intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_STOP)
                 intent.putExtra(StopActivity.EXTRA_STOP_ID, searchSuggestion.id)
                 intent.putExtra(StopActivity.EXTRA_STOP_SYMBOL, searchSuggestion.symbol)
-                Log.i("Profiler", "Intent sent")
                 startActivity(intent)
             }
 
@@ -158,13 +156,13 @@             override fun run() {
                 for (fav in favourites) {
                     fav.registerOnVm(receiver)
                     for (t in fav.timetables) {
-                        val symbol = timetable.getStopSymbol(t[Favourite.TAG_STOP]!!)
-                        val line = timetable.getLineNumber(t[Favourite.TAG_LINE]!!)
+                        val symbol = timetable.getStopSymbol(t.stop)
+                        val line = timetable.getLineNumber(t.line)
                         val intent = Intent(context, VmClient::class.java)
                         intent.putExtra(VmClient.EXTRA_STOP_SYMBOL, symbol)
                         intent.putExtra(VmClient.EXTRA_LINE_NUMBER, line)
                         intent.putExtra(VmClient.EXTRA_REQUESTER,
-                                "${fav.name};${t[Favourite.TAG_STOP]}${t[Favourite.TAG_LINE]}")
+                                "${fav.name};${t.stop}${t.line}")
                         context.startService(intent)
                     }
                 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
index 7a8fb57b6c09ece0b8185e464fb9abea5a5ddb9e..edd6a40349c9a34d0bb79d63b7d7a9a379e853da 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -113,11 +113,9 @@         }
 
         fab.setOnClickListener {
             if (!favourites.has(stopSymbol!!)) {
-                val items = ArrayList<HashMap<String, String>>()
+                val items = HashSet<Plate>()
                 timetable.getLines(stopId!!).forEach {
-                    val o = HashMap<String, String>()
-                    o[Favourite.TAG_STOP] = stopId!!
-                    o[Favourite.TAG_LINE] = it
+                    val o = Plate(it, stopId!!, null)
                     items.add(o)
                 }
                 favourites.add(stopSymbol as String, items)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
index 8be55d625ee4c3f580996c0cf6809352e91a3cb9..4867eed2fb2192b0728f2ae2b3acefd6ebd792df 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
@@ -1,10 +1,9 @@
 package ml.adamsprogs.bimba.models
 
-import android.util.Log
 import java.util.*
 import kotlin.collections.ArrayList
 
-data class Departure(val line: String, private val mode: String, val time: String, val lowFloor: Boolean,
+data class Departure(val line: String, val mode: String, val time: String, val lowFloor: Boolean,
                      val modification: String?, val direction: String, val vm: Boolean = false,
                      var tomorrow: Boolean = false, val onStop: Boolean = false) {
 
@@ -39,29 +38,23 @@         }
 
         fun createDepartures(stopId: String): HashMap<String, ArrayList<Departure>> {
             val timetable = Timetable.getTimetable()
-            Log.i("Profiler/Departure", "Got timetable")
             val departures = timetable.getStopDepartures(stopId)
-            Log.i("Profiler/Departure", "Got departures")
             val moreDepartures = HashMap<String, ArrayList<Departure>>()
             for ((k, v) in departures) {
                 moreDepartures[k] = ArrayList()
                 for (departure in v)
                     moreDepartures[k]!!.add(departure.copy())
             }
-            Log.i("Profiler/Departure", "Duplicated departures")
             val rolledDepartures = HashMap<String, ArrayList<Departure>>()
 
             for ((_, tomorrowDepartures) in moreDepartures) {
                 tomorrowDepartures.forEach { it.tomorrow = true }
             }
-            Log.i("Profiler/Departure", "Set tomorrow")
 
             for ((mode, _) in departures) {
                 rolledDepartures[mode] = (departures[mode] as ArrayList<Departure> +
                         moreDepartures[mode] as ArrayList<Departure>) as ArrayList<Departure>
-                Log.i("Profiler/Departure", "Joined departures for $mode")
                 rolledDepartures[mode] = filterDepartures(rolledDepartures[mode]!!)
-                Log.i("Profiler/Departure", "Filtered departures for $mode")
             }
 
             return rolledDepartures




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
index e08b3ad8e021062b0b3f9341225b007b13b904e8..eb89b9b2a1cebfecf7d681916cee29b6dd66e747 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
@@ -7,46 +7,30 @@ import ml.adamsprogs.bimba.getMode
 import java.util.*
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
+import kotlin.collections.HashSet
 
 class Favourite : Parcelable, MessageReceiver.OnVmListener {
-    override fun onVm(vmDepartures: ArrayList<Departure>?, requester: String) {
-        val requesterName = requester.split(";")[0]
-        val requesterTimetable: String = try {
-            requester.split(";")[1]
-        } catch (e: IndexOutOfBoundsException) {
-            ""
-        }
-        if (vmDepartures != null && requesterName == name) {
-            vmDeparturesMap[requesterTimetable] = vmDepartures
-            this.vmDepartures = vmDeparturesMap.flatMap { it.value } as ArrayList<Departure>
-        }
-        filterVmDepartures()
-    }
-
     private var isRegisteredOnVmListener: Boolean = false
     var name: String
         private set
-    var timetables: ArrayList<HashMap<String, String>>
+    var timetables: HashSet<Plate>
         private set
-    private var oneDayDepartures: ArrayList<HashMap<String, ArrayList<Departure>>>? = null
     private val vmDeparturesMap = HashMap<String, ArrayList<Departure>>()
     private var vmDepartures = ArrayList<Departure>()
+    val timetable = Timetable.getTimetable()
+    val size: Int
+        get() = timetables.size
 
     constructor(parcel: Parcel) {
         val array = ArrayList<String>()
         parcel.readStringList(array)
-        val timetables = ArrayList<HashMap<String, String>>()
-        for (row in array) {
-            val element = HashMap<String, String>()
-            element[TAG_STOP] = row.split("|")[0]
-            element[TAG_LINE] = row.split("|")[1]
-            timetables.add(element)
-        }
+        val timetables = HashSet<Plate>()
+        array.mapTo(timetables) { Plate.fromString(it) }
         this.name = parcel.readString()
         this.timetables = timetables
     }
 
-    constructor(name: String, timetables: ArrayList<HashMap<String, String>>) {
+    constructor(name: String, timetables: HashSet<Plate>) {
         this.name = name
         this.timetables = timetables
 
@@ -57,66 +41,19 @@         return Parcelable.CONTENTS_FILE_DESCRIPTOR
     }
 
     override fun writeToParcel(dest: Parcel?, flags: Int) {
-        val parcel = timetables.map { "${it[TAG_STOP]}|${it[TAG_LINE]}" }
+        val parcel = timetables.map { it.toString() }
         dest?.writeStringList(parcel)
         dest?.writeString(name)
     }
 
-    val timetable = Timetable.getTimetable()
-    val size: Int
-        get() = timetables.size
-
-    var nextDeparture: Departure? = null
-        get() {
-            filterVmDepartures()
-            if (timetables.isEmpty() && vmDepartures.isEmpty())
-                return null
-
-            if (vmDepartures.isNotEmpty()) {
-                return vmDepartures.minBy { it.timeTill() }
-            }
-
-            val twoDayDepartures = ArrayList<Departure>()
-            val today = Calendar.getInstance().getMode()
-            val tomorrowCal = Calendar.getInstance()
-            tomorrowCal.add(Calendar.DAY_OF_MONTH, 1)
-            val tomorrow = tomorrowCal.getMode()
-
-            if (oneDayDepartures == null) {
-                oneDayDepartures = ArrayList()
-                timetables.mapTo(oneDayDepartures!!) { timetable.getStopDepartures(it[TAG_STOP] as String, it[TAG_LINE]) }
-            }
-
-            oneDayDepartures!!.forEach {
-                it[today]!!.forEach {
-                    twoDayDepartures.add(it.copy())
-                }
-            }
-            oneDayDepartures!!.forEach {
-                it[tomorrow]!!.forEach {
-                    val d = it.copy()
-                    d.tomorrow = true
-                    twoDayDepartures.add(d)
-                }
-            }
-
-            if (twoDayDepartures.isEmpty())
-                return null
-
-            return twoDayDepartures
-                    .filter { it.timeTill() >= 0 }
-                    .minBy { it.timeTill() }
-        }
-        private set
-
     private fun filterVmDepartures() {
         this.vmDepartures
                 .filter { it.timeTill() < 0 }
                 .forEach { this.vmDepartures.remove(it) }
     }
 
-    fun delete(stop: String, line: String) {
-        timetables.remove(timetables.find { it[TAG_STOP] == stop && it[TAG_LINE] == line })
+    fun delete(plate: Plate) {
+        timetables.remove(timetables.find { it.stop == plate.stop && it.line == plate.line })
     }
 
     fun registerOnVm(receiver: MessageReceiver) {
@@ -138,9 +75,38 @@
         override fun newArray(size: Int): Array<Favourite?> {
             return arrayOfNulls(size)
         }
+    }
 
-        val TAG_STOP = "stop"
-        val TAG_LINE = "line"
+    fun nextDeparture(): Departure? {
+        filterVmDepartures()
+        if (timetables.isEmpty() && vmDepartures.isEmpty())
+            return null
+
+        if (vmDepartures.isNotEmpty()) {
+            return vmDepartures.minBy { it.timeTill() }
+        }
+
+        val today = Calendar.getInstance().getMode()
+        val tomorrowCal = Calendar.getInstance()
+        tomorrowCal.add(Calendar.DAY_OF_MONTH, 1)
+        val tomorrow = tomorrowCal.getMode()
+
+        val departures = timetable.getStopDepartures(timetables)
+        val todayDepartures = departures[today]!!
+        val tomorrowDepartures = ArrayList<Departure>()
+        val twoDayDepartures = ArrayList<Departure>()
+        departures[tomorrow]!!.mapTo(tomorrowDepartures) {it.copy()}
+        tomorrowDepartures.forEach {it.tomorrow = true}
+
+        todayDepartures.forEach {twoDayDepartures.add(it)}
+        tomorrowDepartures.forEach {twoDayDepartures.add(it)}
+
+        if (twoDayDepartures.isEmpty())
+            return null
+
+        return twoDayDepartures
+                .filter { it.timeTill() >= 0 }
+                .minBy { it.timeTill() }
     }
 
     fun allDepartures(): HashMap<String, ArrayList<Departure>>? {
@@ -149,5 +115,19 @@     }
 
     fun fullTimetable(): HashMap<String, ArrayList<Departure>>? {
         TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+    }
+
+    override fun onVm(vmDepartures: ArrayList<Departure>?, requester: String) {
+        val requesterName = requester.split(";")[0]
+        val requesterTimetable: String = try {
+            requester.split(";")[1]
+        } catch (e: IndexOutOfBoundsException) {
+            ""
+        }
+        if (vmDepartures != null && requesterName == name) {
+            vmDeparturesMap[requesterTimetable] = vmDepartures
+            this.vmDepartures = vmDeparturesMap.flatMap { it.value } as ArrayList<Departure>
+        }
+        filterVmDepartures()
     }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
index 0b729b648a91f5108e529d17b6f5c87b29a515cb..a2cc93ae6b51b9f6ac2e3e6cce371c2621f9fd3a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
@@ -17,18 +17,17 @@
     override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
         val timetable = Timetable.getTimetable()
         val favourites = FavouriteStorage.getFavouriteStorage()
-        val favouriteElement = timetable.getFavouriteElement(favourite.timetables[position][Favourite.TAG_STOP]!!,
-                favourite.timetables[position][Favourite.TAG_LINE]!!)
+        val plate = Plate(favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].line,
+                favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].stop, null)
+        val favouriteElement = timetable.getFavouriteElement(plate)
         holder?.rowTextView?.text = favouriteElement
         holder?.splitButton?.setOnClickListener {
-            favourites.detach(favourite.name, favourite.timetables[position][Favourite.TAG_STOP]!!,
-                    favourite.timetables[position][Favourite.TAG_LINE]!!, favouriteElement)
+            favourites.detach(favourite.name, plate, favouriteElement)
             favourite = favourites.favourites[favourite.name]!!
             notifyDataSetChanged()
         }
         holder?.deleteButton?.setOnClickListener {
-            favourites.delete(favourite.name, favourite.timetables[position][Favourite.TAG_STOP]!!,
-                    favourite.timetables[position][Favourite.TAG_LINE]!!)
+            favourites.delete(favourite.name, plate)
             favourite = favourites.favourites[favourite.name]!!
             notifyDataSetChanged()
         }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
index 8ba9f57fafb2e926b510afa6bf3a56824c25f0d3..adee9273c35f60c60c229eb2cb6476988492d678 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
@@ -2,6 +2,7 @@ package ml.adamsprogs.bimba.models
 
 import android.content.Context
 import android.content.SharedPreferences
+import android.util.Log
 import com.google.gson.Gson
 import com.google.gson.JsonArray
 import com.google.gson.JsonObject
@@ -34,13 +35,8 @@     init {
         val favouritesString = preferences.getString("favourites", "{}")
         val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
         for ((name, jsonTimetables) in favouritesMap.entrySet()) {
-            val timetables = ArrayList<HashMap<String, String>>()
-            for (jsonTimetable in jsonTimetables.asJsonArray) {
-                val timetable = HashMap<String, String>()
-                timetable[Favourite.TAG_STOP] = jsonTimetable.asJsonObject[Favourite.TAG_STOP].asString
-                timetable[Favourite.TAG_LINE] = jsonTimetable.asJsonObject[Favourite.TAG_LINE].asString
-                timetables.add(timetable)
-            }
+            val timetables = HashSet<Plate>()
+            jsonTimetables.asJsonArray.mapTo(timetables) { Plate(it.asJsonObject["line"].asString, it.asJsonObject["stop"].asString, null) }
             favourites[name] = Favourite(name, timetables)
         }
     }
@@ -49,7 +45,7 @@     override fun iterator(): Iterator = favourites.values.iterator()
 
     fun has(name: String): Boolean = favourites.contains(name)
 
-    fun add(name: String, timetables: ArrayList<HashMap<String, String>>) {
+    fun add(name: String, timetables: HashSet<Plate>) {
         if (favourites[name] == null) {
             favourites[name] = Favourite(name, timetables)
             serialize()
@@ -68,8 +64,8 @@         favourites.remove(name)
         serialize()
     }
 
-    fun delete(name: String, stop: String, line: String) {
-        favourites[name]?.delete(stop, line)
+    fun delete(name: String, plate: Plate) {
+        favourites[name]?.delete(plate)
         serialize()
     }
 
@@ -79,8 +75,8 @@         for ((name, favourite) in favourites) {
             val timetables = JsonArray()
             for (timetable in favourite.timetables) {
                 val element = JsonObject()
-                element.addProperty(Favourite.TAG_STOP, timetable[Favourite.TAG_STOP])
-                element.addProperty(Favourite.TAG_LINE, timetable[Favourite.TAG_LINE])
+                element.addProperty("stop", timetable.stop)
+                element.addProperty("line", timetable.line)
                 timetables.add(element)
             }
             rootObject.add(name, timetables)
@@ -89,24 +85,22 @@         val favouritesString = Gson().toJson(rootObject)
         val editor = preferences.edit()
         editor.putString("favourites", favouritesString)
         editor.apply()
+
     }
 
-    fun detach(name: String, stop: String, line: String, newName: String) {
-        val element = HashMap<String, String>()
-        element[Favourite.TAG_STOP] = stop
-        element[Favourite.TAG_LINE] = line
-        val array = ArrayList<HashMap<String, String>>()
-        array.add(element)
+    fun detach(name: String, plate: Plate, newName: String) {
+        val array = HashSet<Plate>()
+        array.add(plate)
         favourites[newName] = Favourite(newName, array)
         serialize()
 
-        delete(name, stop, line)
+        delete(name, plate)
     }
 
     fun merge(names: ArrayList<String>) {
         if (names.size < 2)
             return
-        val newFavourite = Favourite(names[0], ArrayList())
+        val newFavourite = Favourite(names[0], HashSet())
         for (name in names) {
             newFavourite.timetables.addAll(favourites[name]!!.timetables)
             favourites.remove(name)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
index f430213010b49f50058dd0f571ee258ce991bf63..ebbebcbddff627f6d8cf6f741e6adef300005e8b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
@@ -53,7 +53,7 @@         thread {
             val favourite = favourites[position]
             val nextDeparture: Departure?
             try {
-                nextDeparture = favourite.nextDeparture
+                nextDeparture = favourite.nextDeparture()
             } catch (e: ConcurrentModificationException) {
                 return@thread
             }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c62e6982c1416e0b414e513d1be5d9563e2f0dd6
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
@@ -0,0 +1,45 @@
+package ml.adamsprogs.bimba.models
+
+import android.util.Log
+
+data class Plate(val line: String, val stop: String, val departures: HashMap<String, HashSet<Departure>>?) {
+    override fun toString(): String {
+        var result = "$line=$stop={"
+        if (departures != null) {
+            for ((_, column) in departures)
+                for (departure in column) {
+                    result += departure.toString() + ";"
+                }
+        }
+        result += "}"
+        return result
+    }
+
+    companion object {
+        fun fromString(string: String): Plate {
+            val s = string.split("=")
+            val departures = HashMap<String, HashSet<Departure>>()
+            for (d in s[2].replace("{", "").replace("}", "").split(";")) {
+                if (d == "")
+                    continue
+                val dep = Departure.fromString(d)
+                if (departures[dep.mode] == null)
+                    departures[dep.mode] = HashSet()
+                departures[dep.mode]!!.add(dep)
+            }
+            return Plate(s[0], s[1], departures)
+        }
+
+        fun join(set: HashSet<Plate>): HashMap<String, ArrayList<Departure>> {
+            val departures = HashMap<String, ArrayList<Departure>>()
+            for (plate in set) {
+                for ((mode, d) in plate.departures!!) {
+                    if (departures[mode] == null)
+                        departures[mode] = ArrayList()
+                    departures[mode]!!.addAll(d.sortedBy { it.time })
+                }
+            }
+            return departures
+        }
+    }
+}
\ No newline at end of file




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 e36ad93ecce2365dc879c8ebd9bde571e70ab06b..2ca7bd58bf3a7055d735292963e1015965874a92 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -5,6 +5,8 @@ import android.database.CursorIndexOutOfBoundsException
 import android.database.sqlite.SQLiteCantOpenDatabaseException
 import android.database.sqlite.SQLiteDatabase
 import android.database.sqlite.SQLiteDatabaseCorruptException
+import android.util.Log
+import ml.adamsprogs.bimba.CacheManager
 import java.io.File
 
 
@@ -23,15 +25,16 @@                     val db: SQLiteDatabase?
                     try {
                         db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
                                 null, SQLiteDatabase.OPEN_READONLY)
-                    } catch(e: NoSuchFileException) {
+                    } catch (e: NoSuchFileException) {
                         throw SQLiteCantOpenDatabaseException("no such file")
-                    } catch(e: SQLiteCantOpenDatabaseException) {
+                    } catch (e: SQLiteCantOpenDatabaseException) {
                         throw SQLiteCantOpenDatabaseException("cannot open db")
-                    } catch(e: SQLiteDatabaseCorruptException) {
+                    } catch (e: SQLiteDatabaseCorruptException) {
                         throw SQLiteCantOpenDatabaseException("db corrupt")
                     }
                     timetable = Timetable()
                     timetable!!.db = db
+                    timetable!!.cacheManager = CacheManager.getCacheManager(context)
                     return timetable as Timetable
                 } else
                     throw IllegalArgumentException("new timetable requested and no context given")
@@ -41,27 +44,24 @@         }
     }
 
     lateinit var db: SQLiteDatabase
+    private lateinit var cacheManager: CacheManager
     private var _stops: ArrayList<StopSuggestion>? = null
-    private val _stopDepartures = HashMap<String, HashMap<String, ArrayList<Departure>>>()
-    private val _stopDeparturesCount = HashMap<String, Int>()
 
     fun refresh(context: Context) {
         val db: SQLiteDatabase?
         try {
             db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
                     null, SQLiteDatabase.OPEN_READONLY)
-        } catch(e: NoSuchFileException) {
+        } catch (e: NoSuchFileException) {
             throw SQLiteCantOpenDatabaseException("no such file")
-        } catch(e: SQLiteCantOpenDatabaseException) {
+        } catch (e: SQLiteCantOpenDatabaseException) {
             throw SQLiteCantOpenDatabaseException("cannot open db")
-        } catch(e: SQLiteDatabaseCorruptException) {
+        } catch (e: SQLiteDatabaseCorruptException) {
             throw SQLiteCantOpenDatabaseException("db corrupt")
         }
         this.db = db
 
-        for ((k, _) in _stopDepartures)
-            _stopDepartures.remove(k)
-        //todo recreate cache
+        //todo cacheManager.recreate()
     }
 
     fun getStops(): ArrayList<StopSuggestion> {
@@ -106,44 +106,80 @@         cursor.close()
         return number
     }
 
-    fun getStopDepartures(stopId: String, lineId: String? = null, tomorrow: Boolean = false): HashMap<String, ArrayList<Departure>> {
-        val andLine: String = if (lineId == null)
-            ""
-        else
-            "and line_id = '$lineId'"
+    fun getStopDepartures(stopId: String, lineId: String? = null): HashMap<String, ArrayList<Departure>> {
+        val plates = HashSet<Plate>()
 
-        if (lineId == null && _stopDepartures.contains(stopId)) {
-            _stopDeparturesCount[stopId] = _stopDeparturesCount[stopId]!! + 1
-            return _stopDepartures[stopId]!!
+        if (lineId == null) {
+            for (line in getLinesForStop(stopId)) {
+                val plate = Plate(line, stopId, null)
+                if (cacheManager.has(plate))
+                    plates.add(cacheManager.get(plate)!!)
+                else {
+                    val p = Plate(line, stopId, getStopDeparturesByLine(line, stopId)) //fixme to one query
+                    plates.add(p)
+                    cacheManager.push(p)
+                }
+            }
+        } else {
+            val plate = Plate(lineId, stopId, null)
+            if (cacheManager.has(plate))
+                plates.add(cacheManager.get(plate)!!)
+            else {
+                val p = Plate(lineId, stopId, getStopDeparturesByLine(lineId, stopId))
+                plates.add(p)
+                cacheManager.push(p)
+            }
         }
-        _stopDeparturesCount[stopId] = _stopDeparturesCount[stopId]?:0 + 1
+
+        return Plate.join(plates)
+    }
+
+    fun getStopDepartures(plates: HashSet<Plate>): HashMap<String, ArrayList<Departure>> {
+        val result = HashSet<Plate>()
+        val toGet = HashSet<Plate>()
+
+        for (plate in plates) {
+            if (cacheManager.has(plate))
+                result.add(cacheManager.get(plate)!!)
+            else
+                toGet.add(plate)
+        }
+
+        result.addAll(getStopDeparturesByPlates(toGet))
+
+        return Plate.join(result)
+    }
+
+    private fun getStopDeparturesByPlates(plates: HashSet<Plate>): HashSet<Plate> {
+        val result = HashSet<Plate>()
+        plates.mapTo(result) { Plate(it.line, it.stop, getStopDeparturesByLine(it.line, it.stop)) }  //fixme to one query
+        return result
+    }
+
+    private fun getLinesForStop(stopId: String): HashSet<String> {
+        val cursor = db.rawQuery("select line_id from timetables where stop_id=?;", listOf(stopId).toTypedArray())
+        val lines = HashSet<String>()
+        while (cursor.moveToNext())
+            lines.add(cursor.getString(0))
+        cursor.close()
+        return lines
+    }
+
+    private fun getStopDeparturesByLine(lineId: String, stopId: String): HashMap<String, HashSet<Departure>>? {
         val cursor = db.rawQuery("select lines.number, mode, substr('0'||hour, -2) || ':' || " +
                 "substr('0'||minute, -2) as time, lowFloor, modification, headsign from departures join " +
                 "timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) where " +
-                "stop_id = ? $andLine order by mode, time;", listOf(stopId).toTypedArray())
-        val departures = HashMap<String, ArrayList<Departure>>()
-        departures.put(MODE_WORKDAYS, ArrayList())
-        departures.put(MODE_SATURDAYS, ArrayList())
-        departures.put(MODE_SUNDAYS, ArrayList())
+                "stop_id = ? and line_id = ? order by mode, time;", listOf(stopId, lineId).toTypedArray())
+        val departures = HashMap<String, HashSet<Departure>>()
+        departures.put(MODE_WORKDAYS, HashSet())
+        departures.put(MODE_SATURDAYS, HashSet())
+        departures.put(MODE_SUNDAYS, HashSet())
         while (cursor.moveToNext()) { //fixme first moveToNext takes 2s, subsequent ones are instant
             departures[cursor.getString(1)]?.add(Departure(cursor.getString(0),
                     cursor.getString(1), cursor.getString(2), cursor.getInt(3) == 1,
-                    cursor.getString(4), cursor.getString(5), tomorrow = tomorrow))
+                    cursor.getString(4), cursor.getString(5)))
         }
         cursor.close()
-        if (lineId == null) {
-            if (_stopDepartures.size < 10)
-                _stopDepartures[stopId] = departures
-            else {
-                for ((key, value) in _stopDeparturesCount) {
-                    if (value < _stopDeparturesCount[stopId]!!) {
-                        _stopDepartures.remove(key)
-                        _stopDepartures[stopId] = departures
-                        break
-                    }
-                }
-            }
-        }
         return departures
     }
 
@@ -159,12 +195,17 @@         cursor.close()
         return lines
     }
 
-    fun getFavouriteElement(stop: String, line: String): String {
+    fun getFavouriteElement(plate: Plate): String {
+        val q = "select name || ' (' || stops.symbol || stops.number || '): \n' " +
+                "|| lines.number || ' → ' || headsign from timetables join stops on (stops.id = stop_id) " +
+                "join lines on(lines.id = line_id) join nodes on(nodes.symbol = stops.symbol) where " +
+                "stop_id = ${plate.stop} and line_id = ${plate.line}"
+
         val cursor = db.rawQuery("select name || ' (' || stops.symbol || stops.number || '): \n' " +
                 "|| lines.number || ' → ' || headsign from timetables join stops on (stops.id = stop_id) " +
                 "join lines on(lines.id = line_id) join nodes on(nodes.symbol = stops.symbol) where " +
                 "stop_id = ? and line_id = ?",
-                listOf(stop, line).toTypedArray())
+                listOf(plate.stop, plate.line).toTypedArray())
         val element: String
         cursor.moveToNext()
         element = cursor.getString(0)
@@ -178,7 +219,7 @@         try {
             cursor.moveToNext()
             cursor.getString(0)
             cursor.close()
-        } catch(e: CursorIndexOutOfBoundsException) {
+        } catch (e: CursorIndexOutOfBoundsException) {
             return true
         }
         return false
@@ -192,3 +233,4 @@         cursor.close()
         return "%s-%s-%s".format(validity.substring(0..3), validity.substring(4..5), validity.substring(6..7))
     }
 }
+




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8f3e09faf5ea5ec104651267b31f7897dd7c334e..24540c50b4f52056f76e6445b065aee643c9a272 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -66,7 +66,7 @@         "it’s Tuesday, it will be on ‘workdays’ tab).\n"
         "Be sure to consult the messages on\nhttps://www.ztm.poznan.pl/en.\n\n"
     </string>
     <string name="departure_row_getting_departures">Getting departures…</string>
-    <string name="valid_through">Valid since: %1$s</string>
+    <string name="valid_since">Valid since %1$s</string>
     <string name="departure_floor" translatable="false">departure floor type (lowFloor)</string>
     <string name="departure_info" translatable="false">departure info icon</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 60e683c4bf3f682680d82ee5d72896b2daa1dba4..fcd45d495808aa9f80b0104e9bf71dcb705d0ac5 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -32,7 +32,7 @@     Ładowanie…
     <string name="departure_in__singular_genitive">Za %1$s minutę</string>
     <string name="departure_in__plural_genitive">Za %1$s minut</string>
     <string name="departure_in__plural_nominative">Za %1$s minuty</string>
-    <string name="home">Dom</string>
+    <string name="home">Strona główna</string>
     <string name="refresh">Odśwież</string>
     <string name="help">Pomoc</string>
     <string name="title_activity_help">Pomoc</string>
@@ -56,5 +56,5 @@         "zakładce (jeśli jest wtorek, to w „dni robocze”).\n"
         "Pamiętaj, aby sprawdzić aktualności na\nhttps://www.ztm.poznan.pl.\n\n"
     </string>
     <string name="departure_row_getting_departures">Zbieranie odjazdów…</string>
-    <string name="valid_through">Ważny od %1$s</string>
+    <string name="valid_since">Ważny od %1$s</string>
 </resources>
\ No newline at end of file