Bimba.git

commit cc07ea7272bb1fe1445d9398145f46669a01013e

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

timetable parses GTFS

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


diff --git a/.idea/dictionaries/adam.xml b/.idea/dictionaries/adam.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97ec6acaf972c3064bc9ec97c5fbf5d9c8dcb7c3
--- /dev/null
+++ b/.idea/dictionaries/adam.xml
@@ -0,0 +1,7 @@
+<component name="ProjectDictionaryState">
+  <dictionary name="adam">
+    <words>
+      <w>headsign</w>
+    </words>
+  </dictionary>
+</component>
\ No newline at end of file




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/build.gradle b/app/build.gradle
index 8273a67e3346157dafd723c20b1a0e1df89cd6de..629485cf263b63aded4055ca0b69b2cd11c74f28 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
 
 android {
-    compileSdkVersion 26
-    buildToolsVersion "26.0.2"
+    compileSdkVersion 27
+    buildToolsVersion "27.0.3"
     defaultConfig {
-        applicationId "ml.adamsprogs.bimba"
+        applicationId "ml.adamsprogs.bimba.dev"
         minSdkVersion 19
-        targetSdkVersion 26
-        versionCode 8
-        versionName "1.2.2"
+        targetSdkVersion 27
+        versionCode 9
+        versionName "2.0.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         vectorDrawables.useSupportLibrary = true
     }
@@ -26,18 +26,17 @@     implementation fileTree(dir: 'libs', include: ['*.jar'])
     androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    implementation 'com.android.support:appcompat-v7:26.1.0'
-    implementation 'com.android.support:cardview-v7:26.1.0'
-    implementation 'com.android.support:design:26.1.0'
-    implementation 'com.android.support:support-vector-drawable:26.1.0'
+    implementation 'com.android.support:appcompat-v7:27.0.2'
+    implementation 'com.android.support:cardview-v7:27.0.2'
+    implementation 'com.android.support:design:27.0.2'
+    implementation 'com.android.support:support-vector-drawable:27.0.2'
     testImplementation 'junit:junit:4.12'
     implementation 'com.android.support.constraint:constraint-layout:1.0.2'
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     implementation 'com.github.arimorty:floatingsearchview:2.1.1'
-    implementation 'org.tukaani:xz:1.6'
     implementation 'com.google.code.gson:gson:2.8.1'
     implementation 'com.squareup.okhttp3:okhttp:3.8.1'
-    //implementation 'com.gu.android:toolargetool:0.1.3@aar' // TooLargeTool
+    implementation 'org.onebusaway:onebusaway-gtfs:1.3.4'
 }
 repositories {
     maven {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
index 0a3c3a4c18ea63b371fd48371f9fd8dae21d9355..c560ea3e932514ad7de28b93dd8e2956b54a2e99 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
@@ -8,25 +8,21 @@ import android.content.Intent
 import android.support.v4.app.NotificationCompat
 import java.net.HttpURLConnection
 import java.net.URL
-import org.tukaani.xz.XZInputStream
 import java.io.*
 import java.security.MessageDigest
-import kotlin.experimental.and
 import android.app.NotificationManager
 import android.os.Build
-import ml.adamsprogs.bimba.models.Timetable
-
+import android.util.Log
+import java.util.*
 
 class TimetableDownloader : IntentService("TimetableDownloader") {
     companion object {
-        val ACTION_DOWNLOADED = "ml.adamsprogs.bimba.timetableDownloaded"
-        val EXTRA_FORCE = "force"
-        val EXTRA_RESULT = "result"
-        val RESULT_NO_CONNECTIVITY = "no connectivity"
-        val RESULT_VERSION_MISMATCH = "version mismatch"
-        val RESULT_UP_TO_DATE = "up-to-date"
-        val RESULT_DOWNLOADED = "downloaded"
-        val RESULT_VALIDITY_FAILED = "validity failed"
+        const val ACTION_DOWNLOADED = "ml.adamsprogs.bimba.timetableDownloaded"
+        const val EXTRA_FORCE = "force"
+        const val EXTRA_RESULT = "result"
+        const val RESULT_NO_CONNECTIVITY = "no connectivity"
+        const val RESULT_UP_TO_DATE = "up-to-date"
+        const val RESULT_DOWNLOADED = "downloaded"
     }
 
     private lateinit var notificationManager: NotificationManager
@@ -41,52 +37,42 @@             if (!NetworkStateReceiver.isNetworkAvailable(this)) {
                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
-            val metadataUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.meta")
-            var httpCon = metadataUrl.openConnection() as HttpURLConnection
+            val url = URL("http://ztm.poznan.pl/pl/dla-deweloperow/getGTFSFile")
+            val httpCon = url.openConnection() as HttpURLConnection
             if (httpCon.responseCode != HttpURLConnection.HTTP_OK) { //IOEXCEPTION or EOFEXCEPTION or ConnectException
                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
-            val reader = BufferedReader(InputStreamReader(httpCon.inputStream))
-            val lastModified = reader.readLine()
-            val checksum = reader.readLine()
-            size = Integer.parseInt(reader.readLine()) / 1024
-            val dbVersion = reader.readLine()
-            if (Integer.parseInt(dbVersion.split(".")[0]) > Timetable.version) {
-                sendResult(RESULT_VERSION_MISMATCH)
-                return
-            }
-            val dbFilename = reader.readLine()
+            val lastModified = httpCon.getHeaderField("Content-Disposition").split("=")[1].trim('\"').split("_")[0]
             val currentLastModified = prefs.getString("timetableLastModified", "19791012")
-            if (lastModified <= currentLastModified && !intent.getBooleanExtra(EXTRA_FORCE, false)) {
+            if (lastModified <= currentLastModified && lastModified <= today()) {
                 sendResult(RESULT_UP_TO_DATE)
                 return
             }
 
             notify(0)
 
-            val xzDbUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/$dbFilename")
-            httpCon = xzDbUrl.openConnection() as HttpURLConnection
-            if (httpCon.responseCode != HttpURLConnection.HTTP_OK) {
-                sendResult(RESULT_NO_CONNECTIVITY)
-                return
-            }
-            val xzIn = XZInputStream(httpCon.inputStream)
-            val file = File(this.filesDir, "new_timetable.db")
-            if (copyInputStreamToFile(xzIn, file, checksum)) {
-                val oldFile = File(this.filesDir, "timetable.db")
-                oldFile.delete()
-                file.renameTo(oldFile)
-                val prefsEditor = prefs.edit()
-                prefsEditor.putString("timetableLastModified", lastModified)
-                prefsEditor.apply()
-                sendResult(RESULT_DOWNLOADED)
-            } else {
-                sendResult(RESULT_VALIDITY_FAILED)
-            }
+            val file = File(this.filesDir, "new_timetable.zip")
+            copyInputStreamToFile(httpCon.inputStream, file)
+            val oldFile = File(this.filesDir, "timetable.zip")
+            oldFile.delete()
+            file.renameTo(oldFile)
+            val prefsEditor = prefs.edit()
+            prefsEditor.putString("timetableLastModified", lastModified)
+            prefsEditor.apply()
+            sendResult(RESULT_DOWNLOADED)
 
             cancelNotification()
         }
+    }
+
+    private fun today(): String {
+        val cal = Calendar.getInstance()
+        val d = cal[Calendar.DAY_OF_MONTH]
+        val m = cal[Calendar.MONTH]+1
+        val y = cal[Calendar.YEAR]
+
+        return "%d%02d%02d".format(y, m, d)
     }
 
     private fun sendResult(result: String) {
@@ -133,9 +119,8 @@     private fun cancelNotification() {
         notificationManager.cancel(42)
     }
 
-    private fun copyInputStreamToFile(ins: InputStream, file: File, checksum: String): Boolean {
+    private fun copyInputStreamToFile(ins: InputStream, file: File) {
         val md = MessageDigest.getInstance("SHA-512")
-        var hex = ""
         try {
             val out = FileOutputStream(file)
             val buf = ByteArray(5 * 1024)
@@ -155,11 +140,6 @@         } catch (e: Exception) {
             e.printStackTrace()
         } finally {
             ins.close()
-            val digest = md.digest()
-            for (i in 0 until digest.size) {
-                hex += Integer.toString((digest[i] and 0xff.toByte()) + 0x100, 16).padStart(3, '0').substring(1)
-            }
-            return checksum == hex
         }
     }
 }




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 2ac8dd863f1cfb250db50cc77115c67ff4e68247..8476cc8e26c86316784480433f0758e1a69faa40 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -73,8 +73,10 @@             drawerLayout.closeDrawer(drawerView)
             super.onOptionsItemSelected(item)
         }
 
-        val validity = timetable.getValidity()
-        drawerView.menu.findItem(R.id.drawer_validity).title = getString(R.string.valid_since, validity)
+        val validSince = timetable.getValidSince()
+        val validTill = timetable.getValidTill()
+        drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, validSince)
+        drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, validTill)
 
         searchView = search_view
 
@@ -258,7 +260,6 @@         val message: String = when (result) {
             TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.timetable_downloaded)
             TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity)
             TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date)
