Author: Adam Pioterek <adam.pioterek@protonmail.ch>
favourites, no refresh when searchbar has focus
.gitignore | 1 app/build.gradle | 4 app/src/main/AndroidManifest.xml | 2 | 80 app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt | 2 app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt | 2 app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt | 25 app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt | 4 app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt | 40 app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt | 75 app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt | 11 app/src/main/res/drawable/ic_favourite_empty.xml | 9 app/src/main/res/drawable/ic_more.xml | 9 | 2 app/src/main/res/layout/activity_stop.xml | 1 | 0 app/src/main/res/layout/row_favourite.xml | 64 app/src/main/res/menu/favourite_actions.xml | 11 app/src/main/res/values-pl/strings.xml | 3 app/src/main/res/values/strings.xml | 4 research/scraper.py | 251
diff --git a/.gitignore b/.gitignore index 25e5aea018ddc954e9616352ba97ad36fcdb9ab8..03e31c52b899a1f7d7fae329bddd1c922968b798 100644 --- a/.gitignore +++ b/.gitignore @@ -49,5 +49,6 @@ research/cron research/peka-vm-api.md research/svg/ research/timetable/ +research/timetable_new/ research/timetable.db research/timetable.db-journal diff --git a/app/build.gradle b/app/build.gradle index 6f0fc81f446064077f1787e2f4ce4a60915f4042..3af2e3522988b21a6b14c51fb7d4a649747eeb19 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ defaultConfig { applicationId "ml.adamsprogs.bimba" minSdkVersion 19 targetSdkVersion 25 - versionCode 2 - versionName "1.0.1" + versionCode 3 + versionName "1.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d39599789d29518ca69c703776b72dba55903f21..907ec3e2fdfe60bc23b6a159134f22cc61a30d06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:icon="@drawable/logo" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> - <activity android:name=".activities.MainActivity" /> + <activity android:name=".activities.DashActivity" /> <service android:name=".TimetableDownloader" diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e7e894d5d329e49b287b8c16c8a34790b64e7aa --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -0,0 +1,203 @@ +package ml.adamsprogs.bimba.activities + +import android.content.* +import android.os.* +import android.support.design.widget.Snackbar +import android.support.v7.app.* +import android.text.Html +import android.view.View +import com.arlib.floatingsearchview.FloatingSearchView +import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion +import ml.adamsprogs.bimba.models.* +import kotlin.concurrent.thread +import android.app.Activity +import android.support.v4.widget.* +import android.support.v7.widget.* +import android.util.Log +import android.view.inputmethod.InputMethodManager +import com.google.gson.Gson +import com.google.gson.JsonObject +import ml.adamsprogs.bimba.* + +//todo refresh every 15s +class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener { + val context: Context = this + val receiver = MessageReceiver() + lateinit var timetable: Timetable + var stops: ArrayList<StopSuggestion>? = null + lateinit var swipeRefreshLayout: SwipeRefreshLayout + lateinit var favouritesList: RecyclerView + lateinit var searchView: FloatingSearchView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_dash) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO) + + prepareSwipeLayout() + + prepareOnDownloadListener() + startDownloaderService() + + getStops() + + prepareFavourites() + + searchView = findViewById(R.id.search_view) as FloatingSearchView + + searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener { + override fun onFocus() { + swipeRefreshLayout.isEnabled = false + favouritesList.visibility = View.GONE + } + + override fun onFocusCleared() { + swipeRefreshLayout.isEnabled = true + favouritesList.visibility = View.VISIBLE + } + }) + + searchView.setOnQueryChangeListener({ oldQuery, newQuery -> + if (oldQuery != "" && newQuery == "") + searchView.clearSuggestions() + thread { + val newStops = stops!!.filter { deAccent(it.body.split("\n")[0]).contains(deAccent(newQuery), true) } + runOnUiThread { searchView.swapSuggestions(newStops) } + } + }) + + searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener { + override fun onSuggestionClicked(searchSuggestion: SearchSuggestion) { + val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + var view = (context as DashActivity).currentFocus + if (view == null) { + view = View(context) + } + imm.hideSoftInputFromWindow(view.windowToken, 0) + intent = Intent(context, StopActivity::class.java) + intent.putExtra("stopId", (searchSuggestion as StopSuggestion).id) + intent.putExtra("stopSymbol", (searchSuggestion as StopSuggestion).symbol) + startActivity(intent) + } + + override fun onSearchAction(query: String) { + } + }) + + searchView.setOnBindSuggestionCallback { _, _, textView, item, _ -> + val suggestion = item as StopSuggestion + val text = suggestion.body.split("\n") + val t = "<small><font color=\"#a0a0a0\">" + text[1] + "</font></small>" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + textView.text = Html.fromHtml(text[0] + "<br/>" + t, Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") + textView.text = Html.fromHtml(text[0] + "<br/>" + t) + } + } + + //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout) + } + + private fun prepareFavourites() { + val layoutManager = LinearLayoutManager(context) + favouritesList = findViewById(R.id.favouritesList) as RecyclerView + favouritesList.adapter = FavouritesAdapter(context, getFavourites()) + favouritesList.layoutManager = layoutManager + } + + private fun getFavourites(): ArrayList<Favourite> { + val preferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE) + val favouritesString = preferences.getString("favourites", "{}") + val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) + val favourites = ArrayList<Favourite>() + for ((name, jsonTimetables) in favouritesMap.entrySet()) { + val timetables = ArrayList<HashMap<String, String>>() + for (jsonTimetable in jsonTimetables.asJsonArray) { + val timetable = HashMap<String, String>() + timetable["stop"] = jsonTimetable.asJsonObject["stop"].asString + timetable["line"] = jsonTimetable.asJsonObject["line"].asString + timetables.add(timetable) + } + favourites.add(Favourite(name, timetables, context)) + } + return favourites + } + + private fun getStops() { + timetable = Timetable(this) + stops = timetable.getStops() + } + + private fun prepareOnDownloadListener() { + val filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded") + filter.addCategory(Intent.CATEGORY_DEFAULT) + registerReceiver(receiver, filter) + receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) + } + + private fun startDownloaderService() { + startService(Intent(context, TimetableDownloader::class.java)) + } + + private fun prepareSwipeLayout() { + swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout + swipeRefreshLayout.isEnabled = true + swipeRefreshLayout.setOnRefreshListener(this) + swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary) + } + + override fun onRefresh() { + swipeRefreshLayout.isRefreshing = true + Log.i("Refresh", "Downloading") + startDownloaderService() + } + + override fun onBackPressed() { + if (!searchView.setSearchFocused(false)) { + super.onBackPressed() + } + } + + override fun onResume() { + super.onResume() + favouritesList.adapter = FavouritesAdapter(context, getFavourites()) + favouritesList.adapter.notifyDataSetChanged() + } + + override fun onDestroy() { + super.onDestroy() + receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) + unregisterReceiver(receiver) + timetable.close() + } + + fun deAccent(str: String): String { + var result = str.replace('ę', 'e') + result = result.replace('ó', 'o') + result = result.replace('ą', 'a') + result = result.replace('ś', 's') + result = result.replace('ł', 'l') + result = result.replace('ż', 'ż') + result = result.replace('ź', 'ź') + result = result.replace('ć', 'ć') + result = result.replace('ń', 'n') + return result + } + + override fun onTimetableDownload(result: String?) { + Log.i("Refresh", "downloaded: $result") + val message: String + when (result) { + "downloaded" -> message = getString(R.string.timetable_downloaded) + "no connectivity" -> message = getString(R.string.no_connectivity) + "up-to-date" -> message = getString(R.string.timetable_up_to_date) + "validity failed" -> message = getString(R.string.validity_failed) + else -> message = getString(R.string.error_try_later) + } + timetable.refresh() + stops = timetable.getStops() + Snackbar.make(swipeRefreshLayout, message, Snackbar.LENGTH_LONG).show() + swipeRefreshLayout.isRefreshing = false + } +} diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt deleted file mode 100644 index cbb03117e13aa306d279f6f30ff9310b5cbce8f3..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt +++ /dev/null @@ -1,149 +0,0 @@ -package ml.adamsprogs.bimba.activities - -import android.content.Intent -import android.content.IntentFilter -import android.os.* -import android.support.design.widget.Snackbar -import android.support.v7.app.* -import android.text.Html -import android.view.View -import com.arlib.floatingsearchview.FloatingSearchView -import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion -import ml.adamsprogs.bimba.models.* -import kotlin.concurrent.thread -import android.app.Activity -import android.content.Context -import android.support.v4.widget.SwipeRefreshLayout -import android.util.Log -import android.view.inputmethod.InputMethodManager -import ml.adamsprogs.bimba.MessageReceiver -import ml.adamsprogs.bimba.R -import ml.adamsprogs.bimba.TimetableDownloader - - -class MainActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener { - val context: Context = this - val receiver = MessageReceiver() - lateinit var timetable: Timetable - var stops: ArrayList<StopSuggestion>? = null - lateinit var swipeRefreshLayout: SwipeRefreshLayout - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO) - - prepareSwipeLayout() - - prepareOnDownloadListener() - startDownloaderService() - - getStops() - - val searchView = findViewById(R.id.search_view) as FloatingSearchView - - searchView.setOnQueryChangeListener({ _, newQuery -> - thread { - val newStops = stops!!.filter { deAccent(it.body.split("\n")[0]).contains(deAccent(newQuery), true) } - runOnUiThread { searchView.swapSuggestions(newStops) } - } - }) - - searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener { - override fun onSuggestionClicked(searchSuggestion: SearchSuggestion) { - val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - var view = (context as MainActivity).currentFocus - if (view == null) { - view = View(context) - } - imm.hideSoftInputFromWindow(view.windowToken, 0) - intent = Intent(context, StopActivity::class.java) - intent.putExtra("stopId", (searchSuggestion as StopSuggestion).id) - intent.putExtra("stopSymbol", (searchSuggestion as StopSuggestion).symbol) - startActivity(intent) - } - - override fun onSearchAction(query: String) { - } - }) - - searchView.setOnBindSuggestionCallback { _, _, textView, item, _ -> - val suggestion = item as StopSuggestion - val text = suggestion.body.split("\n") - val t = "<small><font color=\"#a0a0a0\">" + text[1] + "</font></small>" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - textView.text = Html.fromHtml(text[0] + "<br/>" + t, Html.FROM_HTML_MODE_LEGACY) - } else { - @Suppress("DEPRECATION") - textView.text = Html.fromHtml(text[0] + "<br/>" + t) - } - } - - //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout) - } - - private fun getStops() { - timetable = Timetable(this) - stops = timetable.getStops() - } - - private fun prepareOnDownloadListener() { - val filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded") - filter.addCategory(Intent.CATEGORY_DEFAULT) - registerReceiver(receiver, filter) - receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - } - - private fun startDownloaderService() { - startService(Intent(context, TimetableDownloader::class.java)) - } - - private fun prepareSwipeLayout() { - swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout - swipeRefreshLayout.isEnabled = true - swipeRefreshLayout.setOnRefreshListener(this) - swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary) - } - - override fun onRefresh() { - swipeRefreshLayout.isRefreshing = true - Log.i("Refresh", "Downloading") - startDownloaderService() - } - - override fun onDestroy() { - super.onDestroy() - receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - unregisterReceiver(receiver) - timetable.close() - } - - fun deAccent(str: String): String { - var result = str.replace('ę', 'e') - result = result.replace('ó', 'o') - result = result.replace('ą', 'a') - result = result.replace('ś', 's') - result = result.replace('ł', 'l') - result = result.replace('ż', 'ż') - result = result.replace('ź', 'ź') - result = result.replace('ć', 'ć') - result = result.replace('ń', 'n') - return result - } - - override fun onTimetableDownload(result: String?) { - Log.i("Refresh", "downloaded: $result") - val message: String - when (result) { - "downloaded" -> message = getString(R.string.timetable_downloaded) - "no connectivity" -> message = getString(R.string.no_connectivity) - "up-to-date" -> message = getString(R.string.timetable_up_to_date) - "validity failed" -> message = getString(R.string.validity_failed) - else -> message = getString(R.string.error_try_later) - } - timetable.refresh() - stops = timetable.getStops() - Snackbar.make(swipeRefreshLayout, message, Snackbar.LENGTH_LONG).show() - swipeRefreshLayout.isRefreshing = false - } -} 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 6e89218c7fd22a9565f0f86a86e19f5eac2a622f..01d759ab8d045afdaa50fc9d544e0b5c641e7dea 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt @@ -53,7 +53,7 @@ when (result) { "downloaded" -> { timetableDownloadReceiver.removeOnTimetableDownloadListener(this) networkStateReceiver.removeOnConnectivityChangeListener(this) - startActivity(Intent(this, MainActivity::class.java)) + startActivity(Intent(this, DashActivity::class.java)) finish() } else -> (findViewById(R.id.noDbCaption) as TextView).text = getString(R.string.error_try_later) 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 98a0fed99b714a63fb103c20389d8563598e7847..d1c341f365785ba288f0dcb13ab2f1025de2fb29 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt @@ -12,7 +12,7 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val timetable = Timetable(this) if(timetable.isDatabaseHealthy()) - startActivity(Intent(this, MainActivity::class.java)) + startActivity(Intent(this, DashActivity::class.java)) else startActivity(Intent(this, NoDbActivity::class.java)) timetable.close() 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 33cce2f47bec51a56085f45083dc0b8fabe48452..6fc79e3fde4c096a6231ef495ba2caf8543db131 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -12,7 +12,7 @@ import android.support.v7.widget.* import android.support.v4.app.* import android.support.v4.view.* import android.support.v4.content.res.ResourcesCompat -import com.google.gson.Gson +import com.google.gson.* import ml.adamsprogs.bimba.models.* import ml.adamsprogs.bimba.* @@ -66,15 +66,24 @@ scheduleRefresh() val fab = findViewById(R.id.fab) as FloatingActionButton + + var favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java) + if (favourites[stopSymbol] == null) { + fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme)) + } + fab.setOnClickListener { - var favouritesString = sharedPreferences.getString("favourites", "{}") - @Suppress("UNCHECKED_CAST") - val favourites = Gson().fromJson(favouritesString, HashMap::class.java) as HashMap<String, ArrayList<HashMap<String,String>>> + favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java) if (favourites[stopSymbol] == null) { - val items = ArrayList<HashMap<String, String>>() - timetable.getLines(stopId)?.forEach {items.add(mapOf("stop" to stopId, "line" to it) as HashMap<String, String>)} - favourites[stopSymbol] = items - favouritesString = Gson().toJson(favourites) + val items = JsonArray() + timetable.getLines(stopId)?.forEach { + val o = JsonObject() + o.addProperty("stop", stopId) + o.addProperty("line", it) + items.add(o) + } + favourites.add(stopSymbol,items) + val favouritesString = Gson().toJson(favourites) val editor = sharedPreferences.edit() editor.putString("favourites", favouritesString) editor.apply() 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 cf66af128555042a84ca3d9faae926d77b90f692..a8b0c8e50301ad5526416091723a16c271574ed5 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt @@ -96,8 +96,8 @@ override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val context = parent?.context val inflater = LayoutInflater.from(context) - val contactView = inflater.inflate(R.layout.departure_row, parent, false) - val viewHolder = ViewHolder(contactView) + val rowView = inflater.inflate(R.layout.row_departure, parent, false) + val viewHolder = ViewHolder(rowView) return viewHolder } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt new file mode 100644 index 0000000000000000000000000000000000000000..7e89bd7184783c96401ef4cc490302041ae0a8d9 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt @@ -0,0 +1,40 @@ +package ml.adamsprogs.bimba.models + +import android.content.Context +import java.util.* + +class Favourite(var name: String, var timetables: ArrayList<HashMap<String, String>>, context: Context) { + val timetable = Timetable(context) + + var nextDeparture: Departure? = null + get() { + val today: String + val allDepartures = ArrayList<Departure>() + val now = Calendar.getInstance() + val departureTime = Calendar.getInstance() + when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.SUNDAY -> today = "sundays" + Calendar.SATURDAY -> today = "saturdays" + else -> today = "workdays" + } + + for (t in timetables) + allDepartures.addAll(timetable.getStopDepartures(t["stop"] as String, t["line"])!![today]!!) + var minDeparture: Departure = allDepartures[0] + var minInterval = 24 * 60L + for (departure in allDepartures) { + departureTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(departure.time.split(":")[0])) + departureTime.set(Calendar.MINUTE, Integer.parseInt(departure.time.split(":")[1])) + if (departure.tomorrow) + departureTime.add(Calendar.DAY_OF_MONTH, 1) + val interval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60) + if (interval in 0..(minInterval - 1)) { + minInterval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60) + minDeparture = departure + } + } + + return minDeparture + } + private set +} diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d97e13817f530006d3a3d4537557e9bedb0f374a --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt @@ -0,0 +1,75 @@ +package ml.adamsprogs.bimba.models + +import android.content.Context +import android.support.v7.widget.PopupMenu +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import ml.adamsprogs.bimba.R +import android.view.LayoutInflater +import java.util.* + + +class FavouritesAdapter(val context: Context, val favourites: List<Favourite>) : + RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() { + override fun getItemCount(): Int { + return favourites.size + } + + override fun onBindViewHolder(holder: ViewHolder?, position: Int) { + + val favourite = favourites[position] + holder?.nameTextView?.text = favourite.name + val nextDeparture = favourite.nextDeparture ?: return + val now = Calendar.getInstance() + val departureTime = Calendar.getInstance() + departureTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(nextDeparture.time.split(":")[0])) + departureTime.set(Calendar.MINUTE, Integer.parseInt(nextDeparture.time.split(":")[1])) + if (nextDeparture.tomorrow) + departureTime.add(Calendar.DAY_OF_MONTH, 1) + val interval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60) + holder?.timeTextView?.text = context.getString(R.string.departure_in, interval.toString()) + holder?.lineTextView?.text = context.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.direction) + holder?.moreButton?.setOnClickListener { + val popup = PopupMenu(context, it) + val inflater = popup.menuInflater + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.favourite_edit -> editFavourite(favourite.name) + R.id.favourite_delete -> deleteFavourite(favourite.name) + else -> false + } + } + inflater.inflate(R.menu.favourite_actions, popup.menu) + popup.show() + } + } + + private fun editFavourite(name: String): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return true + } + + private fun deleteFavourite(name: String): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return true + } + + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { + 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 + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val nameTextView = itemView.findViewById(R.id.favourite_name) as TextView + val timeTextView = itemView.findViewById(R.id.favourite_time) as TextView + val lineTextView = itemView.findViewById(R.id.favourite_line) as TextView + val moreButton = itemView.findViewById(R.id.favourite_more_button) as ImageView + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt index 96a416a9780a934fb7bd6694c1019ffbf3098cb4..2117cdd3dbd5a679c3f0ffb51d6b55e37a610802 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt @@ -67,13 +67,18 @@ cursor.close() return name } - fun getStopDepartures(stopId: String): HashMap<String, ArrayList<Departure>>? { + fun getStopDepartures(stopId: String, lineId: String? = null): HashMap<String, ArrayList<Departure>>? { if (db == null) return null + val andLine:String + if (lineId == null) + andLine = "" + else + andLine = "and line_id = '$lineId'" val cursor = db!!.rawQuery("select lines.number, mode, substr('0'||hour, -2) || ':' || " + "substr('0'||minute, -2) as time, lowFloor, modification, headsign from departures join " + "timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) where " + - "stop_id = ? order by mode, time;", listOf(stopId).toTypedArray()) + "stop_id = ? $andLine order by mode, time;", listOf(stopId).toTypedArray()) val departures = HashMap<String, ArrayList<Departure>>() departures.put("workdays", ArrayList()) departures.put("saturdays", ArrayList()) @@ -95,7 +100,7 @@ "stops on(stop_id = stops.id) where stops.id = ?;", listOf(stopId).toTypedArray()) val lines = ArrayList<String>() while (cursor.moveToNext()) { - lines.add(cursor.getString(1)) + lines.add(cursor.getString(0)) } cursor.close() return lines diff --git a/app/src/main/res/drawable/ic_favourite_empty.xml b/app/src/main/res/drawable/ic_favourite_empty.xml new file mode 100644 index 0000000000000000000000000000000000000000..b36536b997c2bac42f2b4b09bae0e2de03d08917 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_empty.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9d756cad67783d7ac68687ef1d196ed887bbeba --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/layout/activity_dash.xml b/app/src/main/res/layout/activity_dash.xml new file mode 100644 index 0000000000000000000000000000000000000000..f742d2c57060380e3d98edf399bdde2cd36d4001 --- /dev/null +++ b/app/src/main/res/layout/activity_dash.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="ml.adamsprogs.bimba.activities.DashActivity"> + + <android.support.constraint.ConstraintLayout + android:id="@+id/main_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.arlib.floatingsearchview.FloatingSearchView + android:id="@+id/search_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:floatingSearch_close_search_on_keyboard_dismiss="true" + app:floatingSearch_leftActionMode="showSearch" + app:floatingSearch_searchBarMarginLeft="16dp" + app:floatingSearch_searchBarMarginRight="16dp" + app:floatingSearch_searchBarMarginTop="16dp" + app:floatingSearch_searchHint="@string/search_placeholder" + app:floatingSearch_showSearchKey="false" + app:floatingSearch_suggestionsListAnimDuration="250" /> + + + <android.support.v7.widget.RecyclerView + android:id="@+id/favouritesList" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="none" + android:layout_marginTop="128dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="8dp" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginEnd="8dp" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="8dp" /> + + </android.support.constraint.ConstraintLayout> +</android.support.v4.widget.SwipeRefreshLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index c18b6115de3cd974a2f3fac976630ff35e48beab..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/swipeRefreshLayout" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context="ml.adamsprogs.bimba.activities.MainActivity"> - - <android.support.constraint.ConstraintLayout - android:id="@+id/main_layout" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <com.arlib.floatingsearchview.FloatingSearchView - android:id="@+id/search_view" - android:layout_width="match_parent" - android:layout_height="match_parent" - app:floatingSearch_close_search_on_keyboard_dismiss="true" - app:floatingSearch_leftActionMode="showSearch" - app:floatingSearch_searchBarMarginLeft="16dp" - app:floatingSearch_searchBarMarginRight="16dp" - app:floatingSearch_searchBarMarginTop="16dp" - app:floatingSearch_searchHint="@string/search_placeholder" - app:floatingSearch_showSearchKey="false" - app:floatingSearch_suggestionsListAnimDuration="250" /> - - - <android.support.v7.widget.RecyclerView - android:id="@+id/favouritesList" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="none" - android:layout_marginTop="128dp" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="8dp" - app:layout_constraintEnd_toEndOf="parent" - android:layout_marginEnd="8dp" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="8dp" /> - - </android.support.constraint.ConstraintLayout> -</android.support.v4.widget.SwipeRefreshLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_stop.xml b/app/src/main/res/layout/activity_stop.xml index b36285d541bc1d4833dbe2550150d8bc7630f925..a56ab1e2d284b956c2fb13106f8d74bd7d2eabcc 100644 --- a/app/src/main/res/layout/activity_stop.xml +++ b/app/src/main/res/layout/activity_stop.xml @@ -65,7 +65,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="@dimen/fab_margin" - android:visibility="gone" app:srcCompat="@drawable/ic_favourite" /> </android.support.design.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/departure_row.xml b/app/src/main/res/layout/departure_row.xml deleted file mode 100644 index d6b0a4371cce9c0c482bfb3c53e52a97724d6028..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/departure_row.xml +++ /dev/null @@ -1,57 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<android.support.constraint.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - tools:layout_editor_absoluteY="25dp" - tools:layout_editor_absoluteX="0dp"> - - <TextView - android:id="@+id/lineNumber" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="" - android:textAppearance="@style/TextAppearance.AppCompat.Title" - app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="8dp" - app:layout_constraintTop_toTopOf="parent" - android:layout_marginTop="8dp" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="8dp" - app:layout_constraintEnd_toStartOf="@+id/departureTime" - android:layout_marginEnd="8dp" /> - - <ImageView - android:id="@+id/departureTypeIcon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="8dp" - android:layout_marginTop="8dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - android:contentDescription="@string/departure_type_icon_description" /> - - <TextView - android:id="@+id/departureTime" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="" - android:textAppearance="@style/TextAppearance.AppCompat.Headline" - android:layout_marginTop="8dp" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toEndOf="parent" - android:layout_marginStart="64dp" /> - - <TextView - android:id="@+id/departureDirection" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="8dp" - app:layout_constraintTop_toBottomOf="@+id/departureTime" - app:layout_constraintStart_toEndOf="parent" - android:layout_marginStart="64dp" /> -</android.support.constraint.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/row_departure.xml b/app/src/main/res/layout/row_departure.xml new file mode 100644 index 0000000000000000000000000000000000000000..d6b0a4371cce9c0c482bfb3c53e52a97724d6028 --- /dev/null +++ b/app/src/main/res/layout/row_departure.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:layout_editor_absoluteY="25dp" + tools:layout_editor_absoluteX="0dp"> + + <TextView + android:id="@+id/lineNumber" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="" + android:textAppearance="@style/TextAppearance.AppCompat.Title" + app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="8dp" + app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="8dp" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="8dp" + app:layout_constraintEnd_toStartOf="@+id/departureTime" + android:layout_marginEnd="8dp" /> + + <ImageView + android:id="@+id/departureTypeIcon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginTop="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="@string/departure_type_icon_description" /> + + <TextView + android:id="@+id/departureTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" + android:layout_marginTop="8dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="parent" + android:layout_marginStart="64dp" /> + + <TextView + android:id="@+id/departureDirection" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="8dp" + app:layout_constraintTop_toBottomOf="@+id/departureTime" + app:layout_constraintStart_toEndOf="parent" + android:layout_marginStart="64dp" /> +</android.support.constraint.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/row_favourite.xml b/app/src/main/res/layout/row_favourite.xml new file mode 100644 index 0000000000000000000000000000000000000000..e1a3189141bc6f2d966cdb4088f4539ecd20a8c4 --- /dev/null +++ b/app/src/main/res/layout/row_favourite.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:layout_editor_absoluteX="8dp" + tools:layout_editor_absoluteY="128dp"> + + <android.support.constraint.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/favourite_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="21dp" + android:text="aaaa" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" + app:layout_constraintStart_toStartOf="@+id/favourite_time" + app:layout_constraintTop_toTopOf="parent" + tools:layout_editor_absoluteX="16dp" + tools:layout_editor_absoluteY="21dp" /> + + <TextView + android:id="@+id/favourite_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:layout_marginStart="16dp" + android:text="bbbbb" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/favourite_time" + tools:layout_editor_absoluteX="16dp" + tools:layout_editor_absoluteY="78dp" /> + + <TextView + android:id="@+id/favourite_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="ccccc" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" + app:layout_constraintStart_toStartOf="@+id/favourite_line" + app:layout_constraintTop_toBottomOf="@+id/favourite_name" + tools:layout_editor_absoluteX="16dp" + tools:layout_editor_absoluteY="46dp" /> + + <ImageView + android:id="@+id/favourite_more_button" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="16dp" + android:contentDescription="@string/favourite_row_more_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_more" + tools:layout_editor_absoluteX="328dp" + tools:layout_editor_absoluteY="16dp" /> + </android.support.constraint.ConstraintLayout> +</android.support.v7.widget.CardView> \ No newline at end of file diff --git a/app/src/main/res/menu/favourite_actions.xml b/app/src/main/res/menu/favourite_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..bf229e060737d7e460e0ce3529001eec33b63c61 --- /dev/null +++ b/app/src/main/res/menu/favourite_actions.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/favourite_edit" + android:orderInCategory="100" + android:title="@string/action_edit" /> + <item + android:id="@+id/favourite_delete" + android:orderInCategory="200" + android:title="@string/action_delete" /> +</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 01718d6790b7e077858dbdceef2f2025c4097d4e..59eab5f8398508474e4978a95a5c2ea60d22b81b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,7 @@Type <string name="departure_type_icon_description" translatable="false">departure type (timetable, VM)</string> <string name="departure_in">In %1$s minutes</string> <string name="departure_to">→ %1$s</string> + <string name="departure_to_line">%1$s → %2$s</string> <string name="departure_at">At %1$s</string> <string name="no_database_background" translatable="false">no database background</string> <string name="no_db_connect">Connect to the Internet to download the timetable</string> @@ -22,4 +23,7 @@Downloaded timetable is corrupted – can’t update <string name="error_try_later">Error. Try again later</string> <string name="now">Now</string> <string name="stop_already_fav">This stop is already in favourites</string> + <string name="favourite_row_more_button" translatable="false">favourite row more button</string> + <string name="action_edit">Edit</string> + <string name="action_delete">Delete</string> </resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3ab41cc7ffb8d0e04daa8ddc5362e1b1ae7bb473..d95d96583cbfd31bc307f6f239448f4967e10343 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -19,4 +19,7 @@Brak połączenia z Internetem – nie można zaktualizować rozkładu <string name="error_try_later">Błąd. Spróbuj ponownie później</string> <string name="now">Teraz</string> <string name="stop_already_fav">Ten przystanek już jest pośród ulubionych</string> + <string name="departure_to_line">%1$s → %2$s</string> + <string name="action_delete">Usuń</string> + <string name="action_edit">Edytuj</string> </resources> \ No newline at end of file diff --git a/research/scraper.py b/research/scraper.py index 96b855fb4275f76a18daad2486598937c3391e31..37dd35d799fddc496ec0b2f9e1816f63785707d9 100755 --- a/research/scraper.py +++ b/research/scraper.py @@ -16,24 +16,47 @@ import sys import time import requests from bs4 import BeautifulSoup +import secrets -def get_nodes(): + +def remove_options(text): + return re.sub('(<select[^>]*>([^<]*<option[^>]*>[^<]*</option>)+[^<]*</select>)','', text) + + +def get_validity(): + """ + get timetable validity + """ + 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 get_nodes(checksum): """ get nodes """ session = requests.session() index = session.get('https://www.ztm.poznan.pl/goeuropa-api/all-nodes', verify='bundle.pem') - return [(stop['symbol'], stop['name']) for stop in json.loads(index.text)] + 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_stops(node): +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') + 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'] @@ -43,22 +66,27 @@ 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 + return stops, new_checksum -def get_lines(): +def get_lines(checksum): """ get lines """ session = requests.session() index = session.get('https://www.ztm.poznan.pl/goeuropa-api/index', verify='bundle.pem') - soup = BeautifulSoup(index.text, 'html.parser') + 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.*')})} - return lines + return lines, new_checksum def get_route(line_id): @@ -67,7 +95,8 @@ get routes """ session = requests.session() - index = session.get('https://www.ztm.poznan.pl/goeuropa-api/line-info/{}'.format(line_id), verify='bundle.pem') + 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 = {} @@ -80,15 +109,21 @@ routes[direction_id] = route return routes -def get_stop_times(stop_id, line_id, direction_id): +def get_stop_times(stop_id, line_id, direction_id, checksum): """ get timetable """ session = requests.session() index = session.post('https://www.ztm.poznan.pl/goeuropa-api/stop-info/{}/{}'. - format(stop_id, line_id), data={'directionId': direction_id}, verify='bundle.pem') - soup = BeautifulSoup(index.text, 'html.parser') + 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('-') @@ -111,7 +146,7 @@ for dep in deps: schedule.append((hour, *describe(dep['time'], legends), dep['lowFloor'])) schedules[mode_name] = schedule - return schedules, hashlib.sha512(index.text.encode('utf-8')).hexdigest() + return schedules, new_checksum def describe(dep_time, legend): @@ -130,68 +165,146 @@ def main(): """ main function """ + updating = False + changed = False if os.path.exists('timetable.db'): - return + updating = True + print(time.time()) with sqlite3.connect('timetable.db') as connection: - print('creating tables') - cursor = connection.cursor() - 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 INTEGER PRIMARY KEY, stop_id \ - TEXT references stop(id), line_id TEXT references line(id), \ - headsign TEXT, checksum TEXT)') - cursor.execute('create table departures(id INTEGER PRIMARY KEY, timetable_id INTEGER \ - references timetable(id), hour INTEGER, minute INTEGER, mode TEXT, \ - lowFloor INTEGER, modification TEXT)') + try: + 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 + else: + print('creating tables') + 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 departures(id INTEGER PRIMARY KEY, \ + timetable_id TEXT references timetable(id), \ + hour INTEGER, minute INTEGER, mode TEXT, \ + lowFloor INTEGER, modification TEXT)') + + print('getting validity') + cursor.execute("delete from metadata where key = 'validFrom'") + cursor.execute("insert into metadata values('validFrom', ?)", (get_validity(),)) + print('getting nodes') + 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 + 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) + print('getting stops') + node_i = 1 + for symbol, _ in nodes: + print('\rnode {}/{}'.format(node_i, nodes_no), end='') + 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,)) + 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 + print('\ngetting lines') + 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 + cursor.executemany('insert into lines values(?, ?)', lines.items()) + changed = True + else: + cursor.execute('select * from lines') + lines = cursor.fetchall() - print('getting nodes') - nodes = get_nodes() - cursor.executemany('insert into nodes values(?, ?);', nodes) - nodes_no = len(nodes) - print('getting stops') - node_i = 1 - for symbol, _ in nodes: - print('\rstop {}/{}'.format(node_i, nodes_no), end='') - sys.stdout.flush() - cursor.executemany('insert into stops values(?, ?, ?, ?, ?, ?);', get_stops(symbol)) - node_i += 1 - print('') - lines = get_lines() - lines_no = len(lines) - line_i = 1 - tti = 0 - cursor.executemany('insert into lines values(?, ?);', lines.items()) - 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('line {}/{} route {}/{} stop {}/{}'. - format(line_i, lines_no, route_i, routes_no, stop_i, stops_no), end='') - sys.stdout.flush() - timetables, checksum = get_stop_times(stop['id'], line_id, direction) - cursor.execute('insert into timetables values(?, ?, ?, ?, ?);', - (tti, stop['id'], line_id, stops[-1]['name'], checksum)) - for mode, times in timetables.items(): - cursor.executemany('insert into departures values(null, ?, ?, ?, ?, ?, ?);', - [(tti, hour, minute, mode, lowfloor, desc) - for hour, minute, desc, lowfloor in times]) - stop_i += 1 - tti += 1 - print('{}\r'.format(' '*35), end='') - sys.stdout.flush() - route_i += 1 - print('') - line_i += 1 + 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: + timetable_id = secrets.token_hex(4) + print('line {}/{} route {}/{} stop {}/{}'. + format(line_i, lines_no, route_i, routes_no, stop_i, stops_no), end='') + 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'])) + 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 + print('{}\r'.format(' '*35), end='') + sys.stdout.flush() + route_i += 1 + print('') + line_i += 1 + except KeyboardInterrupt: + return 404 print(time.time()) + if changed: + return 0 + return 304 if __name__ == '__main__': - main() + exit(main())