Author: Adam Pioterek <adam.pioterek@protonmail.ch>
adding favourites
%!v(PANIC=String method: strings: negative Repeat count)
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 cb15aee6ae6921f2590a7a42700164f5dde6ce70..648ec173b99655b90e453de672ced8c44999e08f 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -25,10 +25,14 @@ import ml.adamsprogs.bimba.datasources.VmClient import ml.adamsprogs.bimba.models.suggestions.GtfsSuggestion import ml.adamsprogs.bimba.models.suggestions.LineSuggestion import ml.adamsprogs.bimba.models.suggestions.StopSuggestion +import android.support.v7.widget.DefaultItemAnimator +import android.content.Intent //todo cards https://enoent.fr/blog/2015/01/18/recyclerview-basics/ +//todo searchView integration class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, - FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener { + FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener, + FavouritesAdapter.ViewHolder.OnClickListener { val context: Context = this private val receiver = MessageReceiver.getMessageReceiver() lateinit var timetable: Timetable @@ -38,6 +42,13 @@ private lateinit var drawerView: NavigationView lateinit var favouritesList: RecyclerView lateinit var searchView: FloatingSearchView private lateinit var favourites: FavouriteStorage + private lateinit var adapter: FavouritesAdapter + private val actionModeCallback = ActionModeCallback() + private var actionMode: ActionMode? = null + + companion object { + const val REQUEST_EDIT_FAVOURITE = 1 + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,7 +56,6 @@ setContentView(R.layout.activity_dash) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO) setSupportActionBar(toolbar) - supportActionBar?.title = getString(R.string.merge_favourites) getSuggestions() @@ -115,7 +125,7 @@ val intent = Intent(context, StopSpecifyActivity::class.java) intent.putExtra(StopSpecifyActivity.EXTRA_STOP_IDS, searchSuggestion.ids.joinToString(",") { it.id }) intent.putExtra(StopSpecifyActivity.EXTRA_STOP_NAME, searchSuggestion.name) startActivity(intent) - } else if (searchSuggestion is LineSuggestion){ + } else if (searchSuggestion is LineSuggestion) { val intent = Intent(context, LineSpecifyActivity::class.java) intent.putExtra(LineSpecifyActivity.EXTRA_LINE_ID, searchSuggestion.name) startActivity(intent) @@ -144,7 +154,9 @@ private fun prepareFavourites() { favourites = FavouriteStorage.getFavouriteStorage(context) val layoutManager = LinearLayoutManager(context) favouritesList = favourites_list - favouritesList.adapter = FavouritesAdapter(context, favourites.favouritesList, this) + adapter = FavouritesAdapter(context, favourites, this, this) + favouritesList.adapter = adapter + favouritesList.itemAnimator = DefaultItemAnimator() favouritesList.layoutManager = layoutManager } @@ -164,9 +176,7 @@ filter.addAction(VmClient.ACTION_READY) filter.addCategory(Intent.CATEGORY_DEFAULT) registerReceiver(receiver, filter) receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.favouritesList.forEach { - it.registerOnVm(receiver, context) - } + favourites.registerOnVm(receiver, context) } private fun startDownloaderService() { @@ -185,38 +195,17 @@ } override fun onResume() { super.onResume() - (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList + adapter.favourites = favourites favouritesList.adapter.notifyDataSetChanged() } override fun onDestroy() { super.onDestroy() receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.favouritesList.forEach { - it.deregisterOnVm(receiver, context) - } + favourites.deregisterOnVm(receiver, context) unregisterReceiver(receiver) } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_favourite_merge, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - - if (id == R.id.action_merge) { - val names = (favouritesList.adapter as FavouritesAdapter).selectedNames - favourites.merge(names) - (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList - favouritesList.adapter.notifyDataSetChanged() - (favouritesList.adapter as FavouritesAdapter).stopSelecting(names[0]) - } - - return super.onOptionsItemSelected(item) - } - fun deAccent(str: String): String { var result = str.replace('ę', 'e') result = result.replace('ó', 'o') @@ -248,18 +237,35 @@ } } override fun edit(name: String): Boolean { + val positionBefore = favourites.indexOf(name) val intent = Intent(this, EditFavouriteActivity::class.java) - intent.putExtra(EditFavouriteActivity.EXTRA_FAVOURITE, favourites.favourites[name]) - startActivity(intent) - (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList - favouritesList.adapter.notifyDataSetChanged() + intent.putExtra(EditFavouriteActivity.EXTRA_FAVOURITE, favourites[name]) + intent.putExtra(EditFavouriteActivity.EXTRA_POSITION_BEFORE, positionBefore) + startActivityForResult(intent, REQUEST_EDIT_FAVOURITE) return true } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + if (requestCode == REQUEST_EDIT_FAVOURITE) { + if (resultCode == Activity.RESULT_OK) { + val name = data.getStringExtra(EditFavouriteActivity.EXTRA_NEW_NAME) + val positionBefore = data.getIntExtra(EditFavouriteActivity.EXTRA_POSITION_BEFORE, -1) + //adapter.favourites = favourites.favouritesList + if (positionBefore == -1) + favouritesList.adapter.notifyDataSetChanged() + else { + val positionAfter = favourites.indexOf(name) + favouritesList.adapter.notifyItemChanged(positionBefore) + favouritesList.adapter.notifyItemMoved(positionBefore, positionAfter) + } + } + } + } + override fun delete(name: String): Boolean { favourites.delete(name) - (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList - favouritesList.adapter.notifyDataSetChanged() + //adapter.favourites = favourites.favouritesList + favouritesList.adapter.notifyItemRemoved(favourites.indexOf(name)) return true } @@ -267,5 +273,76 @@ @SuppressLint("MissingSuperCall") override fun onSaveInstanceState(outState: Bundle) { //hack below line to be commented to prevent crash on nougat. //super.onSaveInstanceState(outState); + } + + override fun onItemClicked(position: Int) { + if (actionMode != null) { + toggleSelection(position) + } + + //todo else -> StopActivity + } + + override fun onItemLongClicked(position: Int): Boolean { + if (actionMode == null) { + actionMode = startActionMode(actionModeCallback) + } + + toggleSelection(position) + + return true + } + + private fun toggleSelection(position: Int) { + adapter.toggleSelection(position) + val count = adapter.getSelectedItemCount() + + if (count == 0) { + actionMode?.finish() + } else { + actionMode?.title = getString(R.string.merge_favourites) + actionMode?.invalidate() + } + } + + private fun clearSelection() { + adapter.clearSelection() + actionMode?.finish() + } + + private inner class ActionModeCallback : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_favourite_merge, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_merge -> { + val selectedPositions = adapter.getSelectedItems() + val selectedNames = selectedPositions.map { favourites[it]?.name }.filter { it != null }.map { it!! } + favourites.merge(selectedNames) + + adapter.notifyItemChanged(selectedPositions.min()!!) + (1 until selectedPositions.size).forEach { + adapter.notifyItemRemoved(it) + } + + clearSelection() + true + } + + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode) { + (favouritesList.adapter as FavouritesAdapter).clearSelection() + actionMode = null + } } } 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 5a7c10bbf2e68fd18f4f29a9081d1e15bbc87a67..06d645b1b8e90386e934e7ed8b43289a5553cdf3 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt @@ -10,21 +10,29 @@ import ml.adamsprogs.bimba.models.Favourite import ml.adamsprogs.bimba.models.FavouriteEditRowAdapter import ml.adamsprogs.bimba.models.FavouriteStorage import kotlinx.android.synthetic.main.activity_edit_favourite.* +import android.app.Activity +import android.content.Intent + + class EditFavouriteActivity : AppCompatActivity() { companion object { const val EXTRA_FAVOURITE = "favourite" + const val EXTRA_POSITION_BEFORE = "position_before" + const val EXTRA_NEW_NAME = "new_name" } private lateinit var favourites: FavouriteStorage private lateinit var nameEdit: EditText private var favourite: Favourite? = null + private var positionBefore: Int? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_edit_favourite) favourite = intent.getParcelableExtra(EXTRA_FAVOURITE) + positionBefore = intent.getIntExtra(EXTRA_POSITION_BEFORE, -1) if (favourite == null) finish() favourites = FavouriteStorage.getFavouriteStorage(this) @@ -43,6 +51,10 @@ } override fun onBackPressed() { favourites.rename(favourite?.name!!, nameEdit.text.toString()) + val returnIntent = Intent() + returnIntent.putExtra(EXTRA_POSITION_BEFORE, positionBefore) + returnIntent.putExtra(EXTRA_NEW_NAME, nameEdit.text.toString()) + setResult(Activity.RESULT_OK, returnIntent) super.onBackPressed() } } 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 76890245ec5398ab355585494c2b088ec45fd84c..79ee257a0ec0231d16868e4e484c4eb4764a1e62 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -3,6 +3,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.icu.util.JapaneseCalendar import android.support.design.widget.TabLayout import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity @@ -140,11 +141,8 @@ } fab.setOnClickListener { if (!favourites.has(stopSymbol)) { - val items = HashSet<Plate>() - timetable.getTripsForStop(stopSegment!!.stop).values.forEach { - val o = Plate(Plate.ID(it.routeId, stopSegment!!.stop, it.headsign), null) - items.add(o) - } + val items = HashSet<StopSegment>() + items.add(stopSegment!!) favourites.add(stopSymbol, items) fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite, this.theme)) } else { @@ -256,8 +254,8 @@ if (departures == null) return PlaceholderFragment.newInstance(null, relativeTime) if (departures!!.isEmpty()) return PlaceholderFragment.newInstance(ArrayList(), relativeTime) - val sat = timetable.getServiceFor("saturday") - val sun = timetable.getServiceFor("sunday") + val sat = timetable.getServiceFor(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) + val sun = timetable.getServiceFor(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) val list: List<Departure> = when (position) { 1 -> departures!![sat] ?: ArrayList() 2 -> departures!![sun] ?: ArrayList() diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt index a6a5efe8c0ee0cb500afdd641bc0abccc13c3bc3..48ea70cbb6bfa857d14247788331f2775aac048c 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt @@ -38,7 +38,7 @@ } } private val requests = HashMap<AgencyAndId, Set<Request>>() private val vms = HashMap<AgencyAndId, HashSet<Plate>>() //HashSet<Departure>? - private val timetable = Timetable.getTimetable() + private val timetable = Timetable.getTimetable(this) override fun onCreate() { 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 c3eec8df843bb466a90f94a2a2662a69602ad8ca..1381ac50abf19a581df0055d375aaeeb96ee03de 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt @@ -16,13 +16,15 @@ class Favourite : Parcelable, MessageReceiver.OnVmListener { private var isRegisteredOnVmListener: Boolean = false var name: String private set - var timetables: HashSet<Plate> + var timetables: HashSet<StopSegment> private set private var vmDepartures = HashMap<Plate.ID, Set<Departure>>() val timetable = Timetable.getTimetable() val size - get() = this.timetables.size + get() = timetables.sumBy { + it.size + } private val onVmPreparedListeners = HashSet<OnVmPreparedListener>() @@ -35,15 +37,17 @@ onVmPreparedListeners.remove(listener) } constructor(parcel: Parcel) { - val array = ArrayList<String>() - parcel.readStringList(array) - val timetables = HashSet<Plate>() - array.mapTo(timetables) { Plate.fromString(it) } this.name = parcel.readString() - this.timetables = timetables + @Suppress("UNCHECKED_CAST") + val set = HashSet<StopSegment>() + val array = parcel.readParcelableArray(StopSegment::class.java.classLoader) + array.forEach { + set.add(it as StopSegment) + } + this.timetables = set } - constructor(name: String, timetables: HashSet<Plate>) { + constructor(name: String, timetables: HashSet<StopSegment>) { this.name = name this.timetables = timetables @@ -54,9 +58,9 @@ return Parcelable.CONTENTS_FILE_DESCRIPTOR } override fun writeToParcel(dest: Parcel?, flags: Int) { - val parcel = timetables.map { it.toString() } - dest?.writeStringList(parcel) dest?.writeString(name) + val parcelableSegments = timetables.map { it }.toTypedArray() + dest?.writeParcelableArray(parcelableSegments, flags) } private fun filterVmDepartures() { @@ -67,8 +71,10 @@ this.vmDepartures[it.key] = newSet } } - fun delete(plate: Plate) { - timetables.remove(timetables.find { it.id == plate.id }) + fun delete(plateId: Plate.ID) { + timetables.forEach { + it.remove(plateId) + } } fun registerOnVm(receiver: MessageReceiver, context: Context) { @@ -76,16 +82,10 @@ if (!isRegisteredOnVmListener) { receiver.addOnVmListener(this) isRegisteredOnVmListener = true - val segments = HashMap<AgencyAndId, StopSegment>() - timetables.forEach { - if (segments[it.id.stop] == null) - segments[it.id.stop] = StopSegment(it.id.stop, HashSet()) - segments[it.id.stop]!!.plates = segments[it.id.stop]!!.plates!!.plus(it.id) - } - segments.forEach { + timetables.forEach { val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it.value) + intent.putExtra("stop", it) intent.action = "request" context.startService(intent) } @@ -97,16 +97,9 @@ if (isRegisteredOnVmListener) { receiver.removeOnVmListener(this) isRegisteredOnVmListener = false - val segments = HashMap<AgencyAndId, StopSegment>() timetables.forEach { - if (segments[it.id.stop] == null) - segments[it.id.stop] = StopSegment(it.id.stop, HashSet()) - segments[it.id.stop]!!.plates = segments[it.id.stop]!!.plates!!.plus(it.id) - } - - segments.forEach { val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it.value) + intent.putExtra("stop", it) intent.action = "remove" context.startService(intent) } @@ -153,14 +146,22 @@ private fun nowDepartures(): ArrayList{ val today = timetable.getServiceForToday() val tomorrowCal = Calendar.getInstance() tomorrowCal.add(Calendar.DAY_OF_MONTH, 1) - val tomorrow = timetable.getServiceForTomorrow() + val tomorrow = try { + timetable.getServiceForTomorrow() + } catch (e: IllegalArgumentException) { + -1 + } + + val departures = fullTimetable() - val departures = timetable.getStopDepartures(timetables) + println(departures.keys.joinToString(",")) val todayDepartures = departures[today]!! val tomorrowDepartures = ArrayList<Departure>() val twoDayDepartures = ArrayList<Departure>() - departures[tomorrow]!!.mapTo(tomorrowDepartures) { it.copy() } - tomorrowDepartures.forEach { it.tomorrow = true } + if (tomorrow != -1) { + departures[tomorrow]!!.mapTo(tomorrowDepartures) { it.copy() } + tomorrowDepartures.forEach { it.tomorrow = true } + } todayDepartures.forEach { twoDayDepartures.add(it) } tomorrowDepartures.forEach { twoDayDepartures.add(it) } @@ -175,16 +176,25 @@ departures[today] = vmDepartures.flatMap { it.value } as ArrayList return departures } - val departures = timetable.getStopDepartures(timetables) + val departures = fullTimetable() return Departure.rollDepartures(departures) } fun fullTimetable(): Map<AgencyAndId, List<Departure>> { - return timetable.getStopDepartures(timetables) + val departureSet = HashSet<Map<AgencyAndId, List<Departure>>>() + timetables.forEach { departureSet.add(timetable.getStopDeparturesBySegment(it)) } + val departures = HashMap<AgencyAndId, List<Departure>>() + departureSet.forEach { + val map = it + it.keys.forEach { + departures[it] = (departures[it] ?: ArrayList()) + (map[it] ?: ArrayList()) + } + } + return departures } override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) { - if (timetables.any { it.id == plateId }) { + if (timetables.any { it.contains(plateId) }) { if (vmDepartures == null) this.vmDepartures.remove(plateId) else 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 0bdfe6fac3c60ab416514ab4e8c3482cf3cbddd0..841aa4e21c7a3ee97675f16aafab2db90aadb447 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt @@ -17,17 +17,17 @@ override fun onBindViewHolder(holder: ViewHolder?, position: Int) { val timetable = Timetable.getTimetable() val favourites = FavouriteStorage.getFavouriteStorage() - val id = favourite.timetables.sortedBy { "${it.id.line}${it.id.stop}" }[position].id + val id = favourite.timetables.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position] val plate = Plate(id,null) val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${timetable.getLineNumber(plate.id.line)} → ${plate.id.headsign}" holder?.rowTextView?.text = favouriteElement - holder?.splitButton?.setOnClickListener { - favourites.detach(favourite.name, plate, favouriteElement) - favourite = favourites.favourites[favourite.name]!! - notifyDataSetChanged() - } +// holder?.splitButton?.setOnClickListener { +// favourites.detach(favourite.name, id, favouriteElement) +// favourite = favourites.favourites[favourite.name]!! +// notifyDataSetChanged() +// } holder?.deleteButton?.setOnClickListener { - favourites.delete(favourite.name, plate) + favourites.delete(favourite.name, id) favourite = favourites.favourites[favourite.name]!! notifyDataSetChanged() } @@ -43,7 +43,7 @@ } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row) - val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split) +// val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split) val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete) } } \ No newline at end of file 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 f2df3265ab1547b79f57a69f691cca4c137254bf..fc3f1e7a09584272032fc4d917b7b6f7eb362d33 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt @@ -4,7 +4,9 @@ import android.content.Context import android.content.SharedPreferences import com.google.gson.Gson import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonObject +import ml.adamsprogs.bimba.MessageReceiver import ml.adamsprogs.bimba.models.gtfs.AgencyAndId @@ -26,20 +28,22 @@ } val favourites = HashMap<String, Favourite>() private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE) - val favouritesList: List<Favourite> - get() { - return favourites.values.toList().sortedBy { it.name } - } init { val favouritesString = preferences.getString("favourites", "{}") val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) for ((name, jsonTimetables) in favouritesMap.entrySet()) { - val timetables = HashSet<Plate>() + val timetables = HashSet<StopSegment>() jsonTimetables.asJsonArray.mapTo(timetables) { - Plate(Plate.ID(AgencyAndId.convertFromString(it.asJsonObject["line"].asString), - AgencyAndId.convertFromString(it.asJsonObject["stop"].asString), - it.asJsonObject["headsign"].asString), null) + val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null) + val plates = HashSet<Plate.ID>() + it.asJsonObject["plates"].asJsonArray.mapTo(plates) { + Plate.ID(AgencyAndId(it.asJsonObject["line"].asString), + AgencyAndId(it.asJsonObject["stop"].asString), + it.asJsonObject["headsign"].asString) + } + stopSegment.plates = plates + stopSegment } favourites[name] = Favourite(name, timetables) } @@ -49,7 +53,7 @@ override fun iterator(): Iterator = favourites.values.iterator() fun has(name: String): Boolean = favourites.contains(name) - fun add(name: String, timetables: HashSet<Plate>) { + fun add(name: String, timetables: HashSet<StopSegment>) { if (favourites[name] == null) { favourites[name] = Favourite(name, timetables) serialize() @@ -68,7 +72,7 @@ favourites.remove(name) serialize() } - fun delete(name: String, plate: Plate) { + fun delete(name: String, plate: Plate.ID) { favourites[name]?.delete(plate) serialize() } @@ -78,11 +82,18 @@ val rootObject = JsonObject() for ((name, favourite) in favourites) { val timetables = JsonArray() for (timetable in favourite.timetables) { - val element = JsonObject() - element.addProperty("stop", timetable.id.stop.toString()) - element.addProperty("line", timetable.id.line.toString()) - element.addProperty("headsign", timetable.id.headsign) - timetables.add(element) + val segment = JsonObject() + segment.addProperty("stop", timetable.stop.id) + val plates = JsonArray() + for (plate in timetable.plates ?: HashSet()) { + val element = JsonObject() + element.addProperty("stop", plate.stop.id) + element.addProperty("line", plate.line.id) + element.addProperty("headsign", plate.headsign) + plates.add(element) + } + segment.add("plates", plates) + timetables.add(segment) } rootObject.add(name, timetables) } @@ -93,16 +104,18 @@ editor.apply() } - fun detach(name: String, plate: Plate, newName: String) { - val array = HashSet<Plate>() - array.add(plate) - favourites[newName] = Favourite(newName, array) + fun detach(name: String, plate: Plate.ID, newName: String) { + val plates = HashSet<Plate.ID>() + plates.add(plate) + val segments = HashSet<StopSegment>() + segments.add(StopSegment(plate.stop, plates)) + favourites[newName] = Favourite(newName, segments) serialize() delete(name, plate) } - fun merge(names: ArrayList<String>) { + fun merge(names: List<String>) { if (names.size < 2) return val newFavourite = Favourite(names[0], HashSet()) @@ -122,4 +135,32 @@ favourites.remove(oldName) favourites[newName] = favourite serialize() } + + fun registerOnVm(receiver: MessageReceiver, context: Context) { + favourites.values.forEach { + it.registerOnVm(receiver, context) + } + } + + fun deregisterOnVm(receiver: MessageReceiver, context: Context) { + favourites.values.forEach { + it.deregisterOnVm(receiver, context) + } + } + + operator fun get(name: String): Favourite? { + return favourites[name] + } + + operator fun get(position: Int): Favourite? { + return favourites.entries.sortedBy { it.key }[position].value + } + + fun indexOf(name: String): Int { + val favourite = favourites[name] + return favourites.values.sortedBy { it.name }.indexOf(favourite) + } + + val size + get() = favourites.size } \ No newline at end of file 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 9ebfe9ef032d96f9637965f1e344239cae16643d..f9b95488b5e02ea346ef58f9646dbaf09101c512 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt @@ -2,56 +2,60 @@ package ml.adamsprogs.bimba.models import android.app.Activity import android.content.Context -import android.content.Intent -import android.support.design.widget.AppBarLayout import android.support.v4.content.res.ResourcesCompat import android.support.v7.widget.* import android.support.v7.widget.PopupMenu +import android.util.SparseBooleanArray import android.view.* import android.widget.* import ml.adamsprogs.bimba.R import android.view.LayoutInflater import java.util.* import kotlin.concurrent.thread -import android.util.TypedValue -import com.arlib.floatingsearchview.FloatingSearchView import ml.adamsprogs.bimba.Declinator -import ml.adamsprogs.bimba.activities.StopActivity -import ml.adamsprogs.bimba.getColour -import kotlin.collections.ArrayList + -class FavouritesAdapter(val context: Context, var favourites: List<Favourite>, private val onMenuItemClickListener: FavouritesAdapter.OnMenuItemClickListener) : +class FavouritesAdapter(val context: Context, var favourites: FavouriteStorage, + private val onMenuItemClickListener: OnMenuItemClickListener, + private val onClickListener: ViewHolder.OnClickListener) : RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() { - private val isSelecting: Boolean - get() { - return selected.any { it } + private val selectedItems = SparseBooleanArray() + + fun isSelected(position: Int) = getSelectedItems().contains(position) + + fun toggleSelection(position: Int) { + if (selectedItems.get(position, false)) { + selectedItems.delete(position) + } else { + selectedItems.put(position, true) } - private val selected = ArrayList<Boolean>() - val selectedNames: ArrayList<String> - get() { - val l = ArrayList<String>() - for ((i, it) in selected.withIndex()) { - if (it) - l.add(favourites[i].name) - } - return l - } + notifyItemChanged(position) + } - init { - favourites.forEach { - selected.add(false) + fun clearSelection() { + val selection = getSelectedItems() + selectedItems.clear() + for (i in selection) { + notifyItemChanged(i) } } - override fun getItemCount(): Int { - return favourites.size + fun getSelectedItemCount() = selectedItems.size() + + fun getSelectedItems(): List<Int> { + val items = ArrayList<Int>(selectedItems.size()) + (0 until selectedItems.size()).mapTo(items) { selectedItems.keyAt(it) } + return items } + override fun getItemCount() = favourites.size + override fun onBindViewHolder(holder: ViewHolder?, position: Int) { + holder?.selectedOverlay?.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE thread { - val favourite = favourites[position] + val favourite = favourites[position]!! val nextDeparture: Departure? try { nextDeparture = favourite.nextDeparture() @@ -71,27 +75,16 @@ nextDepartureText = context.getString(R.string.no_next_departure) nextDepartureLineText = "" } (context as Activity).runOnUiThread { - holder?.root?.setOnLongClickListener { - toggleSelected(it as CardView, position) - true - } - holder?.root?.setOnClickListener { - val intent = Intent(context, StopActivity::class.java) - intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_FAV) - intent.putExtra(StopActivity.EXTRA_FAVOURITE, favourite) - context.startActivity(intent) - } holder?.nameTextView?.text = favourite.name holder?.timeTextView?.text = nextDepartureText holder?.lineTextView?.text = nextDepartureLineText - if(nextDeparture!=null) { + if (nextDeparture != null) { if (nextDeparture.vm) holder?.typeIcon?.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_vm, context.theme)) else holder?.typeIcon?.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme)) } holder?.moreButton?.setOnClickListener { - unSelect(holder.root, position) val popup = PopupMenu(context, it) val inflater = popup.menuInflater popup.setOnMenuItemClickListener { @@ -108,74 +101,39 @@ } } } - private fun toggleSelected(view: CardView, position: Int) { - growSelected(position) - - if (selected[position]) - unSelect(view, position) - else - select(view, position) - } + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { + val context = parent?.context + val inflater = LayoutInflater.from(context) - private fun growSelected(position: Int) { - while (position >= selected.size) - selected.add(false) + val rowView = inflater.inflate(R.layout.row_favourite, parent, false) + return ViewHolder(rowView, onClickListener) } - private fun select(view: CardView, position: Int) { - growSelected(position) + class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { + override fun onLongClick(v: View?): Boolean { + return listener.onItemLongClicked(adapterPosition) + } - view.setCardBackgroundColor(getColour(R.color.colorAccent, context)) - selected[position] = true - setSelecting() - } + override fun onClick(v: View?) { + listener.onItemClicked(adapterPosition) + } - private fun unSelect(view: CardView, position: Int) { - growSelected(position) + val selectedOverlay: View = itemView.findViewById(R.id.selected_overlay) + val nameTextView: TextView = itemView.findViewById(R.id.favourite_name) + val timeTextView: TextView = itemView.findViewById(R.id.favourite_time) + val lineTextView: TextView = itemView.findViewById(R.id.favourite_line) + val moreButton: ImageView = itemView.findViewById(R.id.favourite_more_button) + val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon) - val colour = TypedValue() - context.theme.resolveAttribute(R.attr.cardBackgroundColor, colour, true) - view.setCardBackgroundColor(colour.data) - selected[position] = false - setSelecting() - } - - private fun setSelecting() { - context as Activity - if (isSelecting) { - context.findViewById<FloatingSearchView>(R.id.search_view).visibility = View.INVISIBLE - context.findViewById<AppBarLayout>(R.id.appbar).visibility = View.VISIBLE - } else { - context.findViewById<FloatingSearchView>(R.id.search_view).visibility = View.VISIBLE - context.findViewById<AppBarLayout>(R.id.appbar).visibility = View.INVISIBLE + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) } - } - - 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) - return ViewHolder(rowView) - } - fun stopSelecting(name: String) { - selected.clear() - favourites.forEach { - if (it.name == name) - selected.add(true) - else - selected.add(false) + interface OnClickListener { + fun onItemClicked(position: Int) + fun onItemLongClicked(position: Int): Boolean } - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val root:CardView = itemView.findViewById(R.id.favourite_card) - val nameTextView:TextView = itemView.findViewById(R.id.favourite_name) - val timeTextView:TextView = itemView.findViewById(R.id.favourite_time) - val lineTextView:TextView = itemView.findViewById(R.id.favourite_line) - val moreButton:ImageView = itemView.findViewById(R.id.favourite_more_button) - val typeIcon:ImageView = itemView.findViewById(R.id.departureTypeIcon) } interface OnMenuItemClickListener { diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt index c982aa67348bc8e1eab7f62988ea1eba4e1f2088..2448eecc8384ea7219b5ff3b990dd3351c9f5d1f 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt @@ -61,4 +61,11 @@ if (plates == null) return false return plates!!.contains(plateId) } + + fun remove(plateId: Plate.ID) { + (plates as HashSet).remove(plateId) + } + + val size: Int + get() = plates?.size ?: 0 } \ 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 954d66484500ef5cf339dffa8f35cdd02d24df41..bb6cee18a3fd8ac8df8975091e80741b91692334 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt @@ -126,11 +126,12 @@ } } } - val stopsFile = File(filesDir, "gtfs_files/trips.txt") - parser.parseAll(stopsFile).forEach { - tripsCache[it[2]] = it - routes[it[2]] = Pair(it[0], it[3]) + if (tripsCache.isEmpty()) + createTripCache() + tripsCache.forEach { + routes[it.key] = Pair(it.value[0], it.value[3]) } + trips.forEach { val headsign = HashSet<String>() @@ -152,6 +153,17 @@ 4586 -> (AWF73, {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego}) */ } + private fun createTripCache() { + val settings = CsvParserSettings() + settings.format.setLineSeparator("\r\n") + settings.format.quote = '"' + val parser = CsvParser(settings) + val stopsFile = File(filesDir, "gtfs_files/trips.txt") + parser.parseAll(stopsFile).forEach { + tripsCache[it[2]] = it + } + } + fun getStopName(stopId: AgencyAndId): String { val file = File(filesDir, "gtfs_files/stops.txt") val mapReader = CsvMapReader(FileReader(file), CsvPreference.STANDARD_PREFERENCE) @@ -211,6 +223,8 @@ segment.fillPlates() return getStopDeparturesBySegment(segment, trips) } + fun getStopDeparturesBySegment(segment: StopSegment) = getStopDeparturesBySegment(segment, getTripsForStop(segment.stop)) + private fun getStopDeparturesBySegment(segment: StopSegment, trips: Map<String, Trip>): HashMap<AgencyAndId, List<Departure>> { println("getStopDeparturesBySegment: ${JCalendar.getInstance().timeInMillis}") val departures = HashMap<AgencyAndId, ArrayList<Departure>>() @@ -249,55 +263,41 @@ println(">: ${JCalendar.getInstance().timeInMillis}") return sortedDepartures } - fun getStopDepartures(plates: Set<Plate>): Map<AgencyAndId, ArrayList<Departure>> { - val trips = HashMap<String, Trip>() - - tripsCache.forEach { trips[it.key] = tripFromCache(it.key) } - return Plate.join(getStopDeparturesByPlates(plates, trips)) - } - - private fun getStopDeparturesByPlates(plates: Set<Plate>, trips: Map<String, Trip>): Set<Plate> { - if (plates.isEmpty()) - return emptySet() - - return plates.map { getStopDeparturesByPlate(it, trips) }.toSet() - } - - private fun getStopDeparturesByPlate(plate: Plate, trips: Map<String, Trip>): Plate { //fixme<c:optimisation> takes too long --- cache - println("getStopDeparturesByPlate: ${JCalendar.getInstance().timeInMillis}") - val resultPlate = Plate(Plate.ID(plate.id), HashMap()) - val stopTimes = HashMap<String, Map<String, Any>>() - val stopTimesFile = File(filesDir, "gtfs_files/stop_times_${plate.id.stop.id}.txt") - val mapReader = CsvMapReader(FileReader(stopTimesFile), CsvPreference.STANDARD_PREFERENCE) - val header = mapReader.getHeader(true) - - var stopTimesRow: Map<String, Any>? = null - val processors = Array<CellProcessor?>(header.size, { null }) - while ({ stopTimesRow = mapReader.read(header, processors); stopTimesRow }() != null) { - val tripId = stopTimesRow!!["trip_id"] as String - stopTimes[tripId] = stopTimesRow!! - } - mapReader.close() - - trips.forEach { - if (it.value.routeId == plate.id.line && - it.value.headsign == plate.id.headsign) { - val time = parseTime(stopTimes[it.key]!!["departure_time"] as String) - val serviceId = it.value.serviceId - val mode = calendarToMode(serviceId) - val lowFloor = it.value.wheelchairAccessible - val mod = explainModification(it.value, - Integer.parseInt(stopTimes[it.key]!!["stop_sequence"] as String)) - - val dep = Departure(plate.id.line, mode, time, lowFloor, mod, plate.id.headsign) - if (resultPlate.departures!![serviceId] == null) - resultPlate.departures[serviceId] = HashSet() - resultPlate.departures[serviceId]!!.add(dep) - } - } - println("</>: ${JCalendar.getInstance().timeInMillis}") - return resultPlate - } +// private fun getStopDeparturesByPlate(plate: Plate, trips: Map<String, Trip>): Plate { //fixme<c:optimisation> takes too long --- cache +// println("getStopDeparturesByPlate: ${JCalendar.getInstance().timeInMillis}") +// val resultPlate = Plate(Plate.ID(plate.id), HashMap()) +// val stopTimes = HashMap<String, Map<String, Any>>() +// val stopTimesFile = File(filesDir, "gtfs_files/stop_times_${plate.id.stop.id}.txt") +// val mapReader = CsvMapReader(FileReader(stopTimesFile), CsvPreference.STANDARD_PREFERENCE) +// val header = mapReader.getHeader(true) +// +// var stopTimesRow: Map<String, Any>? = null +// val processors = Array<CellProcessor?>(header.size, { null }) +// while ({ stopTimesRow = mapReader.read(header, processors); stopTimesRow }() != null) { +// val tripId = stopTimesRow!!["trip_id"] as String +// stopTimes[tripId] = stopTimesRow!! +// } +// mapReader.close() +// +// trips.forEach { +// if (it.value.routeId == plate.id.line && +// it.value.headsign == plate.id.headsign) { +// val time = parseTime(stopTimes[it.key]!!["departure_time"] as String) +// val serviceId = it.value.serviceId +// val mode = calendarToMode(serviceId) +// val lowFloor = it.value.wheelchairAccessible +// val mod = explainModification(it.value, +// Integer.parseInt(stopTimes[it.key]!!["stop_sequence"] as String)) +// +// val dep = Departure(plate.id.line, mode, time, lowFloor, mod, plate.id.headsign) +// if (resultPlate.departures!![serviceId] == null) +// resultPlate.departures[serviceId] = HashSet() +// resultPlate.departures[serviceId]!!.add(dep) +// } +// } +// println("</>: ${JCalendar.getInstance().timeInMillis}") +// return resultPlate +// } private fun parseTime(time: String): Int { val cal = JCalendar.getInstance() @@ -355,6 +355,8 @@ return explanations } private fun getRouteForTrip(trip: Trip): Route { + if (tripsCache.isEmpty()) + createTripCache() val routeId = tripsCache[trip.id.rawId]!![0] val tripsFile = File(filesDir, "gtfs_files/routes.txt") @@ -437,6 +439,8 @@ return filteredTrips } private fun tripFromCache(id: String): Trip { + if (tripsCache.isEmpty()) + createTripCache() return Trip(AgencyAndId(tripsCache[id]!![0]), AgencyAndId(tripsCache[id]!![1]), createTripId(tripsCache[id]!![2]), tripsCache[id]!![3], Integer.parseInt(tripsCache[id]!![4]), @@ -511,26 +515,21 @@ val tomorrowDoW = tomorrow.get(JCalendar.DAY_OF_WEEK) return getServiceFor(tomorrowDoW) } - private fun getServiceFor(day: Int): AgencyAndId { - val dow = arrayOf("sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday") - - return getServiceFor(dow[day - 1]) - } - - fun getServiceFor(day: String): AgencyAndId { + fun getServiceFor(day: Int): AgencyAndId { + val dayColumn = ((day + 5) % 7) + 1 val file = File(filesDir, "gtfs_files/calendar.txt") - val mapReader = CsvMapReader(FileReader(file), CsvPreference.STANDARD_PREFERENCE) - val header = mapReader.getHeader(true) - var row: Map<String, Any>? = null - val processors = Array<CellProcessor?>(header.size, { null }) - while ({ row = mapReader.read(header, processors); row }() != null) { - if ((row!![day] as String) == "1") { - mapReader.close() - return AgencyAndId(row!!["service_id"] as String) + val settings = CsvParserSettings() + settings.format.quote = '"' + settings.format.setLineSeparator("\r\n") + settings.isHeaderExtractionEnabled = true + val parser = CsvParser(settings) + + parser.parseAll(file).forEach { + if ((it[dayColumn] as String) == "1") { + return AgencyAndId(it[0] as String) } } - mapReader.close() throw IllegalArgumentException("Day $day not in calendar") } @@ -575,6 +574,8 @@ return filteredPlates } fun getTripGraphs(id: AgencyAndId): List<Map<Int, List<Int>>> { + if(tripsCache.isEmpty()) + createTripCache() tripsCache.forEach { if (it.value[0] == id.id) { //todo needs stop_times.txt indexed by trips diff --git a/app/src/main/res/layout/row_favourite.xml b/app/src/main/res/layout/row_favourite.xml index 76733ef1eb085c419ae1f120e5d09cc15585cd62..509a5cbe3111eb1ad5746fc948150efcb30b0bb9 100644 --- a/app/src/main/res/layout/row_favourite.xml +++ b/app/src/main/res/layout/row_favourite.xml @@ -6,8 +6,16 @@ android:id="@+id/favourite_card" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="4dp" + android:foreground="?android:attr/selectableItemBackground" tools:layout_editor_absoluteX="8dp" tools:layout_editor_absoluteY="128dp"> + + <View + android:id="@+id/selected_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/colorAccent" + android:visibility="invisible" /> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" @@ -73,4 +81,5 @@ 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/layout/row_favourite_edit.xml b/app/src/main/res/layout/row_favourite_edit.xml index 34d681af9dedd9f4c0ab42612d3ac9416b971d53..7de8a6a53aa15b716770b036dba2906bd855bfe1 100644 --- a/app/src/main/res/layout/row_favourite_edit.xml +++ b/app/src/main/res/layout/row_favourite_edit.xml @@ -19,7 +19,7 @@ android:text="" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.AppCompat" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/favourite_edit_split" + app:layout_constraintEnd_toStartOf="@+id/favourite_edit_delete" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -31,25 +31,25 @@ android:layout_marginBottom="16dp" android:layout_marginStart="9dp" android:layout_marginTop="16dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@+id/favourite_edit_split" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_delete" tools:layout_editor_absoluteX="335dp" tools:layout_editor_absoluteY="16dp" android:contentDescription="@string/favourite_element_delete_button" /> - <ImageView - android:id="@+id/favourite_edit_split" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="16dp" - android:layout_marginEnd="58dp" - android:layout_marginTop="16dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_split" - tools:layout_editor_absoluteX="302dp" - tools:layout_editor_absoluteY="16dp" - android:contentDescription="@string/favourite_element_split_button" /> + <!--<ImageView--> + <!--android:id="@+id/favourite_edit_split"--> + <!--android:layout_width="wrap_content"--> + <!--android:layout_height="wrap_content"--> + <!--android:layout_marginBottom="16dp"--> + <!--android:layout_marginEnd="58dp"--> + <!--android:layout_marginTop="16dp"--> + <!--app:layout_constraintBottom_toBottomOf="parent"--> + <!--app:layout_constraintEnd_toEndOf="parent"--> + <!--app:layout_constraintTop_toTopOf="parent"--> + <!--app:srcCompat="@drawable/ic_split"--> + <!--tools:layout_editor_absoluteX="302dp"--> + <!--tools:layout_editor_absoluteY="16dp"--> + <!--android:contentDescription="@string/favourite_element_split_button" />--> </android.support.constraint.ConstraintLayout> \ No newline at end of file