-            TimetableDownloader.RESULT_VALIDITY_FAILED -> getString(R.string.validity_failed)
             else -> getString(R.string.error_try_later)
         }
         Snackbar.make(findViewById(R.id.drawer_layout), message, Snackbar.LENGTH_LONG).show()




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 930d22eb15b467361c5e3ece567387ec72726189..eca00f605d8c8d27681f58d2a5cfe964aa79fe7e 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -17,6 +17,7 @@ import ml.adamsprogs.bimba.models.*
 import ml.adamsprogs.bimba.*
 import kotlin.concurrent.thread
 import kotlinx.android.synthetic.main.activity_stop.*
+import org.onebusaway.gtfs.model.AgencyAndId
 
 class StopActivity : AppCompatActivity(), MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener {
     companion object {
@@ -29,7 +30,7 @@         val SOURCE_TYPE_STOP = "stop"
         val SOURCE_TYPE_FAV = "favourite"
     }
 
-    private var stopId: String? = null
+    private var stopId: AgencyAndId? = null
     private var stopSymbol: String? = null
     private var favourite: Favourite? = null
     private var timetableType = "departure"
@@ -59,7 +60,7 @@         setSupportActionBar(toolbar)
 
         when (sourceType) {
             SOURCE_TYPE_STOP -> {
-                stopId = intent.getStringExtra(EXTRA_STOP_ID)
+                stopId = intent.getSerializableExtra(EXTRA_STOP_ID) as AgencyAndId
                 stopSymbol = intent.getStringExtra(EXTRA_STOP_SYMBOL)
                 supportActionBar?.title = timetable.getStopName(stopId!!)
             }
@@ -133,7 +134,7 @@
         fab.setOnClickListener {
             if (!favourites.has(stopSymbol!!)) {
                 val items = HashSet<Plate>()
-                timetable.getLines(stopId!!).forEach {
+                timetable.getLinesForStop(stopId!!).forEach {
                     val o = Plate(it, stopId!!, null)
                     items.add(o)
                 }
@@ -246,23 +247,22 @@     }
 
     class PlaceholderFragment : Fragment() {
 
-        override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
-                                  savedInstanceState: Bundle?): View? {
-            val rootView = inflater!!.inflate(R.layout.fragment_stop, container, false)
+        override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+            val rootView = inflater.inflate(R.layout.fragment_stop, container, false)
 
             val layoutManager = LinearLayoutManager(activity)
             val departuresList: RecyclerView = rootView.findViewById(R.id.departuresList)
             departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation))
 
-            val departures = arguments.getStringArrayList("departures")?.map { Departure.fromString(it) }
-            departuresList.adapter = DeparturesAdapter(activity, departures,
-                    arguments["relativeTime"] as Boolean)
+            val departures = arguments?.getStringArrayList("departures")?.map { Departure.fromString(it) }
+            departuresList.adapter = DeparturesAdapter(activity as Context, departures,
+                    arguments?.get("relativeTime") as Boolean)
             departuresList.layoutManager = layoutManager
             return rootView
         }
 
         companion object {
-            private val ARG_SECTION_NUMBER = "section_number"
+            private const val ARG_SECTION_NUMBER = "section_number"
 
             fun newInstance(sectionNumber: Int, departures: ArrayList<Departure>?, relativeTime: Boolean):
                     PlaceholderFragment {
@@ -286,7 +286,7 @@     inner class SectionsPagerAdapter(fm: FragmentManager, var departures: HashMap>?) : FragmentStatePagerAdapter(fm) {
 
         var relativeTime = true
 
-        override fun getItemPosition(obj: Any?): Int {
+        override fun getItemPosition(obj: Any): Int {
             return PagerAdapter.POSITION_NONE
         }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
index ade30923819f5a428acc3a22b80e1e9376e92f55..573413e9eb80eb743d273913ad8c1cee9361c056 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
@@ -9,4 +9,17 @@         Calendar.SUNDAY -> Timetable.MODE_SUNDAYS
         Calendar.SATURDAY -> Timetable.MODE_SATURDAYS
         else -> Timetable.MODE_WORKDAYS
     }
+}
+
+internal fun String.toPascalCase(): String { //check
+    val builder = StringBuilder(this)
+    var isLastCharSeparator = true
+    builder.forEach {
+        isLastCharSeparator = if ((it in 'a'..'z' || it in 'A'..'Z') && isLastCharSeparator){
+            it.toUpperCase()
+            false
+        } else
+            true
+    }
+    return builder.toString()
 }
\ No newline at end of file




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 464217e69dc44aafd37b0cec470b1090cc281651..66f433fa96b1afa00711e5024935f7535f1b8933 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
@@ -1,16 +1,17 @@
 package ml.adamsprogs.bimba.models
 
+import org.onebusaway.gtfs.model.AgencyAndId
 import java.util.*
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
 
-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,
+data class Departure(val line: AgencyAndId, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight
+                     val modification: List<String>, val direction: String, val vm: Boolean = false,
                      var tomorrow: Boolean = false, val onStop: Boolean = false) {
 
     val isModified: Boolean
         get() {
-            return modification != null && modification != "" && modification != "null"
+            return modification.isNotEmpty()
         }
 
     override fun toString(): String {




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 a2cc93ae6b51b9f6ac2e3e6cce371c2621f9fd3a..8aeaa6ccbd3fa18f982d2b58d851195af8d48ba7 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
@@ -18,8 +18,8 @@     override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
         val timetable = Timetable.getTimetable()
         val favourites = FavouriteStorage.getFavouriteStorage()
         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)
+                favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].stop, "",null)
+        val favouriteElement = "${timetable.getStopName(plate.stop)} ( ${timetable.getStopSymbol(plate.stop)}):\n${timetable.getLineNumber(plate.line)} → ${plate.headsign}"
         holder?.rowTextView?.text = favouriteElement
         holder?.splitButton?.setOnClickListener {
             favourites.detach(favourite.name, plate, favouriteElement)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
index b50d8b0bbd84bcf7f2a37c11a8978072f9f58a27..89476d0e4c136b34ca6a61b0a427cbe227d2aded 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
@@ -1,8 +1,10 @@
 package ml.adamsprogs.bimba.models
 
-data class Plate(val line: String, val stop: String, val departures: HashMap<String, HashSet<Departure>>?) {
+import org.onebusaway.gtfs.model.AgencyAndId
+
+data class Plate(val line: AgencyAndId, val stop: AgencyAndId, val headsign: String, val departures: HashMap<AgencyAndId, HashSet<Departure>>?) {
     override fun toString(): String {
-        var result = "$line=$stop={"
+        var result = "$line=$stop=$headsign={"
         if (departures != null) {
             for ((_, column) in departures)
                 for (departure in column) {
@@ -15,9 +17,9 @@     }
 
     companion object {
         fun fromString(string: String): Plate {
-            val s = string.split("=")
+            val (line, stop, headsign, departuresString) = string.split("=")
             val departures = HashMap<String, HashSet<Departure>>()
-            s[2].replace("{", "").replace("}", "").split(";")
+            departuresString.replace("{", "").replace("}", "").split(";")
                     .filter { it != "" }
                     .forEach {
                         try {
@@ -28,10 +30,10 @@                             departures[dep.mode]!!.add(dep)
                         } catch (e: IllegalArgumentException) {
                         }
                     }
-            return Plate(s[0], s[1], departures)
+            return Plate(line, stop, headsign, departures)
         }
 
-        fun join(set: HashSet<Plate>): HashMap<String, ArrayList<Departure>> {
+        fun join(set: Set<Plate>): HashMap<String, ArrayList<Departure>> {
             val departures = HashMap<String, ArrayList<Departure>>()
             for (plate in set) {
                 for ((mode, d) in plate.departures!!) {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt
index a9071c7c56ff94066c808e5421276882231dde0f..b31866db0372b70a2cc463c23da7e0bddfc8717b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt
@@ -3,24 +3,24 @@
 import android.os.Parcel
 import android.os.Parcelable
 import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
+import org.onebusaway.gtfs.model.AgencyAndId
 
-class StopSuggestion(text: String, val id: String, val symbol: String) : SearchSuggestion {
-    private val body: String = text
+class StopSuggestion(private val directions: HashSet<String>, val id: AgencyAndId) : SearchSuggestion {
 
-    constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString(), parcel.readString())
+    @Suppress("UNCHECKED_CAST")
+    constructor(parcel: Parcel) : this(parcel.readSerializable() as HashSet<String>, parcel.readSerializable() as AgencyAndId)
 
     override fun describeContents(): Int {
         return Parcelable.CONTENTS_FILE_DESCRIPTOR
     }
 
     override fun writeToParcel(dest: Parcel?, flags: Int) {
-        dest?.writeString(body)
-        dest?.writeString(id)
-        dest?.writeString(symbol)
+        dest?.writeSerializable(directions)
+        dest?.writeSerializable(id)
     }
 
     override fun getBody(): String {
-        return body
+        return "${Timetable.getTimetable().getStopName(id)}\n${directions.sortedBy{it}.joinToString()}"
     }
 
     companion object CREATOR : Parcelable.Creator<StopSuggestion> {




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 7bec2461efcac9263d22dc3bd1dc5464c45ceac4..b2648495b1e06aae110ce0b24914aa3519437058 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -1,64 +1,51 @@
 package ml.adamsprogs.bimba.models
 
 import android.content.Context
-import android.database.CursorIndexOutOfBoundsException
-import android.database.sqlite.SQLiteCantOpenDatabaseException
-import android.database.sqlite.SQLiteDatabase
-import android.database.sqlite.SQLiteDatabaseCorruptException
 import ml.adamsprogs.bimba.CacheManager
+import ml.adamsprogs.bimba.toPascalCase
 import java.io.File
-
+import org.onebusaway.gtfs.impl.GtfsDaoImpl
+import org.onebusaway.gtfs.model.*
+import org.onebusaway.gtfs.serialization.GtfsReader
+import org.onebusaway.gtfs.services.GtfsDao
+import java.util.*
 
 class Timetable private constructor() {
     companion object {
-        val version = 1
-        val MODE_WORKDAYS = "workdays"
-        val MODE_SATURDAYS = "saturdays"
-        val MODE_SUNDAYS = "sundays"
+        const val MODE_WORKDAYS = "workdays"
+        const val MODE_SATURDAYS = "saturdays"
+        const val MODE_SUNDAYS = "sundays"
         private var timetable: Timetable? = null
 
         fun getTimetable(context: Context? = null, force: Boolean = false): Timetable {
-            if (timetable == null || force)
+            return if (timetable == null || force)
                 if (context != null) {
-                    val db: SQLiteDatabase?
-                    try {
-                        db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
-                                null, SQLiteDatabase.OPEN_READONLY)
-                    } catch (e: NoSuchFileException) {
-                        throw SQLiteCantOpenDatabaseException("no such file")
-                    } catch (e: SQLiteCantOpenDatabaseException) {
-                        throw SQLiteCantOpenDatabaseException("cannot open db")
-                    } catch (e: SQLiteDatabaseCorruptException) {
-                        throw SQLiteCantOpenDatabaseException("db corrupt")
-                    }
                     timetable = Timetable()
-                    timetable!!.db = db
+                    timetable!!.store = read(context)
                     timetable!!.cacheManager = CacheManager.getCacheManager(context)
-                    return timetable as Timetable
+                    timetable as Timetable
                 } else
                     throw IllegalArgumentException("new timetable requested and no context given")
             else
-                return timetable as Timetable
+                timetable as Timetable
+        }
+
+        private fun read(context: Context): GtfsDao {
+            val reader = GtfsReader()
+            reader.setInputLocation(File(context.filesDir, "timetable.zip"))
+            val store = GtfsDaoImpl()
+            reader.entityStore = store
+            reader.run()
+            return store
         }
     }
 
-    lateinit var db: SQLiteDatabase
+    lateinit var store: GtfsDao
     private lateinit var cacheManager: CacheManager
-    private var _stops: ArrayList<StopSuggestion>? = null
+    private var _stops: ArrayList<StopSuggestion>? = null //todo stops to cache
 
     fun refresh(context: Context) {
-        val db: SQLiteDatabase?
-        try {
-            db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
-                    null, SQLiteDatabase.OPEN_READONLY)
-        } catch (e: NoSuchFileException) {
-            throw SQLiteCantOpenDatabaseException("no such file")
-        } catch (e: SQLiteCantOpenDatabaseException) {
-            throw SQLiteCantOpenDatabaseException("cannot open db")
-        } catch (e: SQLiteDatabaseCorruptException) {
-            throw SQLiteCantOpenDatabaseException("db corrupt")
-        }
-        this.db = db
+        this.store = read(context)
 
         cacheManager.recreate(getStopDeparturesByPlates(cacheManager.keys().toSet()))
 
@@ -69,50 +56,61 @@     fun getStops(force: Boolean = false): List {
         if (_stops != null && !force)
             return _stops!!
 
-        val stops = ArrayList<StopSuggestion>()
-        val cursor = db.rawQuery("select name ||char(10)|| headsigns as suggestion, id, stops.symbol || number as stopSymbol from stops" +
-                " join nodes on(stops.symbol = nodes.symbol) order by name, id;", null)
-        while (cursor.moveToNext())
-            stops.add(StopSuggestion(cursor.getString(0), cursor.getString(1), cursor.getString(2)))
-        cursor?.close()
-        _stops = stops
-        return stops
-    }
+        /*
+        AWF
+        232 → Os. Rusa|8:1435|AWF03
+        AWF
+        232 → Rondo Kaponiera|8:1436|AWF04
+        AWF
+        76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński|8:1437|AWF02
+        AWF
+        76 → Os. Dębina, 603 → Łęczyca/Dworcowa|8:1634|AWF01
+        AWF
+        29 → Pl. Wiosny Ludów|8:171|AWF42
+        AWF
+        10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody|8:172|AWF41
+        AWF
+        10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego|8:4586|AWF73
+        */
 
-    fun getStopName(stopId: String): String {
-        val cursor = db.rawQuery("select name from nodes join stops on(stops.symbol = nodes.symbol) where id = ?;",
-                listOf(stopId).toTypedArray())
-        val name: String
-        cursor.moveToNext()
-        name = cursor.getString(0)
-        cursor.close()
-        return name
+        //trip_id, stop_id from stop_times if drop_off_type in {0,3}
+        //route_id as line, trip_id, headsign from trips
+        //stop_id, stop_code from stops
+
+        val map = HashMap<AgencyAndId, HashSet<String>>()
+
+        store.allStopTimes.filter { it.dropOffType == 0 || it.dropOffType == 3 }.forEach {
+            val trip = it.trip
+            val line = trip.route.shortName
+            val headsign = (trip.tripHeadsign).toPascalCase()
+            val stopId = it.stop.id
+            if (map[stopId] == null)
+                map[stopId] = HashSet()
+            map[stopId]!!.add("$line → $headsign")
+        }
+
+        val stops = map.entries.map { StopSuggestion(it.value, it.key) }.toSet()
+
+
+        _stops = stops.sortedBy { this.getStopSymbol(it.id) } as ArrayList<StopSuggestion>
+        return _stops!!
     }
 
-    fun getStopSymbol(stopId: String): String {
-        val cursor = db.rawQuery("select symbol||number from stops where id = ?", listOf(stopId).toTypedArray())
-        val symbol: String
-        cursor.moveToNext()
-        symbol = cursor.getString(0)
-        cursor.close()
-        return symbol
-    }
+    fun getStopName(stopId: AgencyAndId) = store.getStopForId(stopId).name!!
+
+    fun getStopSymbol(stopId: AgencyAndId) = store.getStopForId(stopId).code!!
 
-    fun getLineNumber(lineId: String): String {
-        val cursor = db.rawQuery("select number from lines where id = ?", listOf(lineId).toTypedArray())
-        val number: String
-        cursor.moveToNext()
-        number = cursor.getString(0)
-        cursor.close()
-        return number
-    }
+    fun getLineNumber(lineId: AgencyAndId) = store.getRouteForId(lineId).shortName!!
 
-    fun getStopDepartures(stopId: String): Map<String, List<Departure>> {
+    fun getStopDepartures(stopId: AgencyAndId): Map<String, List<Departure>> {
         val plates = HashSet<Plate>()
         val toGet = HashSet<Plate>()
 
-        getLinesForStop(stopId)
-                .map { Plate(it, stopId, null) }
+        getTripsForStop(stopId)
+                .map {
+                    it.tripHeadsign
+                    Plate(it.route.id, stopId, it.tripHeadsign, null)
+                }
                 .forEach {
                     if (cacheManager.has(it))
                         plates.add(cacheManager.get(it)!!)
@@ -126,22 +124,6 @@
         return Plate.join(plates)
     }
 
-    fun getStopDepartures(stopId: String, lineId: String): Map<String, List<Departure>> {
-        val plates = HashSet<Plate>()
-        val toGet = HashSet<Plate>()
-
-        val plate = Plate(lineId, stopId, null)
-        if (cacheManager.has(plate))
-            plates.add(cacheManager.get(plate)!!)
-        else {
-            toGet.add(plate)
-        }
-
-        getStopDeparturesByPlates(toGet).forEach { cacheManager.push(it); plates.add(it) }
-
-        return Plate.join(plates)
-    }
-
     fun getStopDepartures(plates: Set<Plate>): Map<String, ArrayList<Departure>> {
         val result = HashSet<Plate>()
         val toGet = HashSet<Plate>()
@@ -161,93 +143,87 @@
     private fun getStopDeparturesByPlates(plates: Set<Plate>): Set<Plate> {
         if (plates.isEmpty())
             return emptySet()
-        val result = HashMap<String, Plate>()
 
-        val condition = plates.joinToString(" or ") { "(stop_id = '${it.stop}' and line_id = '${it.line}')" }
+        return plates.map { getStopDeparturesByPlate(it) }.toSet()
+    }
 
-        val sql = "select " +
-                "lines.number, mode, substr('0'||hour, -2) || ':' || " +
-                "substr('0'||minute, -2) as time, lowFloor, modification, headsign, stop_id, line_id " +
-                "from " +
-                "departures join timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) " +
-                "where " +
-                condition +
-                "order by " +
-                "mode, time;"
-        val cursor = db.rawQuery(sql, null)
+    private fun getStopDeparturesByPlate(plate: Plate): Plate {
+        val p = Plate(plate.line, plate.stop, plate.headsign, HashMap())
+        store.allStopTimes
+                .filter { it.stop.id == plate.stop }
+                .filter { it.trip.route.id == plate.line }
+                .filter { it.trip.tripHeadsign.toLowerCase() == plate.headsign.toLowerCase() }
+                .forEach {
+                    val time = it.departureTime
+                    val serviceId = it.trip.serviceId
+                    val mode = calendarToMode(serviceId.id.toInt())
+                    val lowFloor = it.trip.wheelchairAccessible == 1
+                    val mod = explainModification(it.trip, it.trip.id.id.split("^")[1], it.stopSequence)
+
+                    val dep = Departure(plate.line, mode, time, lowFloor, mod, plate.headsign)
+                    if (p.departures!![serviceId] == null)
+                        p.departures[serviceId] = HashSet()
+                    p.departures[serviceId]!!.add(dep)
+                }
+        return p
+    }
 
-        while (cursor.moveToNext()) {
-            val lineId = cursor.getString(7)
-            val stopId = cursor.getString(6)
+    private fun calendarToMode(serviceId: Int): List<Int> {
+        val calendar = store.getCalendarForId(serviceId)
+        val days = ArrayList<Int>()
+        if (calendar.monday == 1) days.add(0)
+        if (calendar.tuesday == 1) days.add(1)
+        if (calendar.wednesday == 1) days.add(2)
+        if (calendar.thursday == 1) days.add(3)
+        if (calendar.friday == 1) days.add(4)
+        if (calendar.saturday == 1) days.add(5)
+        if (calendar.sunday == 1) days.add(6)
+        return days
+    }
 
-            if (!result.containsKey("$lineId@$stopId")) {
-                result["$lineId@$stopId"] = Plate(lineId, stopId, HashMap())
-                result["$lineId@$stopId"]?.departures?.put(MODE_WORKDAYS, HashSet())
-                result["$lineId@$stopId"]?.departures?.put(MODE_SATURDAYS, HashSet())
-                result["$lineId@$stopId"]?.departures?.put(MODE_SUNDAYS, HashSet())
-            }
+    private fun explainModification(trip: Trip, modifications: String, stopSequence: Int): List<String> {
+        val mods = modifications.replace("+", "").split(",")
+        var definitions = trip.route.desc.split("^")
+        definitions = definitions.slice(2..definitions.size)
 
-            result["$lineId@$stopId"]?.departures?.get(cursor.getString(1))?.add(
-                    Departure(cursor.getString(0), cursor.getString(1),
-                            cursor.getString(2), cursor.getInt(3) == 1,
-                            cursor.getString(4), cursor.getString(5)))
+        val definitionsMap = HashMap<String, String>()
+
+        definitions.forEach {
+            val (key, definition) = it.split(" - ")
+            definitionsMap[key] = definition
         }
-        cursor.close()
-        return result.values.toSet()
+
+        val explanations = ArrayList<String>()
+
+        mods.forEach {
+            if (it.contains(":")) {
+                val (key, start, stop) = it.split(":")
+                if (stopSequence in start.toInt()..stop.toInt())
+                    explanations.add(definitionsMap[key]!!)
+            } else {
+                explanations.add(definitionsMap[it]!!)
+            }
+        }
+
+        return explanations
     }
 
-    private fun getLinesForStop(stopId: String): Set<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()
+    fun getLinesForStop(stopId: AgencyAndId): Set<AgencyAndId> {
+        val lines = HashSet<AgencyAndId>()
+        store.allStopTimes.filter { it.stop.id == stopId }.forEach { lines.add(it.trip.route.id) }
         return lines
     }
 
-    fun getLines(stopId: String): List<String> {
-        val cursor = db.rawQuery(" select distinct line_id from timetables join " +
-                "stops on(stop_id = stops.id) where stops.id = ?;",
-                listOf(stopId).toTypedArray())
-        val lines = ArrayList<String>()
-        while (cursor.moveToNext()) {
-            lines.add(cursor.getString(0))
-        }
-        cursor.close()
+    private fun getTripsForStop(stopId: AgencyAndId): Set<Trip> {
+        val lines = HashSet<Trip>()
+        store.allStopTimes.filter { it.stop.id == stopId }.forEach { lines.add(it.trip) }
         return lines
     }
 
-    fun getFavouriteElement(plate: Plate): String {
-        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(plate.stop, plate.line).toTypedArray())
-        val element: String
-        cursor.moveToNext()
-        element = cursor.getString(0)
-        cursor.close()
-        return element
-    }
+    fun isEmpty() = store.allFeedInfos.isEmpty()
 
-    fun isEmpty(): Boolean {
-        val cursor = db.rawQuery("select * from metadata;", null)
-        try {
-            cursor.moveToNext()
-            cursor.getString(0)
-            cursor.close()
-        } catch (e: CursorIndexOutOfBoundsException) {
-            return true
-        }
-        return false
-    }
+    fun getValidSince() = store.allFeedInfos.toTypedArray()[0].startDate.asString!!
 
-    fun getValidity(): String {
-        val cursor = db.rawQuery("select value from metadata where key = 'validFrom'", null)
-        cursor.moveToNext()
-        val validity = cursor.getString(0)
-        cursor.close()
-        return "%s-%s-%s".format(validity.substring(0..3), validity.substring(4..5), validity.substring(6..7))
-    }
+    fun getValidTill() = store.allFeedInfos.toTypedArray()[0].endDate.asString!!
 }
 




diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png
index 18369c5dfd306a3661c2d4ff99a632158e2895d7..11ef909fbfa1a99c4d00ec374e1a0a1c0365c4af 100644
Binary files a/app/src/main/res/drawable/logo.png and b/app/src/main/res/drawable/logo.png differ




diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml
index 34ef96f91038985d8a1983e9d15b9f6b83bb497d..a1d293088df728c10d1eb40562e52de72ad04000 100644
--- a/app/src/main/res/menu/menu_drawer.xml
+++ b/app/src/main/res/menu/menu_drawer.xml
@@ -20,7 +20,10 @@              android:id="@+id/drawer_group_validity"
         android:enabled="false">
         <item
-            android:id="@+id/drawer_validity"
+            android:id="@+id/drawer_validity_since"
+            android:title="" />
+        <item
+            android:id="@+id/drawer_validity_till"
             android:title="" />
     </group>
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 177034ccde1941a01f5c80bd83c7619cc2dae5a9..cedb2477f8b47ae364493fe4b01f0d797d1bfa9f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
 <resources>
-    <string name="app_name" translatable="false">Bimba</string>
+    <string name="app_name" translatable="false">Bimba-dev</string>
     <string name="no_timetable">No timetable found. Connect to the Internet and wait for a timetable to be downloaded</string>
     <string name="timetable_downloaded">New timetable downloaded</string>
     <string name="title_activity_stop" translatable="false">StopActivity</string>
@@ -67,7 +67,18 @@         "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_since">Valid since %1$s</string>
+    <string name="valid_till">Valid till %1$s</string>
     <string name="departure_floor" translatable="false">departure floor type (lowFloor)</string>
     <string name="departure_info" translatable="false">departure info icon</string>
     <string name="refreshing_cache">Refreshing cache. May take some time…</string>
+
+    <string-array name="daysOfWeek">
+        <item>Monday</item>
+        <item>Tuesday</item>
+        <item>Wednesday</item>
+        <item>Thursday</item>
+        <item>Friday</item>
+        <item>Saturday</item>
+        <item>Sunday</item>
+    </string-array>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index cabe97a591e5a5b49262af0aab80be4c0fc16d34..71678974fad1cc955d9f7fe97b6050b8bac97e69 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -37,6 +37,7 @@     Erfrischen
     <string name="help">Hilfe</string>
     <string name="title_activity_help">Hilfe</string>
     <string name="valid_since">Gilt seit %1$s</string>
+    <string name="valid_till">Gilt bis %1$s</string>
     <string name="departure_row_getting_departures">Abfahrten sammeln…</string>
     <string name="help_text">
         "Warum gibt es keinen Fahrplan für Samstag?\n\n"




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index c6ea5e9a9bb1921cd51d3773a5824738186fbce0..fbed0de352a859707c197c72559c73bd4e8e0b8c 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -9,6 +9,7 @@     Schermo principale
     <string name="help">Aiuto</string>
     <string name="title_activity_help">Aiuto</string>
     <string name="valid_since">Valido da %1$s</string>
+    <string name="valid_till">Valido a %1$s</string>
     <string name="no_timetable">Nessun orario è stato trovato. Connettiti a Internet e aspetta che un orario venga scaricato</string>
     <string name="timetable_downloaded">Nuovo orario è stato scaricato</string>
     <string name="tab_workday_text">Giorni di lavoro</string>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 327158c95ac287d69894bd806ee48caed60981a5..d846964b159c1c23bbe4786abd035c1c83e49642 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -57,5 +57,6 @@         "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_since">Ważny od %1$s</string>
+    <string name="valid_till">Ważny do %1$s</string>
     <string name="refreshing_cache">Odświeżanie pamięci podręcznej. Może chwilę potrwać…</string>
 </resources>
\ No newline at end of file