Bimba.git

commit 3ddda8aee6d63ffd1adecd2a8551c7f07027ab93

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

quicker transition to StopActivity (gettting departures in bg)

%!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/Declinator.kt b/app/src/main/java/ml/adamsprogs/bimba/Declinator.kt
index ea7988accf3986731c2ae4d50e2b22832b8841e5..27b3d806e0d1aa507e44faf18ebb8083263f600f 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/Declinator.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/Declinator.kt
@@ -3,14 +3,14 @@
 class Declinator {
     companion object {
         fun decline(number: Long): Int {
-            when {
-                number == 0L -> return R.string.now
-                number % 10 == 0L -> return R.string.departure_in__plural_genitive
-                number == 1L -> return R.string.departure_in__singular_genitive
-                number in listOf<Long>(12,13,14) -> return R.string.departure_in__plural_genitive
-                number % 10 in listOf<Long>(2, 3, 4) -> return R.string.departure_in__plural_nominative
-                number % 10 in listOf<Long>(1,5,6,7,8,9) -> return R.string.departure_in__plural_genitive
-                else -> return -1
+            return when {
+                number == 0L -> R.string.now
+                number % 10 == 0L -> R.string.departure_in__plural_genitive
+                number == 1L -> R.string.departure_in__singular_genitive
+                number in listOf<Long>(12,13,14) -> R.string.departure_in__plural_genitive
+                number % 10 in listOf<Long>(2, 3, 4) -> R.string.departure_in__plural_nominative
+                number % 10 in listOf<Long>(1,5,6,7,8,9) -> R.string.departure_in__plural_genitive
+                else -> -1
             }
         }
     }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
index ac29fc4fceb1e21e7bb2767cf6d0572bf16d8264..28a62879797264439b8d0a2e62cdf70f76d05ea1 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
@@ -7,8 +7,8 @@ import android.util.Log
 import ml.adamsprogs.bimba.models.Departure
 
 class MessageReceiver : BroadcastReceiver() {
-    val onTimetableDownloadListeners: HashSet<OnTimetableDownloadListener> = HashSet()
-    val onVmListeners: HashSet<OnVmListener> = HashSet()
+    private val onTimetableDownloadListeners: HashSet<OnTimetableDownloadListener> = HashSet()
+    private val onVmListeners: HashSet<OnVmListener> = HashSet()
 
     override fun onReceive(context: Context?, intent: Intent?) {
         Log.i("Recv", "${intent?.action}")




diff --git a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
index 71ef786ec8e6cfec1495edf62cdbca6f904f51dd..70a3a1652bc188fb666fd3841247a52422388b1c 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
@@ -7,7 +7,7 @@ import android.content.Context
 
 class NetworkStateReceiver : BroadcastReceiver() {
 
-    val onConnectivityChangeListeners = HashSet<OnConnectivityChangeListener>()
+    private val onConnectivityChangeListeners = HashSet<OnConnectivityChangeListener>()
 
     override fun onReceive(context: Context, intent: Intent) {
         if (intent.extras != null) {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
index ac1a62fa93168ced7d984c0ba40a2c68bea52fc4..9912fb6a5c6b5f7ebe188773dba983abccc33370 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
@@ -26,8 +26,8 @@         val RESULT_UP_TO_DATE = "up-to-date"
         val RESULT_DOWNLOADED = "downloaded"
         val RESULT_VALIDITY_FAILED = "validity failed"
     }
-    lateinit var notificationManager: NotificationManager
-    var size: Int = 0
+    private lateinit var notificationManager: NotificationManager
+    private var size: Int = 0
 
     override fun onHandleIntent(intent: Intent?) {
 
@@ -138,7 +138,7 @@             e.printStackTrace()
         } finally {
             ins.close()
             val digest = md.digest()
-            for (i in 0..digest.size - 1) {
+            for (i in 0 until digest.size) {
                 hex += Integer.toString((digest[i] and 0xff.toByte()) + 0x100, 16).padStart(3, '0').substring(1)
             }
             Log.i("Downloader", "checksum is $checksum, and hex is $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 4f2f7d19ea02042e944d687c3025a1f936a988ad..5322e013f4698048f772d16695b019ff7800c9ee 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -26,12 +26,12 @@     val context: Context = this
     val receiver = MessageReceiver()
     lateinit var timetable: Timetable
     var stops: ArrayList<StopSuggestion>? = null
-    lateinit var drawerLayout: DrawerLayout
-    lateinit var drawer: NavigationView
+    private lateinit var drawerLayout: DrawerLayout
+    private lateinit var drawer: NavigationView
     lateinit var favouritesList: RecyclerView
     lateinit var searchView: FloatingSearchView
     lateinit var favourites: FavouriteStorage
-    var timer = Timer()
+    private var timer = Timer()
     private lateinit var timerTask: TimerTask
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -243,16 +243,17 @@     }
 
     override fun onTimetableDownload(result: String?) {
         Log.i("Refresh", "downloaded: $result")
-        val message: String
-        when (result) {
-            TimetableDownloader.RESULT_DOWNLOADED -> message = getString(R.string.timetable_downloaded)
-            TimetableDownloader.RESULT_NO_CONNECTIVITY -> message = getString(R.string.no_connectivity)
-            TimetableDownloader.RESULT_UP_TO_DATE -> message = getString(R.string.timetable_up_to_date)
-            TimetableDownloader.RESULT_VALIDITY_FAILED -> message = getString(R.string.validity_failed)
-            else -> message = getString(R.string.error_try_later)
+        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)
+        }
+        if (result == TimetableDownloader.RESULT_DOWNLOADED) {
+            timetable.refresh(context)
+            stops = timetable.getStops()
         }
-        timetable.refresh()
-        stops = timetable.getStops()
         Snackbar.make(findViewById(R.id.drawer_layout), message, Snackbar.LENGTH_LONG).show()
     }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
index e8d0b94f23b4029ad6379ebd7e0b6f016aec1293..3034ce46980b184e3776a19abea7046668bbf725 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
@@ -17,15 +17,15 @@     companion object {
         val EXTRA_FAVOURITE = "favourite"
     }
 
-    lateinit var favourites: FavouriteStorage
-    lateinit var nameEdit: EditText
-    var favourite: Favourite? = null
+    private lateinit var favourites: FavouriteStorage
+    private lateinit var nameEdit: EditText
+    private var favourite: Favourite? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_edit_favourite)
 
-        favourite = intent.getParcelableExtra<Favourite>(EXTRA_FAVOURITE)
+        favourite = intent.getParcelableExtra(EXTRA_FAVOURITE)
         if (favourite == null)
             finish()
         favourites = FavouriteStorage.getFavouriteStorage(this)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
index b35bf71c9670673bb7ec6a368874560476b3fac0..acd4d6fbe1ef29d2ed450a0aedad5a8be982517b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
@@ -9,15 +9,15 @@ import ml.adamsprogs.bimba.*
 
 
 class NoDbActivity : AppCompatActivity(), NetworkStateReceiver.OnConnectivityChangeListener, MessageReceiver.OnTimetableDownloadListener {
-    val networkStateReceiver = NetworkStateReceiver()
-    val timetableDownloadReceiver = MessageReceiver()
-    var serviceRunning = false
-    var askedForNetwork = false
+    private val networkStateReceiver = NetworkStateReceiver()
+    private val timetableDownloadReceiver = MessageReceiver()
+    private var serviceRunning = false
+    private var askedForNetwork = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_nodb)
-        var filter: IntentFilter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
+        var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
         filter.addCategory(Intent.CATEGORY_DEFAULT)
         registerReceiver(timetableDownloadReceiver, filter)
         timetableDownloadReceiver.addOnTimetableDownloadListener(this)
@@ -34,7 +34,7 @@     }
 
     override fun onResume() {
         super.onResume()
-        var filter: IntentFilter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
+        var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
         filter.addCategory(Intent.CATEGORY_DEFAULT)
         registerReceiver(timetableDownloadReceiver, filter)
         if (!NetworkStateReceiver.isNetworkAvailable(this)) {
@@ -47,7 +47,7 @@         } else if (!serviceRunning)
             downloadTimetable()
     }
 
-    fun downloadTimetable() {
+    private fun downloadTimetable() {
         (findViewById(R.id.noDbCaption) as TextView).text = getString(R.string.no_db_downloading)
         serviceRunning = true
         intent = Intent(this, TimetableDownloader::class.java)




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 aaa8ccb668a1bb08f3507cdc3ccb5537a1796c2e..7e7b3aa73343cc6766039f3e6aea66f64985d915 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
@@ -12,8 +12,11 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         try {
-            Timetable.getTimetable(this)
-            startActivity(Intent(this, DashActivity::class.java))
+            val timetable = Timetable.getTimetable(this)
+            if (timetable.isEmpty())
+                startActivity(Intent(this, NoDbActivity::class.java))
+            else
+                startActivity(Intent(this, DashActivity::class.java))
         } catch(e: SQLiteCantOpenDatabaseException) {
             startActivity(Intent(this, NoDbActivity::class.java))
         }




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 3d8a2d320d9d10d1c84afa08d9e54022be81f941..5ffdd690c00edecc66cb3eb9da03cfc6f223c848 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -15,6 +15,7 @@ import android.support.v4.content.res.ResourcesCompat
 
 import ml.adamsprogs.bimba.models.*
 import ml.adamsprogs.bimba.*
+import kotlin.concurrent.thread
 
 
 class StopActivity : AppCompatActivity(), MessageReceiver.OnVmListener {
@@ -57,7 +58,11 @@
         viewPager = findViewById(R.id.container) as ViewPager
         tabLayout = findViewById(R.id.tabs) as TabLayout
 
-        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, Departure.createDepartures(stopId))
+        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, null)
+        thread {
+            sectionsPagerAdapter!!.departures = Departure.createDepartures(stopId)
+            runOnUiThread { sectionsPagerAdapter?.notifyDataSetChanged() }
+        }
 
         viewPager!!.adapter = sectionsPagerAdapter
         viewPager!!.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
@@ -111,7 +116,7 @@         registerReceiver(receiver, filter)
         receiver.addOnVmListener(context as MessageReceiver.OnVmListener)
     }
 
-    override fun onVm(vmDepartures: ArrayList<Departure>?, requester:String) {
+    override fun onVm(vmDepartures: ArrayList<Departure>?, requester: String) {
         if (timetableType == "departure" && requester == REQUESTER_ID) {
             val fullDepartures = Departure.createDepartures(stopId)
             if (vmDepartures != null) {
@@ -182,11 +187,11 @@             val rootView = inflater!!.inflate(R.layout.fragment_stop, container, false)
 
             val layoutManager = LinearLayoutManager(activity)
             val departuresList: RecyclerView = rootView.findViewById(R.id.departuresList) as RecyclerView
-            val dividerItemDecoration = DividerItemDecoration(departuresList.context, layoutManager.orientation)
-            departuresList.addItemDecoration(dividerItemDecoration)
-            val adapter = DeparturesAdapter(activity, arguments.getStringArrayList("departures").map { Departure.fromString(it) },
+            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)
-            departuresList.adapter = adapter
             departuresList.layoutManager = layoutManager
             return rootView
         }
@@ -200,9 +205,12 @@                 val fragment = PlaceholderFragment()
                 val args = Bundle()
                 args.putInt(ARG_SECTION_NUMBER, sectionNumber)
                 args.putString("stop", stopId)
-                val d = ArrayList<String>()
-                departures?.mapTo(d) { it.toString() }
-                args.putStringArrayList("departures", d)
+                if (departures != null) {
+                    val d = ArrayList<String>()
+                    departures.mapTo(d) { it.toString() }
+                    args.putStringArrayList("departures", d)
+                } else
+                    args.putStringArrayList("departures", null)
                 args.putBoolean("relativeTime", relativeTime)
                 fragment.arguments = args
                 return fragment
@@ -210,7 +218,7 @@             }
         }
     }
 
-    inner class SectionsPagerAdapter(fm: FragmentManager, var departures: HashMap<String, ArrayList<Departure>>) : FragmentStatePagerAdapter(fm) {
+    inner class SectionsPagerAdapter(fm: FragmentManager, var departures: HashMap<String, ArrayList<Departure>>?) : FragmentStatePagerAdapter(fm) {
 
         var relativeTime = true
 
@@ -225,7 +233,7 @@                 0 -> mode = Timetable.MODE_WORKDAYS
                 1 -> mode = Timetable.MODE_SATURDAYS
                 2 -> mode = Timetable.MODE_SUNDAYS
             }
-            return PlaceholderFragment.newInstance(position + 1, stopId, departures[mode], relativeTime)
+            return PlaceholderFragment.newInstance(position + 1, stopId, departures?.get(mode), relativeTime)
         }
 
         override fun getCount() = 3




diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
index 59572d65b5f42dcd6f63cfaa7bda08380ff22c3a..ade30923819f5a428acc3a22b80e1e9376e92f55 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
@@ -4,9 +4,9 @@ import ml.adamsprogs.bimba.models.Timetable
 import java.util.*
 
 internal fun Calendar.getMode(): String {
-    when (this.get(Calendar.DAY_OF_WEEK)) {
-        Calendar.SUNDAY -> return Timetable.MODE_SUNDAYS
-        Calendar.SATURDAY -> return Timetable.MODE_SATURDAYS
-        else -> return Timetable.MODE_WORKDAYS
+    return when (this.get(Calendar.DAY_OF_WEEK)) {
+        Calendar.SUNDAY -> Timetable.MODE_SUNDAYS
+        Calendar.SATURDAY -> Timetable.MODE_SATURDAYS
+        else -> Timetable.MODE_WORKDAYS
     }
 }
\ 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 a5d9aec9bc7885788f50b9708ed9522134122f12..21df84c11607746d60a7705b6e7b5117c74c5bf5 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
@@ -1,9 +1,10 @@
 package ml.adamsprogs.bimba.models
 
 import java.util.*
+import kotlin.collections.ArrayList
 
-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: String, private val mode: String, val time: String, private val lowFloor: Boolean,
+                     private val modification: String?, val direction: String, val vm: Boolean = false,
                      var tomorrow: Boolean = false, val onStop: Boolean = false) {
 
     override fun toString(): String {
@@ -15,20 +16,13 @@         return Departure.fromString(this.toString())
     }
 
     companion object {
-        private fun filterDepartures(departures: List<Departure>?): ArrayList<Departure> {
+        private fun filterDepartures(departures: List<Departure>): ArrayList<Departure> {
             val filtered = ArrayList<Departure>()
             val lines = HashMap<String, Int>()
-            val now = Calendar.getInstance()
-            for (departure in departures!!) {
-                val time = Calendar.getInstance()
-                time.set(Calendar.HOUR_OF_DAY, Integer.parseInt(departure.time.split(":")[0]))
-                time.set(Calendar.MINUTE, Integer.parseInt(departure.time.split(":")[1]))
-                time.set(Calendar.SECOND, 0)
-                time.set(Calendar.MILLISECOND, 0)
-                if (departure.tomorrow)
-                    time.add(Calendar.DAY_OF_MONTH, 1)
+            val sortedDepartures = departures.sortedBy { it.timeTill() }
+            for (departure in sortedDepartures) {
                 var lineExistedTimes = lines[departure.line]
-                if ((now.before(time) || now == time) && lineExistedTimes ?: 0 < 3) {
+                if (departure.timeTill() >= 0 && lineExistedTimes ?: 0 < 3) {
                     lineExistedTimes = (lineExistedTimes ?: 0) + 1
                     lines[departure.line] = lineExistedTimes
                     filtered.add(departure)
@@ -40,7 +34,12 @@
         fun createDepartures(stopId: String): HashMap<String, ArrayList<Departure>> {
             val timetable = Timetable.getTimetable()
             val departures = timetable.getStopDepartures(stopId)
-            val moreDepartures = timetable.getStopDepartures(stopId)
+            val moreDepartures = HashMap<String, ArrayList<Departure>>()
+            for ((k,v) in departures) {
+                moreDepartures[k] = ArrayList()
+                for (departure in v)
+                    moreDepartures[k]!!.add(departure.copy())
+            }
             val rolledDepartures = HashMap<String, ArrayList<Departure>>()
 
             for ((_, tomorrowDepartures) in moreDepartures) {
@@ -50,7 +49,7 @@
             for ((mode, _) in departures) {
                 rolledDepartures[mode] = (departures[mode] as ArrayList<Departure> +
                         moreDepartures[mode] as ArrayList<Departure>) as ArrayList<Departure>
-                rolledDepartures[mode] = filterDepartures(rolledDepartures[mode])
+                rolledDepartures[mode] = filterDepartures(rolledDepartures[mode]!!)
             }
 
             return rolledDepartures




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
index c175a6e11d1209aa827274f6484acc3e8ce1c561..63af2772ec7bbdbc190eb3e214a83e173d73aab5 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
@@ -12,13 +12,32 @@ import android.view.LayoutInflater
 import ml.adamsprogs.bimba.Declinator
 import java.util.*
 
-class DeparturesAdapter(val context: Context, val departures: List<Departure>, val relativeTime: Boolean) :
+class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) :
         RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() {
+
+    companion object {
+        const val VIEW_TYPE_LOADING: Int = 0
+        const val VIEW_TYPE_CONTENT: Int = 1
+    }
+
     override fun getItemCount(): Int {
+
+        if (departures == null)
+            return 1
         return departures.size
     }
 
+    override fun getItemViewType(position: Int): Int {
+        return if (departures == null)
+            VIEW_TYPE_LOADING
+        else
+            VIEW_TYPE_CONTENT
+    }
+
     override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
+        if (departures == null) {
+            return
+        }
         val departure = departures[position]
         val now = Calendar.getInstance()
         val departureTime = Calendar.getInstance()
@@ -30,12 +49,12 @@
         val departureIn = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)
         val timeString: String
 
-        if (departureIn > 60 || departureIn < 0 || !relativeTime)
-            timeString = context.getString(R.string.departure_at, departure.time)
+        timeString = if (departureIn > 60 || departureIn < 0 || !relativeTime)
+            context.getString(R.string.departure_at, departure.time)
         else if (departureIn > 0 && !departure.onStop)
-            timeString = context.getString(Declinator.decline(departureIn), departureIn.toString())
+            context.getString(Declinator.decline(departureIn), departureIn.toString())
         else
-            timeString = context.getString(R.string.now)
+            context.getString(R.string.now)
 
         val line = holder?.lineTextView
         line?.text = departure.line
@@ -55,8 +74,7 @@         val context = parent?.context
         val inflater = LayoutInflater.from(context)
 
         val rowView = inflater.inflate(R.layout.row_departure, parent, false)
-        val viewHolder = ViewHolder(rowView)
-        return viewHolder
+        return ViewHolder(rowView)
     }
 
     inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {




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 828f537146df436aa4ec9c59e56ee1da81ad00e5..357e0ef9d4721ae8a4c5ab3016501a59fb0f8c91 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
@@ -12,11 +12,10 @@
 class Favourite : Parcelable, MessageReceiver.OnVmListener {
     override fun onVm(vmDepartures: ArrayList<Departure>?, requester: String) {
         val requesterName = requester.split(";")[0]
-        var requesterTimetable: String
-        try {
-            requesterTimetable = requester.split(";")[1]
+        val requesterTimetable: String = try {
+            requester.split(";")[1]
         } catch (e: IndexOutOfBoundsException) {
-            requesterTimetable = ""
+            ""
         }
         Log.i("VM", "got vm for $requesterName and my name is $name")
         if (vmDepartures != null && requesterName == name) {
@@ -93,7 +92,7 @@             tomorrowCal.add(Calendar.DAY_OF_MONTH, 1)
             val tomorrow = tomorrowCal.getMode()
 
             if (oneDayDepartures == null) {
-                oneDayDepartures = ArrayList<HashMap<String, ArrayList<Departure>>>()
+                oneDayDepartures = ArrayList()
                 timetables.mapTo(oneDayDepartures!!) { timetable.getStopDepartures(it[TAG_STOP] as String, it[TAG_LINE]) }
             }
 
@@ -119,7 +118,7 @@                     .minBy { it.timeTill() }
         }
         private set
 
-    fun filterVmDepartures() {
+    private fun filterVmDepartures() {
         this.vmDepartures
                 .filter { it.timeTill() < 0 }
                 .forEach { this.vmDepartures.remove(it) }




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 5672e0a5d124d93edd93d69fe76ac873a0c78c2f..96881bab8514c1b54cb2635ea4e9c7826e832158 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
@@ -8,7 +8,7 @@ import android.widget.ImageView
 import android.widget.TextView
 import ml.adamsprogs.bimba.R
 
-class FavouriteEditRowAdapter(var favourite: Favourite) :
+class FavouriteEditRowAdapter(private var favourite: Favourite) :
         RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
     override fun getItemCount(): Int {
         return favourite.size
@@ -39,8 +39,7 @@         val context = parent?.context
         val inflater = LayoutInflater.from(context)
 
         val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false)
-        val viewHolder = ViewHolder(rowView)
-        return viewHolder
+        return ViewHolder(rowView)
     }
 
     inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {




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 2949ffb25807d7320416e2bf7247b6b6a8d35127..e66d81c93a955e206d16cf36ac0a98f8a49d5794 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
@@ -12,20 +12,20 @@ class FavouriteStorage private constructor(context: Context) : Iterable {
     companion object {
         private var favouriteStorage: FavouriteStorage? = null
         fun getFavouriteStorage(context: Context? = null): FavouriteStorage {
-            if (favouriteStorage == null) {
+            return if (favouriteStorage == null) {
                 if (context == null)
                     throw IllegalArgumentException("requested new storage context not given")
                 else {
                     favouriteStorage = FavouriteStorage(context)
-                    return favouriteStorage as FavouriteStorage
+                    favouriteStorage as FavouriteStorage
                 }
             } else
-                return favouriteStorage as FavouriteStorage
+                favouriteStorage as FavouriteStorage
         }
     }
 
     val favourites = HashMap<String, Favourite>()
-    val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
+    private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
     val favouritesList: List<Favourite>
         get() {
             return favourites.values.toList()
@@ -74,7 +74,7 @@         favourites[name]?.delete(stop, line)
         serialize()
     }
 
-    fun serialize() {
+    private fun serialize() {
         val rootObject = JsonObject()
         for ((name, favourite) in favourites) {
             val timetables = JsonArray()
@@ -108,7 +108,7 @@
     fun merge(names: ArrayList<String>) {
         if (names.size < 2)
             return
-        val newFavourite = Favourite(names[0], ArrayList<HashMap<String, String>>())
+        val newFavourite = Favourite(names[0], ArrayList())
         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 be2da2c0ebedf4b2a452871c3a8e73ff3b937627..fdc2ca495df514048bcd8dfc08169088b7bee2bb 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
@@ -18,14 +18,14 @@ import ml.adamsprogs.bimba.Declinator
 import kotlin.collections.ArrayList
 
 //todo list to storage
-class FavouritesAdapter(val context: Context, var favourites: List<Favourite>, val onMenuItemClickListener: FavouritesAdapter.OnMenuItemClickListener) :
+class FavouritesAdapter(val context: Context, var favourites: List<Favourite>, private val onMenuItemClickListener: FavouritesAdapter.OnMenuItemClickListener) :
         RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
 
-    val isSelecting: Boolean
+    private val isSelecting: Boolean
         get() {
             return selected.any { it }
         }
-    val selected = ArrayList<Boolean>()
+    private val selected = ArrayList<Boolean>()
     val selectedNames: ArrayList<String>
         get() {
             val l = ArrayList<String>()
@@ -66,7 +66,7 @@                     return@thread
                 nextDepartureText = context.getString(Declinator.decline(interval), interval.toString())
                 nextDepartureLineText = context.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.direction)
             } else {
-                //fixme too early
+                //fixme too early ?
                 nextDepartureText = context.getString(R.string.no_next_departure)
                 nextDepartureLineText = ""
             }
@@ -102,7 +102,7 @@             }
         }
     }
 
-    fun toggleSelected(view: CardView, position: Int) {
+    private fun toggleSelected(view: CardView, position: Int) {
         growSelected(position)
 
         if (selected[position])
@@ -116,7 +116,7 @@         while (position >= selected.size)
             selected.add(false)
     }
 
-    fun select(view: CardView, position: Int) {
+    private fun select(view: CardView, position: Int) {
         growSelected(position)
 
         @Suppress("DEPRECATION")
@@ -128,7 +128,7 @@         selected[position] = true
         setSelecting()
     }
 
-    fun unSelect(view: CardView, position: Int) {
+    private fun unSelect(view: CardView, position: Int) {
         growSelected(position)
 
         val colour = TypedValue()
@@ -138,7 +138,7 @@         selected[position] = false
         setSelecting()
     }
 
-    fun setSelecting() {
+    private fun setSelecting() {
         context as Activity
         if (isSelecting) {
             context.findViewById(R.id.search_view).visibility = View.INVISIBLE
@@ -154,8 +154,7 @@         val context = parent?.context
         val inflater = LayoutInflater.from(context)
 
         val rowView = inflater.inflate(R.layout.row_favourite, parent, false)
-        val viewHolder = ViewHolder(rowView)
-        return viewHolder
+        return ViewHolder(rowView)
     }
 
     fun stopSelecting(name: String) {




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 78a96302b0a4b9131a086c8b47e2a1dffad29263..4cf47653862c3a2204262901f603e67cfcbf2003 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt
@@ -6,7 +6,7 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
 
 class StopSuggestion(text: String, val id: String, val symbol: String) : SearchSuggestion {
     private val body: String = text
-    val CONTENTS_SUGGESTION = 0x0105
+    private val CONTENTS_SUGGESTION = 0x0105
 
     constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString(), parcel.readString())
 




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 d77862ff8b8c28ad2712b5a33d985c280aa46963..9f38d413c8d0eada814603ae307b0fb6e4c3e5da 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -1,10 +1,12 @@
 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 java.io.File
+
 
 class Timetable private constructor() {
     companion object {
@@ -40,16 +42,26 @@     }
 
     lateinit var db: SQLiteDatabase
     private var _stops: ArrayList<StopSuggestion>? = null
+    private val _stopDepartures = HashMap<String, HashMap<String, ArrayList<Departure>>>()
+    private val _stopDeparturesCount = HashMap<String, Int>()
 
-    init {
-        readDbFile()
-    }
+    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
 
-    fun refresh() {
-        readDbFile()
-    }
-
-    private fun readDbFile() {
+        for ((k, _) in _stopDepartures)
+            _stopDepartures.remove(k)
+        //todo recreate cache
     }
 
     fun getStops(): ArrayList<StopSuggestion> {
@@ -95,11 +107,16 @@         return number
     }
 
     fun getStopDepartures(stopId: String, lineId: String? = null, tomorrow: Boolean = false): HashMap<String, ArrayList<Departure>> {
-        val andLine: String
-        if (lineId == null)
-            andLine = ""
+        val andLine: String = if (lineId == null)
+            ""
         else
-            andLine = "and line_id = '$lineId'"
+            "and line_id = '$lineId'"
+
+        if (lineId == null && _stopDepartures.contains(stopId)) {
+            _stopDeparturesCount[stopId] = _stopDeparturesCount[stopId]!! + 1
+            return _stopDepartures[stopId]!!
+        }
+        _stopDeparturesCount[stopId] = _stopDeparturesCount[stopId]?:0 + 1
         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 " +
@@ -114,6 +131,19 @@                     cursor.getString(1), cursor.getString(2), cursor.getInt(3) == 1,
                     cursor.getString(4), cursor.getString(5), tomorrow = tomorrow))
         }
         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
     }
 
@@ -140,5 +170,17 @@         cursor.moveToNext()
         element = cursor.getString(0)
         cursor.close()
         return element
+    }
+
+    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
     }
 }




diff --git a/app/src/main/res/layout/row_departure.xml b/app/src/main/res/layout/row_departure.xml
index 82ad610c4a49c3ee73231d57eddbc75f142d2815..21cef242ab886a2888a793b44d70600eaa40b954 100644
--- a/app/src/main/res/layout/row_departure.xml
+++ b/app/src/main/res/layout/row_departure.xml
@@ -38,7 +38,7 @@         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginStart="64dp"
         android:layout_marginTop="8dp"
-        android:text=""
+        android:text="@string/departure_row_getting_departures"
         android:textAppearance="@style/TextAppearance.AppCompat.Headline"
         app:layout_constraintStart_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 931fb2eaf4de825a9ff72fb9ac8f103276dfae16..33c0b0bc4be7d5185ece9d532f7809bdca720bea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -65,4 +65,5 @@         "Official timetable is especially prepared for holidays—it will show up as today (if"
         "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>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 6ad589e2c40f195793158af99f92d106103b7221..3d08457d03c0ac3c8ebe6fcebc503caa94e9697f 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -55,4 +55,5 @@         "Oficjalny rozkład jest specjalnie przygotowywany na święta — będzie widoczny w dzisiejszej"
         "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>
 </resources>
\ No newline at end of file




diff --git a/build.gradle b/build.gradle
index fadd9574797265e217e3c5152b8a46ce3bb9b107..8bb20f456380ed224c6055ed3de7c3076557fe5b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@         maven { url 'https://maven.google.com' }
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.0.0-alpha9'
+        classpath 'com.android.tools.build:gradle:3.0.0-beta2'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
         // NOTE: Do not place your application dependencies here; they belong




diff --git a/research/scraper.py b/research/scraper.py
index 1b6777d9f113eed31d9b564aacce15c047923d05..7109b9d730a39c50dfc4babceef440dcbcb85020 100755
--- a/research/scraper.py
+++ b/research/scraper.py
@@ -8,298 +8,230 @@ bike stations: http://www.ztm.poznan.pl/goeuropa-api/bike-stations
 
 """
 import json
-import hashlib
 import os
 import re
 import sqlite3
 import sys
-import time
 import requests
 from bs4 import BeautifulSoup
-import secrets
 
-
-def remove_options(text):
-    return re.sub('(<select[^>]*>([^<]*<option[^>]*>[^<]*</option>)+[^<]*</select>)','', text)
-
-
-def get_validity():
+class TimetableDownloader:
     """
-    get timetable validity
+    downloader class
     """
-    session = requests.session()
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/index', verify='bundle.pem')
-    option = re.search('<option value="[0-9]{8}" selected', index.text).group()
-    return option.split('"')[1]
+    def __init__(self, verbose):
+        self.session = requests.session()
+        self.verbose = verbose
 
 
-def get_nodes(checksum):
-    """
-    get nodes
-    """
-    session = requests.session()
+    def __get_validity(self):
+        """
+        get timetable validity
+        """
+        index = self.__get('https://www.ztm.poznan.pl/goeuropa-api/index')
+        option = re.search('<option value="[0-9]{8}" selected', index.text).group()
+        return option.split('"')[1]
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/all-nodes', verify='bundle.pem')
-    new_checksum = hashlib.sha512(index.text.encode('utf-8')).hexdigest()
-    if checksum == new_checksum:
-        return None
-    return [(stop['symbol'], stop['name']) for stop in json.loads(index.text)], new_checksum
 
+    def __get_nodes(self):
+        """
+        get nodes
+        """
+        index = self.__get('https://www.ztm.poznan.pl/goeuropa-api/all-nodes')
+        return [(stop['symbol'], stop['name']) for stop in json.loads(index.text)]
 
-def get_stops(node, checksum):
-    """
-    get stops
-    """
-    session = requests.session()
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/node_stops/{}'.format(node),
-                        verify='bundle.pem')
-    new_checksum = hashlib.sha512(index.text.encode('utf-8')).hexdigest()
-    if checksum == new_checksum:
-        return None
-    stops = []
-    for stop in json.loads(index.text):
-        stop_id = stop['stop']['id']
-        number = re.findall("\\d+", stop['stop']['symbol'])[0]
-        lat = stop['stop']['lat']
-        lon = stop['stop']['lon']
-        directions = ', '.join(['{} → {}'.format(transfer['name'], transfer['headsign'])
-                                for transfer in stop['transfers']])
-        stops.append((stop_id, node, number, lat, lon, directions))
-    return stops, new_checksum
+    def __get_stops(self, node):
+        """
+        get stops
+        """
+        index = self.__get('https://www.ztm.poznan.pl/goeuropa-api/node_stops/{}'.format(node))
+        stops = []
+        for stop in json.loads(index.text):
+            stop_id = stop['stop']['id']
+            number = re.findall("\\d+", stop['stop']['symbol'])[0]
+            lat = stop['stop']['lat']
+            lon = stop['stop']['lon']
+            directions = ', '.join(['{} → {}'.format(transfer['name'], transfer['headsign'])
+                                    for transfer in stop['transfers']])
+            stops.append((stop_id, node, number, lat, lon, directions))
+        return stops
 
 
-def get_lines(checksum):
-    """
-    get lines
-    """
-    session = requests.session()
+    def __get_lines(self):
+        """
+        get lines
+        """
+        index = self.__get('https://www.ztm.poznan.pl/goeuropa-api/index')
+        soup = BeautifulSoup(index.text, 'html.parser')
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/index', verify='bundle.pem')
-    index = re.sub('route-modal-[0-9a-f]{7}', '', index.text)
-    index = remove_options(index)
-    new_checksum = hashlib.sha512(index.encode('utf-8')).hexdigest()
-    if new_checksum == checksum:
-        return None
-    soup = BeautifulSoup(index, 'html.parser')
+        lines = {line['data-lineid']: line.text for line in
+                 soup.findAll(attrs={'class': re.compile(r'.*\blineNo-bt\b.*')})}
 
-    lines = {line['data-lineid']: line.text for line in
-             soup.findAll(attrs={'class': re.compile(r'.*\blineNo-bt\b.*')})}
+        return lines
 
-    return lines, new_checksum
 
+    def __get_route(self, line_id):
+        """
+        get routes
+        """
+        index = self.__get('https://www.ztm.poznan.pl/goeuropa-api/line-info/{}'.format(line_id))
+        soup = BeautifulSoup(index.text, 'html.parser')
+        directions = soup.findAll(attrs={'class': re.compile(r'.*\baccordion-item\b.*')})
+        routes = {}
+        for direction in directions:
+            direction_id = direction['data-directionid']
+            route = [{'id': stop.find('a')['data-stopid'], 'name': stop['data-name'],
+                      'onDemand': re.search('stop-onDemand', str(stop['class'])) != None}
+                     for stop in direction.findAll(attrs={'class': re.compile(r'.*\bstop-itm\b.*')})]
+            routes[direction_id] = route
+        return routes
 
-def get_route(line_id):
-    """
-    get routes
-    """
-    session = requests.session()
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/line-info/{}'.format(line_id),
-                        verify='bundle.pem')
-    soup = BeautifulSoup(index.text, 'html.parser')
-    directions = soup.findAll(attrs={'class': re.compile(r'.*\baccordion-item\b.*')})
-    routes = {}
-    for direction in directions:
-        direction_id = direction['data-directionid']
-        route = [{'id': stop.find('a')['data-stopid'], 'name': stop['data-name'],
-                  'onDemand': re.search('stop-onDemand', str(stop['class'])) != None}
-                 for stop in direction.findAll(attrs={'class': re.compile(r'.*\bstop-itm\b.*')})]
-        routes[direction_id] = route
-    return routes
-
+    def __get_stop_times(self, stop_id, line_id, direction_id):
+        """
+        get timetable
+        """
+        index = self.__post('https://www.ztm.poznan.pl/goeuropa-api/stop-info/{}/{}'.
+                                  format(stop_id, line_id), {'directionId': direction_id})
+        soup = BeautifulSoup(index.text, 'html.parser')
+        legends = {}
+        for row in soup.find(attrs={'class': re.compile(r'.*\blegend-box\b.*')}).findAll('li'):
+            row = row.text.split('-')
+            row[0] = row[0].rstrip()
+            row[1] = row[1].lstrip()
+            if row[0] != '_':
+                legends[row[0]] = '-'.join(row[1:])
+        schedules = {}
+        for mode in soup.findAll(attrs={'class': re.compile(r'.*\bmode-tab\b.*')}):
+            mode_name = mode['data-mode']
+            schedule = {row.find('th').text: [
+                {'time': minute.text, 'lowFloor': re.search('n-line', str(minute['class'])) != None}
+                for minute in row.findAll('a')]
+                        for row in mode.find(attrs={'class': re.compile(r'.*\bscheduler-hours\b.*')}).
+                        findAll('tr')}
+            schedule_2 = {hour: times for hour, times in schedule.items() if times != []}
+            schedule = []
+            for hour, deps in schedule_2.items():
+                for dep in deps:
+                    schedule.append((hour, *self.__describe(dep['time'], legends), dep['lowFloor']))
+            schedules[mode_name] = schedule
 
-def get_stop_times(stop_id, line_id, direction_id, checksum):
-    """
-    get timetable
-    """
-    session = requests.session()
+        return schedules
 
-    index = session.post('https://www.ztm.poznan.pl/goeuropa-api/stop-info/{}/{}'.
-                         format(stop_id, line_id), data={'directionId': direction_id},
-                         verify='bundle.pem')
-    index = re.sub('route-modal-[0-9a-f]{7}', '', index.text)
-    index = remove_options(index)
-    new_checksum = hashlib.sha512(index.encode('utf-8')).hexdigest()
-    if new_checksum == checksum:
-        return None
-    soup = BeautifulSoup(index, 'html.parser')
-    legends = {}
-    for row in soup.find(attrs={'class': re.compile(r'.*\blegend-box\b.*')}).findAll('li'):
-        row = row.text.split('-')
-        row[0] = row[0].rstrip()
-        row[1] = row[1].lstrip()
-        if row[0] != '_':
-            legends[row[0]] = '-'.join(row[1:])
-    schedules = {}
-    for mode in soup.findAll(attrs={'class': re.compile(r'.*\bmode-tab\b.*')}):
-        mode_name = mode['data-mode']
-        schedule = {row.find('th').text: [
-            {'time': minute.text, 'lowFloor': re.search('n-line', str(minute['class'])) != None}
-            for minute in row.findAll('a')]
-                    for row in mode.find(attrs={'class': re.compile(r'.*\bscheduler-hours\b.*')}).
-                    findAll('tr')}
-        schedule_2 = {hour: times for hour, times in schedule.items() if times != []}
-        schedule = []
-        for hour, deps in schedule_2.items():
-            for dep in deps:
-                schedule.append((hour, *describe(dep['time'], legends), dep['lowFloor']))
-        schedules[mode_name] = schedule
 
-    return schedules, new_checksum
+    @staticmethod
+    def __describe(dep_time, legend):
+        """
+        describe departure
+        """
+        desc = []
+        while re.match('^\\d+$', dep_time) is None:
+            try:
+                if dep_time[-1] != ',':
+                    desc.append(legend[dep_time[-1]])
+            except KeyError:
+                pass
+            dep_time = dep_time[:-1]
+        return (int(dep_time), '; '.join(desc))
 
 
-def describe(dep_time, legend):
-    """
-    describe departure
-    """
-    desc = []
-    while re.match('^\\d+$', dep_time) is None:
+    def __get(self, url):
         try:
-            if dep_time[-1] != ',':
-                desc.append(legend[dep_time[-1]])
-        except KeyError:
-            pass
-        dep_time = dep_time[:-1]
-    return (int(dep_time), '; '.join(desc))
+            return self.session.get(url, verify='bundle.pem')
+        except:
+            self.session = requests.session()
+            return self.session.get(url, verify='bundle.pem')
 
 
-def main():
-    """
-    main function
-    """
-    updating = False
-    changed = False
-    if os.path.exists('timetable.db'):
-        updating = True
+    def __post(self, url, data):
+        try:
+            return self.session.post(url, data=data, verify='bundle.pem')
+        except:
+            self.session = requests.session()
+            return self.session.post(url, data=data, verify='bundle.pem')
 
 
-    with sqlite3.connect('timetable.db') as connection:
-        try:
+    def download(self):
+        """
+        main function
+        """
+        if os.path.exists('timetable.db'):
+            connection = sqlite3.connect('timetable.db')
             cursor = connection.cursor()
-            if updating:
-                cursor.execute("select value from metadata where key = 'validFrom'")
-                current_valid_from = cursor.fetchone()[0]
-                if get_validity() <= current_valid_from:
-                    return 304
+            cursor.execute("select value from metadata where key = 'validFrom'")
+            current_valid_from = cursor.fetchone()[0]
+            cursor.close()
+            connection.close()
+            if self.__get_validity() <= current_valid_from:
+                return 304
             else:
+                os.remove('timetable.db')
+
+        with sqlite3.connect('timetable.db') as connection:
+            try:
+                cursor = connection.cursor()
                 cursor.execute('create table metadata(key TEXT PRIMARY KEY, value TEXT)')
-                cursor.execute('create table checksums(checksum TEXT, for TEXT, id TEXT)')
                 cursor.execute('create table nodes(symbol TEXT PRIMARY KEY, name TEXT)')
                 cursor.execute('create table stops(id TEXT PRIMARY KEY, symbol TEXT \
                                 references node(symbol), number TEXT, lat REAL, lon REAL, \
                                 headsigns TEXT)')
                 cursor.execute('create table lines(id TEXT PRIMARY KEY, number TEXT)')
-                cursor.execute('create table timetables(id TEXT PRIMARY KEY, stop_id TEXT references stop(id), \
-                                line_id TEXT references line(id), headsign TEXT)')
+                cursor.execute('create table timetables(id TEXT PRIMARY KEY, stop_id TEXT references \
+                                stop(id), line_id TEXT references line(id), headsign TEXT)')
                 cursor.execute('create table departures(id INTEGER PRIMARY KEY, \
                                 timetable_id TEXT references timetable(id), \
                                 hour INTEGER, minute INTEGER, mode TEXT, \
                                 lowFloor INTEGER, modification TEXT)')
 
-            cursor.execute("delete from metadata where key = 'validFrom'")
-            validity = get_validity()
-            print(validity)
-            cursor.execute("insert into metadata values('validFrom', ?)", (validity,))
-            cursor.execute("select checksum from checksums where for = 'nodes'")
-            checksum = cursor.fetchone()
-            if checksum != None:
-                checksum = checksum[0]
-            else:
-                checksum = ''
-            nodes_result = get_nodes(checksum)
-            if nodes_result is not None:
-                nodes, checksum = nodes_result
-                cursor.execute('delete from nodes')
-                cursor.execute("delete from checksums where for = 'nodes'")
-                cursor.execute("insert into checksums values(?, 'nodes', null)", (checksum,))  # update
+                validity = self.__get_validity()
+                print(validity)
+                sys.stdout.flush()
+                cursor.execute("insert into metadata values('validFrom', ?)", (validity,))
+                nodes = self.__get_nodes()
                 cursor.executemany('insert into nodes values(?, ?)', nodes)
-                changed = True
-            else:
-                cursor.execute('select * from nodes')
-                nodes = cursor.fetchall()
-                nodes = [(sym, nam) for sym, nam, _ in nodes]
-            nodes_no = len(nodes)
-            node_i = 1
-            for symbol, _ in nodes:
-                print('node {}'.format(node_i))
-                sys.stdout.flush()
-                cursor.execute("select checksum from checksums where for = 'node' and id = ?", (symbol,))
-                checksum = cursor.fetchone()
-                if checksum != None:
-                    checksum = checksum[0]
-                else:
-                    checksum = ''
-                stops_result = get_stops(symbol, checksum)
-                if stops_result is not None:
-                    stops, checksum = stops_result
-                    cursor.execute('delete from stops where symbol = ?', (symbol,))
+                node_i = 1
+                for symbol, _ in nodes:
+                    if self.verbose:
+                        print('node {}'.format(node_i))
+                    stops = self.__get_stops(symbol)
                     cursor.executemany('insert into stops values(?, ?, ?, ?, ?, ?)', stops)
-                    cursor.execute("update checksums set checksum = ? where for = 'node' and id = ?", (checksum, symbol))
-                    changed = True
-                node_i += 1
-            cursor.execute("select checksum from checksums where for = 'lines'")
-            checksum = cursor.fetchone()
-            if checksum != None:
-                checksum = checksum[0]
-            else:
-                checksum = ''
-            lines_result = get_lines(checksum)
-            if lines_result is not None:
-                lines, checksum = lines_result
-                cursor.execute('delete from lines')
-                cursor.execute("delete from checksums where for = 'lines'")
-                cursor.execute("insert into checksums values(?, 'lines', null)", (checksum,))  # update
+                    node_i += 1
+                lines = self.__get_lines()
                 cursor.executemany('insert into lines values(?, ?)', lines.items())
-                changed = True
-            else:
-                cursor.execute('select * from lines')
-                lines = cursor.fetchall()
 
-            lines_no = len(lines)
-            line_i = 1
-            for line_id, _ in lines.items():
-                route = get_route(line_id)
-                routes_no = len(route)
-                route_i = 1
-                for direction, stops in route.items():
-                    stops_no = len(stops)
-                    stop_i = 1
-                    for stop in stops:
-                        print("stop {} in route {} in line {}".format(stop_i, route_i, line_i))
-                        timetable_id = secrets.token_hex(4)
-                        sys.stdout.flush()
-                        cursor.execute("select checksum from checksums where for = 'timetable' and id = ?", (timetable_id,))
-                        checksum = cursor.fetchone()
-                        if checksum != None:
-                            checksum = checksum[0]
-                        else:
-                            checksum = ''
-                        stop_times = get_stop_times(stop['id'], line_id, direction, checksum)
-                        if stop_times is not None:
-                            timetables, checksum = stop_times
-                            cursor.execute('delete from timetables where line_id = ? and stop_id = ?',
-                                           (line_id, stop['id']))
+                timetable_id = 1
+                line_i = 1
+                for line_id, _ in lines.items():
+                    route = self.__get_route(line_id)
+                    route_i = 1
+                    for direction, stops in route.items():
+                        stop_i = 1
+                        for stop in stops:
+                            if self.verbose:
+                                print("stop {} in route {} in line {}".format(stop_i, route_i, line_i))
+                            timetables = self.__get_stop_times(stop['id'], line_id, direction)
                             cursor.execute('insert into timetables values(?, ?, ?, ?)',
                                            (timetable_id, stop['id'], line_id, stops[-1]['name']))
-                            cursor.execute("insert into checksums values(?, 'timetable', ?)", (checksum, timetable_id))
-                            changed = True
-                            cursor.execute('delete from departures where timetable_id = ?',
-                                           (timetable_id,))
                             for mode, times in timetables.items():
                                 cursor.executemany('insert into departures values(null, ?, ?, ?, ?, ?, \
                                                     ?)', [(timetable_id, hour, minute, mode, lowfloor, desc)
                                                           for hour, minute, desc, lowfloor in times])
-                        stop_i += 1
-                        sys.stdout.flush()
-                    route_i += 1
-                line_i += 1
-        except KeyboardInterrupt:
-            return 404
-    if changed:
+                            stop_i += 1
+                            timetable_id += 1
+                        route_i += 1
+                    line_i += 1
+            except KeyboardInterrupt:
+                return 404
         return 0
-    return 304
 
 
 if __name__ == '__main__':
-    exit(main())
+    verbose = False
+    try:
+        if sys.argv[1] == '-v':
+            verbose = True
+    except IndexError:
+        pass
+    downloader = TimetableDownloader(verbose)
+    exit(downloader.download())