Author: Adam Pioterek <adam.pioterek@protonmail.ch>
timetable parses GTFS
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/.idea/dictionaries/adam.xml b/.idea/dictionaries/adam.xml new file mode 100644 index 0000000000000000000000000000000000000000..97ec6acaf972c3064bc9ec97c5fbf5d9c8dcb7c3 --- /dev/null +++ b/.idea/dictionaries/adam.xml @@ -0,0 +1,7 @@ +<component name="ProjectDictionaryState"> + <dictionary name="adam"> + <words> + <w>headsign</w> + </words> + </dictionary> +</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 635999df1e86791ad3787e455b4524e4d8879b93..ba7052b8197ddf8ba8756022d905d03055c7ad60 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ </value> </option> </component> - <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <output url="file://$PROJECT_DIR$/build/classes" /> </component> <component name="ProjectType"> diff --git a/app/build.gradle b/app/build.gradle index 8273a67e3346157dafd723c20b1a0e1df89cd6de..629485cf263b63aded4055ca0b69b2cd11c74f28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,14 +2,14 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" + compileSdkVersion 27 + buildToolsVersion "27.0.3" defaultConfig { - applicationId "ml.adamsprogs.bimba" + applicationId "ml.adamsprogs.bimba.dev" minSdkVersion 19 - targetSdkVersion 26 - versionCode 8 - versionName "1.2.2" + targetSdkVersion 27 + versionCode 9 + versionName "2.0.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } @@ -26,18 +26,17 @@ implementation fileTree(dir: 'libs', include: ['*.jar']) androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - implementation 'com.android.support:appcompat-v7:26.1.0' - implementation 'com.android.support:cardview-v7:26.1.0' - implementation 'com.android.support:design:26.1.0' - implementation 'com.android.support:support-vector-drawable:26.1.0' + implementation 'com.android.support:appcompat-v7:27.0.2' + implementation 'com.android.support:cardview-v7:27.0.2' + implementation 'com.android.support:design:27.0.2' + implementation 'com.android.support:support-vector-drawable:27.0.2' testImplementation 'junit:junit:4.12' implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'com.github.arimorty:floatingsearchview:2.1.1' - implementation 'org.tukaani:xz:1.6' implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.squareup.okhttp3:okhttp:3.8.1' - //implementation 'com.gu.android:toolargetool:0.1.3@aar' // TooLargeTool + implementation 'org.onebusaway:onebusaway-gtfs:1.3.4' } repositories { maven { diff --git a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt index 0a3c3a4c18ea63b371fd48371f9fd8dae21d9355..c560ea3e932514ad7de28b93dd8e2956b54a2e99 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt @@ -8,25 +8,21 @@ import android.content.Intent import android.support.v4.app.NotificationCompat import java.net.HttpURLConnection import java.net.URL -import org.tukaani.xz.XZInputStream import java.io.* import java.security.MessageDigest -import kotlin.experimental.and import android.app.NotificationManager import android.os.Build -import ml.adamsprogs.bimba.models.Timetable - +import android.util.Log +import java.util.* class TimetableDownloader : IntentService("TimetableDownloader") { companion object { - val ACTION_DOWNLOADED = "ml.adamsprogs.bimba.timetableDownloaded" - val EXTRA_FORCE = "force" - val EXTRA_RESULT = "result" - val RESULT_NO_CONNECTIVITY = "no connectivity" - val RESULT_VERSION_MISMATCH = "version mismatch" - val RESULT_UP_TO_DATE = "up-to-date" - val RESULT_DOWNLOADED = "downloaded" - val RESULT_VALIDITY_FAILED = "validity failed" + const val ACTION_DOWNLOADED = "ml.adamsprogs.bimba.timetableDownloaded" + const val EXTRA_FORCE = "force" + const val EXTRA_RESULT = "result" + const val RESULT_NO_CONNECTIVITY = "no connectivity" + const val RESULT_UP_TO_DATE = "up-to-date" + const val RESULT_DOWNLOADED = "downloaded" } private lateinit var notificationManager: NotificationManager @@ -41,52 +37,42 @@ if (!NetworkStateReceiver.isNetworkAvailable(this)) { sendResult(RESULT_NO_CONNECTIVITY) return } - val metadataUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.meta") - var httpCon = metadataUrl.openConnection() as HttpURLConnection + val url = URL("http://ztm.poznan.pl/pl/dla-deweloperow/getGTFSFile") + val httpCon = url.openConnection() as HttpURLConnection if (httpCon.responseCode != HttpURLConnection.HTTP_OK) { //IOEXCEPTION or EOFEXCEPTION or ConnectException sendResult(RESULT_NO_CONNECTIVITY) return } - val reader = BufferedReader(InputStreamReader(httpCon.inputStream)) - val lastModified = reader.readLine() - val checksum = reader.readLine() - size = Integer.parseInt(reader.readLine()) / 1024 - val dbVersion = reader.readLine() - if (Integer.parseInt(dbVersion.split(".")[0]) > Timetable.version) { - sendResult(RESULT_VERSION_MISMATCH) - return - } - val dbFilename = reader.readLine() + val lastModified = httpCon.getHeaderField("Content-Disposition").split("=")[1].trim('\"').split("_")[0] val currentLastModified = prefs.getString("timetableLastModified", "19791012") - if (lastModified <= currentLastModified && !intent.getBooleanExtra(EXTRA_FORCE, false)) { + if (lastModified <= currentLastModified && lastModified <= today()) { sendResult(RESULT_UP_TO_DATE) return } notify(0) - val xzDbUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/$dbFilename") - httpCon = xzDbUrl.openConnection() as HttpURLConnection - if (httpCon.responseCode != HttpURLConnection.HTTP_OK) { - sendResult(RESULT_NO_CONNECTIVITY) - return - } - val xzIn = XZInputStream(httpCon.inputStream) - val file = File(this.filesDir, "new_timetable.db") - if (copyInputStreamToFile(xzIn, file, checksum)) { - val oldFile = File(this.filesDir, "timetable.db") - oldFile.delete() - file.renameTo(oldFile) - val prefsEditor = prefs.edit() - prefsEditor.putString("timetableLastModified", lastModified) - prefsEditor.apply() - sendResult(RESULT_DOWNLOADED) - } else { - sendResult(RESULT_VALIDITY_FAILED) - } + val file = File(this.filesDir, "new_timetable.zip") + copyInputStreamToFile(httpCon.inputStream, file) + val oldFile = File(this.filesDir, "timetable.zip") + oldFile.delete() + file.renameTo(oldFile) + val prefsEditor = prefs.edit() + prefsEditor.putString("timetableLastModified", lastModified) + prefsEditor.apply() + sendResult(RESULT_DOWNLOADED) cancelNotification() } + } + + private fun today(): String { + val cal = Calendar.getInstance() + val d = cal[Calendar.DAY_OF_MONTH] + val m = cal[Calendar.MONTH]+1 + val y = cal[Calendar.YEAR] + + return "%d%02d%02d".format(y, m, d) } private fun sendResult(result: String) { @@ -133,9 +119,8 @@ private fun cancelNotification() { notificationManager.cancel(42) } - private fun copyInputStreamToFile(ins: InputStream, file: File, checksum: String): Boolean { + private fun copyInputStreamToFile(ins: InputStream, file: File) { val md = MessageDigest.getInstance("SHA-512") - var hex = "" try { val out = FileOutputStream(file) val buf = ByteArray(5 * 1024) @@ -155,11 +140,6 @@ } catch (e: Exception) { e.printStackTrace() } finally { ins.close() - val digest = md.digest() - for (i in 0 until digest.size) { - hex += Integer.toString((digest[i] and 0xff.toByte()) + 0x100, 16).padStart(3, '0').substring(1) - } - return checksum == hex } } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt index 2ac8dd863f1cfb250db50cc77115c67ff4e68247..8476cc8e26c86316784480433f0758e1a69faa40 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -73,8 +73,10 @@ drawerLayout.closeDrawer(drawerView) super.onOptionsItemSelected(item) } - val validity = timetable.getValidity() - drawerView.menu.findItem(R.id.drawer_validity).title = getString(R.string.valid_since, validity) + val validSince = timetable.getValidSince() + val validTill = timetable.getValidTill() + drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, validSince) + drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, validTill) searchView = search_view @@ -258,7 +260,6 @@ val message: String = when (result) { TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.timetable_downloaded) TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity) TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date) - TimetableDownloader.RESULT_VALIDITY_FAILED -> getString(R.string.validity_failed) else -> getString(R.string.error_try_later) } Snackbar.make(findViewById(R.id.drawer_layout), message, Snackbar.LENGTH_LONG).show() diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt index 930d22eb15b467361c5e3ece567387ec72726189..eca00f605d8c8d27681f58d2a5cfe964aa79fe7e 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -17,6 +17,7 @@ import ml.adamsprogs.bimba.models.* import ml.adamsprogs.bimba.* import kotlin.concurrent.thread import kotlinx.android.synthetic.main.activity_stop.* +import org.onebusaway.gtfs.model.AgencyAndId class StopActivity : AppCompatActivity(), MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener { companion object { @@ -29,7 +30,7 @@ val SOURCE_TYPE_STOP = "stop" val SOURCE_TYPE_FAV = "favourite" } - private var stopId: String? = null + private var stopId: AgencyAndId? = null private var stopSymbol: String? = null private var favourite: Favourite? = null private var timetableType = "departure" @@ -59,7 +60,7 @@ setSupportActionBar(toolbar) when (sourceType) { SOURCE_TYPE_STOP -> { - stopId = intent.getStringExtra(EXTRA_STOP_ID) + stopId = intent.getSerializableExtra(EXTRA_STOP_ID) as AgencyAndId stopSymbol = intent.getStringExtra(EXTRA_STOP_SYMBOL) supportActionBar?.title = timetable.getStopName(stopId!!) } @@ -133,7 +134,7 @@ fab.setOnClickListener { if (!favourites.has(stopSymbol!!)) { val items = HashSet<Plate>() - timetable.getLines(stopId!!).forEach { + timetable.getLinesForStop(stopId!!).forEach { val o = Plate(it, stopId!!, null) items.add(o) } @@ -246,23 +247,22 @@ } class PlaceholderFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - val rootView = inflater!!.inflate(R.layout.fragment_stop, container, false) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val rootView = inflater.inflate(R.layout.fragment_stop, container, false) val layoutManager = LinearLayoutManager(activity) val departuresList: RecyclerView = rootView.findViewById(R.id.departuresList) departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation)) - val departures = arguments.getStringArrayList("departures")?.map { Departure.fromString(it) } - departuresList.adapter = DeparturesAdapter(activity, departures, - arguments["relativeTime"] as Boolean) + val departures = arguments?.getStringArrayList("departures")?.map { Departure.fromString(it) } + departuresList.adapter = DeparturesAdapter(activity as Context, departures, + arguments?.get("relativeTime") as Boolean) departuresList.layoutManager = layoutManager return rootView } companion object { - private val ARG_SECTION_NUMBER = "section_number" + private const val ARG_SECTION_NUMBER = "section_number" fun newInstance(sectionNumber: Int, departures: ArrayList<Departure>?, relativeTime: Boolean): PlaceholderFragment { @@ -286,7 +286,7 @@ inner class SectionsPagerAdapter(fm: FragmentManager, var departures: HashMap>?) : FragmentStatePagerAdapter(fm) { var relativeTime = true - override fun getItemPosition(obj: Any?): Int { + override fun getItemPosition(obj: Any): Int { return PagerAdapter.POSITION_NONE } diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt index ade30923819f5a428acc3a22b80e1e9376e92f55..573413e9eb80eb743d273913ad8c1cee9361c056 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt @@ -9,4 +9,17 @@ Calendar.SUNDAY -> Timetable.MODE_SUNDAYS Calendar.SATURDAY -> Timetable.MODE_SATURDAYS else -> Timetable.MODE_WORKDAYS } +} + +internal fun String.toPascalCase(): String { //check + val builder = StringBuilder(this) + var isLastCharSeparator = true + builder.forEach { + isLastCharSeparator = if ((it in 'a'..'z' || it in 'A'..'Z') && isLastCharSeparator){ + it.toUpperCase() + false + } else + true + } + return builder.toString() } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt index 464217e69dc44aafd37b0cec470b1090cc281651..66f433fa96b1afa00711e5024935f7535f1b8933 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt @@ -1,16 +1,17 @@ package ml.adamsprogs.bimba.models +import org.onebusaway.gtfs.model.AgencyAndId import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap -data class Departure(val line: String, val mode: String, val time: String, val lowFloor: Boolean, - val modification: String?, val direction: String, val vm: Boolean = false, +data class Departure(val line: AgencyAndId, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight + val modification: List<String>, val direction: String, val vm: Boolean = false, var tomorrow: Boolean = false, val onStop: Boolean = false) { val isModified: Boolean get() { - return modification != null && modification != "" && modification != "null" + return modification.isNotEmpty() } override fun toString(): String { diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt index a2cc93ae6b51b9f6ac2e3e6cce371c2621f9fd3a..8aeaa6ccbd3fa18f982d2b58d851195af8d48ba7 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt @@ -18,8 +18,8 @@ override fun onBindViewHolder(holder: ViewHolder?, position: Int) { val timetable = Timetable.getTimetable() val favourites = FavouriteStorage.getFavouriteStorage() val plate = Plate(favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].line, - favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].stop, null) - val favouriteElement = timetable.getFavouriteElement(plate) + favourite.timetables.sortedBy { "${it.line}${it.stop}" }[position].stop, "",null) + val favouriteElement = "${timetable.getStopName(plate.stop)} ( ${timetable.getStopSymbol(plate.stop)}):\n${timetable.getLineNumber(plate.line)} → ${plate.headsign}" holder?.rowTextView?.text = favouriteElement holder?.splitButton?.setOnClickListener { favourites.detach(favourite.name, plate, favouriteElement) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt index b50d8b0bbd84bcf7f2a37c11a8978072f9f58a27..89476d0e4c136b34ca6a61b0a427cbe227d2aded 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt @@ -1,8 +1,10 @@ package ml.adamsprogs.bimba.models -data class Plate(val line: String, val stop: String, val departures: HashMap<String, HashSet<Departure>>?) { +import org.onebusaway.gtfs.model.AgencyAndId + +data class Plate(val line: AgencyAndId, val stop: AgencyAndId, val headsign: String, val departures: HashMap<AgencyAndId, HashSet<Departure>>?) { override fun toString(): String { - var result = "$line=$stop={" + var result = "$line=$stop=$headsign={" if (departures != null) { for ((_, column) in departures) for (departure in column) { @@ -15,9 +17,9 @@ } companion object { fun fromString(string: String): Plate { - val s = string.split("=") + val (line, stop, headsign, departuresString) = string.split("=") val departures = HashMap<String, HashSet<Departure>>() - s[2].replace("{", "").replace("}", "").split(";") + departuresString.replace("{", "").replace("}", "").split(";") .filter { it != "" } .forEach { try { @@ -28,10 +30,10 @@ departures[dep.mode]!!.add(dep) } catch (e: IllegalArgumentException) { } } - return Plate(s[0], s[1], departures) + return Plate(line, stop, headsign, departures) } - fun join(set: HashSet<Plate>): HashMap<String, ArrayList<Departure>> { + fun join(set: Set<Plate>): HashMap<String, ArrayList<Departure>> { val departures = HashMap<String, ArrayList<Departure>>() for (plate in set) { for ((mode, d) in plate.departures!!) { diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt index a9071c7c56ff94066c808e5421276882231dde0f..b31866db0372b70a2cc463c23da7e0bddfc8717b 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSuggestion.kt @@ -3,24 +3,24 @@ import android.os.Parcel import android.os.Parcelable import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion +import org.onebusaway.gtfs.model.AgencyAndId -class StopSuggestion(text: String, val id: String, val symbol: String) : SearchSuggestion { - private val body: String = text +class StopSuggestion(private val directions: HashSet<String>, val id: AgencyAndId) : SearchSuggestion { - constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString(), parcel.readString()) + @Suppress("UNCHECKED_CAST") + constructor(parcel: Parcel) : this(parcel.readSerializable() as HashSet<String>, parcel.readSerializable() as AgencyAndId) override fun describeContents(): Int { return Parcelable.CONTENTS_FILE_DESCRIPTOR } override fun writeToParcel(dest: Parcel?, flags: Int) { - dest?.writeString(body) - dest?.writeString(id) - dest?.writeString(symbol) + dest?.writeSerializable(directions) + dest?.writeSerializable(id) } override fun getBody(): String { - return body + return "${Timetable.getTimetable().getStopName(id)}\n${directions.sortedBy{it}.joinToString()}" } companion object CREATOR : Parcelable.Creator<StopSuggestion> { diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt index 7bec2461efcac9263d22dc3bd1dc5464c45ceac4..b2648495b1e06aae110ce0b24914aa3519437058 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt @@ -1,64 +1,51 @@ package ml.adamsprogs.bimba.models import android.content.Context -import android.database.CursorIndexOutOfBoundsException -import android.database.sqlite.SQLiteCantOpenDatabaseException -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteDatabaseCorruptException import ml.adamsprogs.bimba.CacheManager +import ml.adamsprogs.bimba.toPascalCase import java.io.File - +import org.onebusaway.gtfs.impl.GtfsDaoImpl +import org.onebusaway.gtfs.model.* +import org.onebusaway.gtfs.serialization.GtfsReader +import org.onebusaway.gtfs.services.GtfsDao +import java.util.* class Timetable private constructor() { companion object { - val version = 1 - val MODE_WORKDAYS = "workdays" - val MODE_SATURDAYS = "saturdays" - val MODE_SUNDAYS = "sundays" + const val MODE_WORKDAYS = "workdays" + const val MODE_SATURDAYS = "saturdays" + const val MODE_SUNDAYS = "sundays" private var timetable: Timetable? = null fun getTimetable(context: Context? = null, force: Boolean = false): Timetable { - if (timetable == null || force) + return if (timetable == null || force) if (context != null) { - val db: SQLiteDatabase? - try { - db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path, - null, SQLiteDatabase.OPEN_READONLY) - } catch (e: NoSuchFileException) { - throw SQLiteCantOpenDatabaseException("no such file") - } catch (e: SQLiteCantOpenDatabaseException) { - throw SQLiteCantOpenDatabaseException("cannot open db") - } catch (e: SQLiteDatabaseCorruptException) { - throw SQLiteCantOpenDatabaseException("db corrupt") - } timetable = Timetable() - timetable!!.db = db + timetable!!.store = read(context) timetable!!.cacheManager = CacheManager.getCacheManager(context) - return timetable as Timetable + timetable as Timetable } else throw IllegalArgumentException("new timetable requested and no context given") else - return timetable as Timetable + timetable as Timetable + } + + private fun read(context: Context): GtfsDao { + val reader = GtfsReader() + reader.setInputLocation(File(context.filesDir, "timetable.zip")) + val store = GtfsDaoImpl() + reader.entityStore = store + reader.run() + return store } } - lateinit var db: SQLiteDatabase + lateinit var store: GtfsDao private lateinit var cacheManager: CacheManager - private var _stops: ArrayList<StopSuggestion>? = null + private var _stops: ArrayList<StopSuggestion>? = null //todo stops to cache fun refresh(context: Context) { - val db: SQLiteDatabase? - try { - db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path, - null, SQLiteDatabase.OPEN_READONLY) - } catch (e: NoSuchFileException) { - throw SQLiteCantOpenDatabaseException("no such file") - } catch (e: SQLiteCantOpenDatabaseException) { - throw SQLiteCantOpenDatabaseException("cannot open db") - } catch (e: SQLiteDatabaseCorruptException) { - throw SQLiteCantOpenDatabaseException("db corrupt") - } - this.db = db + this.store = read(context) cacheManager.recreate(getStopDeparturesByPlates(cacheManager.keys().toSet())) @@ -69,50 +56,61 @@ fun getStops(force: Boolean = false): List { if (_stops != null && !force) return _stops!! - val stops = ArrayList<StopSuggestion>() - val cursor = db.rawQuery("select name ||char(10)|| headsigns as suggestion, id, stops.symbol || number as stopSymbol from stops" + - " join nodes on(stops.symbol = nodes.symbol) order by name, id;", null) - while (cursor.moveToNext()) - stops.add(StopSuggestion(cursor.getString(0), cursor.getString(1), cursor.getString(2))) - cursor?.close() - _stops = stops - return stops - } + /* + AWF + 232 → Os. Rusa|8:1435|AWF03 + AWF + 232 → Rondo Kaponiera|8:1436|AWF04 + AWF + 76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński|8:1437|AWF02 + AWF + 76 → Os. Dębina, 603 → Łęczyca/Dworcowa|8:1634|AWF01 + AWF + 29 → Pl. Wiosny Ludów|8:171|AWF42 + AWF + 10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody|8:172|AWF41 + AWF + 10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego|8:4586|AWF73 + */ - fun getStopName(stopId: String): String { - val cursor = db.rawQuery("select name from nodes join stops on(stops.symbol = nodes.symbol) where id = ?;", - listOf(stopId).toTypedArray()) - val name: String - cursor.moveToNext() - name = cursor.getString(0) - cursor.close() - return name + //trip_id, stop_id from stop_times if drop_off_type in {0,3} + //route_id as line, trip_id, headsign from trips + //stop_id, stop_code from stops + + val map = HashMap<AgencyAndId, HashSet<String>>() + + store.allStopTimes.filter { it.dropOffType == 0 || it.dropOffType == 3 }.forEach { + val trip = it.trip + val line = trip.route.shortName + val headsign = (trip.tripHeadsign).toPascalCase() + val stopId = it.stop.id + if (map[stopId] == null) + map[stopId] = HashSet() + map[stopId]!!.add("$line → $headsign") + } + + val stops = map.entries.map { StopSuggestion(it.value, it.key) }.toSet() + + + _stops = stops.sortedBy { this.getStopSymbol(it.id) } as ArrayList<StopSuggestion> + return _stops!! } - fun getStopSymbol(stopId: String): String { - val cursor = db.rawQuery("select symbol||number from stops where id = ?", listOf(stopId).toTypedArray()) - val symbol: String - cursor.moveToNext() - symbol = cursor.getString(0) - cursor.close() - return symbol - } + fun getStopName(stopId: AgencyAndId) = store.getStopForId(stopId).name!! + + fun getStopSymbol(stopId: AgencyAndId) = store.getStopForId(stopId).code!! - fun getLineNumber(lineId: String): String { - val cursor = db.rawQuery("select number from lines where id = ?", listOf(lineId).toTypedArray()) - val number: String - cursor.moveToNext() - number = cursor.getString(0) - cursor.close() - return number - } + fun getLineNumber(lineId: AgencyAndId) = store.getRouteForId(lineId).shortName!! - fun getStopDepartures(stopId: String): Map<String, List<Departure>> { + fun getStopDepartures(stopId: AgencyAndId): Map<String, List<Departure>> { val plates = HashSet<Plate>() val toGet = HashSet<Plate>() - getLinesForStop(stopId) - .map { Plate(it, stopId, null) } + getTripsForStop(stopId) + .map { + it.tripHeadsign + Plate(it.route.id, stopId, it.tripHeadsign, null) + } .forEach { if (cacheManager.has(it)) plates.add(cacheManager.get(it)!!) @@ -126,22 +124,6 @@ return Plate.join(plates) } - fun getStopDepartures(stopId: String, lineId: String): Map<String, List<Departure>> { - val plates = HashSet<Plate>() - val toGet = HashSet<Plate>() - - val plate = Plate(lineId, stopId, null) - if (cacheManager.has(plate)) - plates.add(cacheManager.get(plate)!!) - else { - toGet.add(plate) - } - - getStopDeparturesByPlates(toGet).forEach { cacheManager.push(it); plates.add(it) } - - return Plate.join(plates) - } - fun getStopDepartures(plates: Set<Plate>): Map<String, ArrayList<Departure>> { val result = HashSet<Plate>() val toGet = HashSet<Plate>() @@ -161,93 +143,87 @@ private fun getStopDeparturesByPlates(plates: Set<Plate>): Set<Plate> { if (plates.isEmpty()) return emptySet() - val result = HashMap<String, Plate>() - val condition = plates.joinToString(" or ") { "(stop_id = '${it.stop}' and line_id = '${it.line}')" } + return plates.map { getStopDeparturesByPlate(it) }.toSet() + } - val sql = "select " + - "lines.number, mode, substr('0'||hour, -2) || ':' || " + - "substr('0'||minute, -2) as time, lowFloor, modification, headsign, stop_id, line_id " + - "from " + - "departures join timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) " + - "where " + - condition + - "order by " + - "mode, time;" - val cursor = db.rawQuery(sql, null) + private fun getStopDeparturesByPlate(plate: Plate): Plate { + val p = Plate(plate.line, plate.stop, plate.headsign, HashMap()) + store.allStopTimes + .filter { it.stop.id == plate.stop } + .filter { it.trip.route.id == plate.line } + .filter { it.trip.tripHeadsign.toLowerCase() == plate.headsign.toLowerCase() } + .forEach { + val time = it.departureTime + val serviceId = it.trip.serviceId + val mode = calendarToMode(serviceId.id.toInt()) + val lowFloor = it.trip.wheelchairAccessible == 1 + val mod = explainModification(it.trip, it.trip.id.id.split("^")[1], it.stopSequence) + + val dep = Departure(plate.line, mode, time, lowFloor, mod, plate.headsign) + if (p.departures!![serviceId] == null) + p.departures[serviceId] = HashSet() + p.departures[serviceId]!!.add(dep) + } + return p + } - while (cursor.moveToNext()) { - val lineId = cursor.getString(7) - val stopId = cursor.getString(6) + private fun calendarToMode(serviceId: Int): List<Int> { + val calendar = store.getCalendarForId(serviceId) + val days = ArrayList<Int>() + if (calendar.monday == 1) days.add(0) + if (calendar.tuesday == 1) days.add(1) + if (calendar.wednesday == 1) days.add(2) + if (calendar.thursday == 1) days.add(3) + if (calendar.friday == 1) days.add(4) + if (calendar.saturday == 1) days.add(5) + if (calendar.sunday == 1) days.add(6) + return days + } - if (!result.containsKey("$lineId@$stopId")) { - result["$lineId@$stopId"] = Plate(lineId, stopId, HashMap()) - result["$lineId@$stopId"]?.departures?.put(MODE_WORKDAYS, HashSet()) - result["$lineId@$stopId"]?.departures?.put(MODE_SATURDAYS, HashSet()) - result["$lineId@$stopId"]?.departures?.put(MODE_SUNDAYS, HashSet()) - } + private fun explainModification(trip: Trip, modifications: String, stopSequence: Int): List<String> { + val mods = modifications.replace("+", "").split(",") + var definitions = trip.route.desc.split("^") + definitions = definitions.slice(2..definitions.size) - result["$lineId@$stopId"]?.departures?.get(cursor.getString(1))?.add( - Departure(cursor.getString(0), cursor.getString(1), - cursor.getString(2), cursor.getInt(3) == 1, - cursor.getString(4), cursor.getString(5))) + val definitionsMap = HashMap<String, String>() + + definitions.forEach { + val (key, definition) = it.split(" - ") + definitionsMap[key] = definition } - cursor.close() - return result.values.toSet() + + val explanations = ArrayList<String>() + + mods.forEach { + if (it.contains(":")) { + val (key, start, stop) = it.split(":") + if (stopSequence in start.toInt()..stop.toInt()) + explanations.add(definitionsMap[key]!!) + } else { + explanations.add(definitionsMap[it]!!) + } + } + + return explanations } - private fun getLinesForStop(stopId: String): Set<String> { - val cursor = db.rawQuery("select line_id from timetables where stop_id=?;", listOf(stopId).toTypedArray()) - val lines = HashSet<String>() - while (cursor.moveToNext()) - lines.add(cursor.getString(0)) - cursor.close() + fun getLinesForStop(stopId: AgencyAndId): Set<AgencyAndId> { + val lines = HashSet<AgencyAndId>() + store.allStopTimes.filter { it.stop.id == stopId }.forEach { lines.add(it.trip.route.id) } return lines } - fun getLines(stopId: String): List<String> { - val cursor = db.rawQuery(" select distinct line_id from timetables join " + - "stops on(stop_id = stops.id) where stops.id = ?;", - listOf(stopId).toTypedArray()) - val lines = ArrayList<String>() - while (cursor.moveToNext()) { - lines.add(cursor.getString(0)) - } - cursor.close() + private fun getTripsForStop(stopId: AgencyAndId): Set<Trip> { + val lines = HashSet<Trip>() + store.allStopTimes.filter { it.stop.id == stopId }.forEach { lines.add(it.trip) } return lines } - fun getFavouriteElement(plate: Plate): String { - val cursor = db.rawQuery("select name || ' (' || stops.symbol || stops.number || '): \n' " + - "|| lines.number || ' → ' || headsign from timetables join stops on (stops.id = stop_id) " + - "join lines on(lines.id = line_id) join nodes on(nodes.symbol = stops.symbol) where " + - "stop_id = ? and line_id = ?", - listOf(plate.stop, plate.line).toTypedArray()) - val element: String - cursor.moveToNext() - element = cursor.getString(0) - cursor.close() - return element - } + fun isEmpty() = store.allFeedInfos.isEmpty() - fun isEmpty(): Boolean { - val cursor = db.rawQuery("select * from metadata;", null) - try { - cursor.moveToNext() - cursor.getString(0) - cursor.close() - } catch (e: CursorIndexOutOfBoundsException) { - return true - } - return false - } + fun getValidSince() = store.allFeedInfos.toTypedArray()[0].startDate.asString!! - fun getValidity(): String { - val cursor = db.rawQuery("select value from metadata where key = 'validFrom'", null) - cursor.moveToNext() - val validity = cursor.getString(0) - cursor.close() - return "%s-%s-%s".format(validity.substring(0..3), validity.substring(4..5), validity.substring(6..7)) - } + fun getValidTill() = store.allFeedInfos.toTypedArray()[0].endDate.asString!! } diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png index 18369c5dfd306a3661c2d4ff99a632158e2895d7..11ef909fbfa1a99c4d00ec374e1a0a1c0365c4af 100644 Binary files a/app/src/main/res/drawable/logo.png and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml index 34ef96f91038985d8a1983e9d15b9f6b83bb497d..a1d293088df728c10d1eb40562e52de72ad04000 100644 --- a/app/src/main/res/menu/menu_drawer.xml +++ b/app/src/main/res/menu/menu_drawer.xml @@ -20,7 +20,10 @@ android:id="@+id/drawer_group_validity" android:enabled="false"> <item - android:id="@+id/drawer_validity" + android:id="@+id/drawer_validity_since" + android:title="" /> + <item + android:id="@+id/drawer_validity_till" android:title="" /> </group> </menu> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 177034ccde1941a01f5c80bd83c7619cc2dae5a9..cedb2477f8b47ae364493fe4b01f0d797d1bfa9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ <resources> - <string name="app_name" translatable="false">Bimba</string> + <string name="app_name" translatable="false">Bimba-dev</string> <string name="no_timetable">No timetable found. Connect to the Internet and wait for a timetable to be downloaded</string> <string name="timetable_downloaded">New timetable downloaded</string> <string name="title_activity_stop" translatable="false">StopActivity</string> @@ -67,7 +67,18 @@ "Be sure to consult the messages on\nhttps://www.ztm.poznan.pl/en.\n\n" </string> <string name="departure_row_getting_departures">Getting departures…</string> <string name="valid_since">Valid since %1$s</string> + <string name="valid_till">Valid till %1$s</string> <string name="departure_floor" translatable="false">departure floor type (lowFloor)</string> <string name="departure_info" translatable="false">departure info icon</string> <string name="refreshing_cache">Refreshing cache. May take some time…</string> + + <string-array name="daysOfWeek"> + <item>Monday</item> + <item>Tuesday</item> + <item>Wednesday</item> + <item>Thursday</item> + <item>Friday</item> + <item>Saturday</item> + <item>Sunday</item> + </string-array> </resources> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cabe97a591e5a5b49262af0aab80be4c0fc16d34..71678974fad1cc955d9f7fe97b6050b8bac97e69 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -37,6 +37,7 @@ Erfrischen <string name="help">Hilfe</string> <string name="title_activity_help">Hilfe</string> <string name="valid_since">Gilt seit %1$s</string> + <string name="valid_till">Gilt bis %1$s</string> <string name="departure_row_getting_departures">Abfahrten sammeln…</string> <string name="help_text"> "Warum gibt es keinen Fahrplan für Samstag?\n\n" diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c6ea5e9a9bb1921cd51d3773a5824738186fbce0..fbed0de352a859707c197c72559c73bd4e8e0b8c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -9,6 +9,7 @@Schermo principale <string name="help">Aiuto</string> <string name="title_activity_help">Aiuto</string> <string name="valid_since">Valido da %1$s</string> + <string name="valid_till">Valido a %1$s</string> <string name="no_timetable">Nessun orario è stato trovato. Connettiti a Internet e aspetta che un orario venga scaricato</string> <string name="timetable_downloaded">Nuovo orario è stato scaricato</string> <string name="tab_workday_text">Giorni di lavoro</string> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 327158c95ac287d69894bd806ee48caed60981a5..d846964b159c1c23bbe4786abd035c1c83e49642 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -57,5 +57,6 @@ "Pamiętaj, aby sprawdzić aktualności na\nhttps://www.ztm.poznan.pl.\n\n" </string> <string name="departure_row_getting_departures">Zbieranie odjazdów…</string> <string name="valid_since">Ważny od %1$s</string> + <string name="valid_till">Ważny do %1$s</string> <string name="refreshing_cache">Odświeżanie pamięci podręcznej. Może chwilę potrwać…</string> </resources> \ No newline at end of file