Author: Adam <git@apiote.xyz>
search by position and related concepts
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt index b06ba0824486a44e7fa53987d5a7a55a6bc0b50d..6acbe27969f244d5917f458f5f24867189b522df 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt @@ -7,8 +7,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.google.android.material.chip.Chip import com.google.android.material.textfield.TextInputEditText import xyz.apiote.bimba.czwek.repo.Place +import xyz.apiote.bimba.czwek.search.Query class DashboardViewModel : ViewModel() { companion object { @@ -35,9 +37,23 @@ fun set(source: String, place: Place) { mutableData[source]!!.value = place } + fun unset(source: String) { + mutableData[source]!!.value = null + } + val spans = mutableMapOf( ORIGIN_KEY to "", DEST_KEY to "" + ) + + val positionChips = mutableMapOf<String, Chip?>( + ORIGIN_KEY to null, + DEST_KEY to null + ) + + val positionQueries = mutableMapOf<String, Query?>( + ORIGIN_KEY to null, + DEST_KEY to null ) val textInputs = mutableMapOf<String, TextInputEditText>() diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt index 6e848a144af2c35c587c4347558d324745536f14..214295e7e85b1689cee3619b321d67ae940e4642 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt @@ -42,6 +42,7 @@ import xyz.apiote.bimba.czwek.dashboard.ui.journey.JourneyFragment import xyz.apiote.bimba.czwek.dashboard.ui.map.MapFragment import xyz.apiote.bimba.czwek.databinding.ActivityMainBinding import xyz.apiote.bimba.czwek.onboarding.FirstRunActivity +import xyz.apiote.bimba.czwek.search.Query import xyz.apiote.bimba.czwek.search.ResultsActivity import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker import xyz.apiote.bimba.czwek.settings.ServerChooserActivity @@ -143,7 +144,7 @@ permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false -> { when (permissionAsker) { is HomeFragment -> { - showResults(ResultsActivity.Mode.MODE_LOCATION) + showResults(Query(Query.Mode.LOCATION)) } is MapFragment -> { @@ -205,8 +206,7 @@ Manifest.permission.ACCESS_COARSE_LOCATION ) -> { when (fragment) { is HomeFragment -> { - // TODO make Query class and use also in journey - showResults(ResultsActivity.Mode.MODE_LOCATION) + showResults(Query(Query.Mode.LOCATION)) } is MapFragment -> { @@ -233,20 +233,8 @@ } } } - fun onSearchClicked(text: CharSequence?) { - // TODO make Query class and use also in journey - if (OpenLocationCode.isValidCode(text.toString())) { - val olc = OpenLocationCode(text.toString()) - if (!olc.isFull) { - showResults(ResultsActivity.Mode.MODE_SHORT_CODE_LOCATION, text.toString()) - } else { - val area = olc.decode() - showResults(olc.code, area.centerLatitude, area.centerLongitude) - } - } else if (OpenLocationCode.isValidCode( - text.toString().trim().split(" ").first().trim(',').trim() - ) - ) { + fun onSearchClicked(text: CharSequence) { + if (OpenLocationCode.isValidCode(text.toString().trim().split(" ").first().trim(',').trim())) { if (PreferenceManager.getDefaultSharedPreferences(applicationContext) .getLong(DownloadCitiesWorker.LAST_UPDATE_KEY, -1) < 0 ) { @@ -258,46 +246,29 @@ .setIcon(R.drawable.geocoding) .setTitle(R.string.no_geocoding_data) .setMessage(R.string.no_geocoding_data_description) .setPositiveButton(R.string.ok) { _, _ -> - showResults( - ResultsActivity.Mode.MODE_SEARCH, - text.toString() - ) + showResults(Query(text.toString(), Query.Mode.NAME)) } .show() PreferenceManager.getDefaultSharedPreferences(applicationContext).edit { putBoolean(NO_GEOCODING_DATA_SHOWN, true) } + } else { + showResults(Query(text.toString(), Query.Mode.NAME)) } } else { - showResults(ResultsActivity.Mode.MODE_SHORT_CODE, text.toString()) + showResults(Query(text.toString())) } } else { - showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString()) + showResults(Query(text.toString())) } } - private fun showResults(query: String, centerLatitude: Double, centerLongitude: Double) { - /* todo [3.2] (ux,low) animation - https://developer.android.com/guide/fragments/animate - https://github.com/raheemadamboev/fab-explosion-animation-app - */ - startActivity( - ResultsActivity.getIntent( - this, - ResultsActivity.Mode.MODE_POSITION, - query, - centerLatitude, - centerLongitude - ) - ) - } - - private fun showResults(mode: ResultsActivity.Mode, query: String = "") { + private fun showResults(query: Query) { /* todo [3.2] (ux,low) animation https://developer.android.com/guide/fragments/animate https://github.com/raheemadamboev/fab-explosion-animation-app */ - startActivity(ResultsActivity.getIntent(this, mode, query)) + startActivity(ResultsActivity.getIntent(this, query)) } fun showBadge(complete: Boolean = false) { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyFragment.kt index 1f7d529564f043535e3c99c29992875981811f89..3b7fe0587642546d30be3ebd49b23b9aec4bc173 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyFragment.kt @@ -30,8 +30,11 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.swiperefreshlayout.widget.CircularProgressDrawable +import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable -import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.chip.ChipGroup +import com.google.openlocationcode.OpenLocationCode import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dashboard.DashboardViewModel import xyz.apiote.bimba.czwek.dashboard.MainActivity @@ -39,6 +42,9 @@ import xyz.apiote.bimba.czwek.databinding.FragmentJourneyBinding import xyz.apiote.bimba.czwek.dpToPixelI import xyz.apiote.bimba.czwek.journeys.JourneysActivity import xyz.apiote.bimba.czwek.repo.Place +import xyz.apiote.bimba.czwek.repo.Position +import xyz.apiote.bimba.czwek.search.Query +import xyz.apiote.bimba.czwek.search.Query.Mode import xyz.apiote.bimba.czwek.search.ResultsActivity class JourneyFragment : Fragment(), LocationListener { @@ -50,8 +56,7 @@ private var _binding: FragmentJourneyBinding? = null private val binding get() = _binding!! private lateinit var dashboard: MainActivity - private var hereChipRequester: String? = null - private var searchRequester: String? = null + private lateinit var viewModel: JourneyViewModel private val activityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -62,7 +67,7 @@ @Suppress("DEPRECATION") it.data?.extras?.getParcelable(PLACE_KEY) } place?.let { - dashboard.viewModel.set(searchRequester!!, it) + dashboard.viewModel.set(viewModel.searchRequester!!, it) } } @@ -71,13 +76,14 @@ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val journeyViewModel = - ViewModelProvider(this)[JourneyViewModel::class.java] + viewModel = ViewModelProvider(this)[JourneyViewModel::class.java] // TODO separate layout: two columns for horizontal _binding = FragmentJourneyBinding.inflate(inflater, container, false) val root: View = binding.root + + dashboard = activity as MainActivity dashboard.viewModel.textInputs[DashboardViewModel.ORIGIN_KEY] = binding.origin dashboard.viewModel.textInputs[DashboardViewModel.DEST_KEY] = binding.destination @@ -124,17 +130,17 @@ } windowInsets } - dashboard = activity as MainActivity - dashboard.hideBadge() chipifyOrigin(dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value) chipifyDestination(dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value) binding.originChipHere.setOnClickListener { + setChipProgress(binding.originChipHere) setHere(DashboardViewModel.ORIGIN_KEY) } binding.destinationChipHere.setOnClickListener { + setChipProgress(binding.destinationChipHere) setHere(DashboardViewModel.DEST_KEY) } @@ -194,15 +200,7 @@ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { - if (s.isNullOrBlank()) { - dashboard.viewModel.spans[DashboardViewModel.ORIGIN_KEY] = "" - binding.goButton.isEnabled = false - return - } - binding.goButton.isEnabled = - s.toString().replace(dashboard.viewModel.spans[DashboardViewModel.ORIGIN_KEY]!!, "") == "" && isDestinationClean() - - // todo if searchText looks like coordinates or a plus code (short, long, compound) -> suggest it + afterTextChanged(DashboardViewModel.ORIGIN_KEY, binding.originSuggestions, s, inflater) } }) @@ -212,30 +210,72 @@ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { - if (s.isNullOrBlank()) { - dashboard.viewModel.spans[DashboardViewModel.DEST_KEY] = "" - binding.goButton.isEnabled = false - return - } - binding.goButton.isEnabled = - s.toString().replace(dashboard.viewModel.spans[DashboardViewModel.DEST_KEY]!!, "") == "" && isOriginClean() - - // todo if searchText looks like coordinates or a plus code (short, long, compound) -> suggest it + afterTextChanged(DashboardViewModel.DEST_KEY, binding.destinationSuggestions, s, inflater) } }) return root } - fun searchText(source: String) { - // TODO or coordinates intent - searchRequester = source + private fun afterTextChanged( + source: String, + suggestions: ChipGroup, + s: Editable?, + inflater: LayoutInflater + ) { + if (s.isNullOrBlank()) { + dashboard.viewModel.spans[source] = "" + binding.goButton.isEnabled = false + return + } + binding.goButton.isEnabled = + s.toString().replace( + dashboard.viewModel.spans[source]!!, + "" + ) == "" && isOtherClean(source) + + val q = Query( + dashboard.viewModel.textInputs[source]!!.text.toString() + .replace(dashboard.viewModel.spans[source]!!, "") + ) + q.parse(requireContext()) + if (q.mode in arrayOf(Mode.POSITION, Mode.LOCATION_PLUS_CODE)) { + val chip = inflater.inflate(R.layout.chip_suggestion, suggestions, false) as Chip + chip.text = q.toString() + chip.isCheckable = false + chip.isChipIconVisible = true + chip.setChipIconResource(R.drawable.position) + chip.setOnClickListener { + if (q.mode == Mode.POSITION) { + dashboard.viewModel.set( + source, + Place(q.position!!.latitude, q.position!!.longitude) + ) + } else { + setChipProgress(chip) + dashboard.viewModel.positionQueries[source] = q + viewModel.hereChipRequester = source + setHere(source) + } + } + suggestions.removeView(dashboard.viewModel.positionChips[source]) + dashboard.viewModel.positionChips[source] = chip + suggestions.addView(chip, 1) + } else { + suggestions.removeView(dashboard.viewModel.positionChips[source]) + } + } + + private fun getSearchText(source: String): String = + dashboard.viewModel.textInputs[source]!!.text.toString() + .replace(dashboard.viewModel.spans[source]!!, "") + + private fun searchText(source: String) { + viewModel.searchRequester = source activityLauncher.launch( ResultsActivity.getIntent( requireContext(), - ResultsActivity.Mode.MODE_SEARCH, - dashboard.viewModel.textInputs[source]!!.text.toString().replace(dashboard.viewModel.spans[source]!!, ""), - true + Query(getSearchText(source)) ) ) } @@ -247,16 +287,28 @@ private fun isDestinationClean(): Boolean = binding.destination.text.toString().let { it.replace(dashboard.viewModel.spans[DashboardViewModel.DEST_KEY]!!, "") == "" && it != "" } + + private fun isOtherClean(source: String): Boolean = + if (source == DashboardViewModel.ORIGIN_KEY) { + isDestinationClean() + } else if (source == DashboardViewModel.DEST_KEY) { + isOriginClean() + } else { + TODO("throw exception") + } private fun chipifyOrigin(place: Place?) { val source = DashboardViewModel.ORIGIN_KEY val otherSource = DashboardViewModel.otherSource(source) if (place != null) { - chipify(place, dashboard.viewModel.textInputs[source]!!) + chipify(place, source) dashboard.viewModel.spans[source] = place.shortString() if (dashboard.viewModel.data[otherSource]!!.value != null && isDestinationClean()) { binding.goButton.isEnabled = true } + } else { + binding.origin.setText("") + binding.goButton.isEnabled = false } } @@ -264,15 +316,19 @@ private fun chipifyDestination(place: Place?) { val source = DashboardViewModel.DEST_KEY val otherSource = DashboardViewModel.otherSource(source) if (place != null) { - chipify(place, dashboard.viewModel.textInputs[source]!!) + chipify(place, source) dashboard.viewModel.spans[source] = place.shortString() if (dashboard.viewModel.data[otherSource]!!.value != null && isOriginClean()) { binding.goButton.isEnabled = true } + } else { + binding.destination.setText("") + binding.goButton.isEnabled = false } } - private fun chipify(place: Place, textView: TextInputEditText) { + private fun chipify(place: Place, source: String) { + val textView = dashboard.viewModel.textInputs[source]!! val text = place.shortString() textView.setText(text) var chip: ChipDrawable? = ChipDrawable.createFromResource(requireContext(), R.xml.journey_chip) @@ -303,7 +359,7 @@ } else if (e.x > textView.totalPaddingLeft + chipContentRect.right && e.x < textView.totalPaddingLeft + chipCloseRect.right && e.y > textView.totalPaddingTop && e.y < textView.totalPaddingTop + chipCloseRect.bottom ) { if (e.action == ACTION_UP) { - textView.setText("") + dashboard.viewModel.unset(source) chip = null } true @@ -317,7 +373,7 @@ } } private fun setHere(source: String) { - hereChipRequester = source + viewModel.hereChipRequester = source if (dashboard.onGpsClicked(this)) { try { val locationManager = @@ -341,8 +397,53 @@ super.onDestroyView() _binding = null } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(DashboardViewModel.ORIGIN_KEY, getSearchText(DashboardViewModel.ORIGIN_KEY)) + outState.putString(DashboardViewModel.DEST_KEY, getSearchText(DashboardViewModel.DEST_KEY)) + // TODO and rest of params + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + + if (savedInstanceState != null) { + arrayOf(DashboardViewModel.ORIGIN_KEY, DashboardViewModel.DEST_KEY).forEach { source -> + val searchString = savedInstanceState.getString(source) ?: "" + if (searchString != "") { + binding.origin.setText(searchString) + } + } + // TODO and rest of params + } + } + override fun onLocationChanged(location: Location) { - hereChipRequester?.let { dashboard.viewModel.set(it, Place(location.latitude, location.longitude)) } - hereChipRequester = null + viewModel.hereChipRequester?.let { + val query = dashboard.viewModel.positionQueries[it] + val position = if (query != null && query.mode == Mode.LOCATION_PLUS_CODE) { + OpenLocationCode(query.raw).recover(location.latitude, location.longitude).decode().let { + viewModel.loadingChip?.setChipIconResource(R.drawable.position) + Position(it.centerLatitude, it.centerLatitude) + } + } else { + viewModel.loadingChip?.setChipIconResource(R.drawable.gps_black) + Position(location.latitude, location.longitude) + } + dashboard.viewModel.set( + it, + Place(position.latitude, position.longitude) + ) + dashboard.viewModel.positionQueries[it] = null + } + viewModel.hereChipRequester = null + } + + private fun setChipProgress(chip: Chip) { + viewModel.loadingChip = chip + chip.chipIcon = CircularProgressDrawable(requireContext()).apply { + setStyle(CircularProgressDrawable.DEFAULT) + start() + } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyViewModel.kt index 87c80d7c047df700c968d48c73e0600f1e474384..d26d70d8a50adccd3e5487d7a304369bcdd1ae7f 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/journey/JourneyViewModel.kt @@ -4,15 +4,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.dashboard.ui.journey -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.annotation.SuppressLint import androidx.lifecycle.ViewModel +import com.google.android.material.chip.Chip class JourneyViewModel : ViewModel() { - private val _text = MutableLiveData<String>().apply { - value = "This is voyage Fragment" - } - val text: LiveData<String> = _text + var hereChipRequester: String? = null + var searchRequester: String? = null + + @SuppressLint("StaticFieldLeak") + var loadingChip: Chip? = null } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Query.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Query.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd2f303f39d626769cd2a9fda0a4e47d67aab570 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Query.kt @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.bimba.czwek.search + +import android.content.Context +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import com.google.openlocationcode.OpenLocationCode +import kotlinx.parcelize.Parcelize +import xyz.apiote.bimba.czwek.repo.Position + +class Query : Parcelable { + companion object { + @Suppress("unused") + @JvmField + val CREATOR = object : Parcelable.Creator<Query> { + @Suppress("DEPRECATION") + override fun createFromParcel(source: Parcel?): Query? { + if (source == null) { + return null + } else { + val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + source.readParcelable(Mode::class.java.classLoader, Mode::class.java)!! + } else { + source.readParcelable(Mode::class.java.classLoader)!! + } + val raw = source.readString()!! + val position = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + source.readParcelable(Position::class.java.classLoader, Position::class.java)!! + } else { + source.readParcelable(Position::class.java.classLoader)!! + } + return Query(mode, raw, position) + } + } + + override fun newArray(size: Int): Array<out Query?>? { + return arrayOfNulls(size) + } + + } + } + + @Parcelize + enum class Mode : Parcelable { + LOCATION, POSITION, NAME, LOCATION_PLUS_CODE, UNKNOWN + } + + constructor(raw: String) { + mode = Mode.UNKNOWN + this.raw = raw + position = null + } + + constructor(raw: String, mode: Mode) { + this.mode = mode + this.raw = raw + position = null + } + + constructor(mode: Mode) { + if (mode != Mode.LOCATION) { + throw Exception("Cannot initialise Query from bare Mode other than LOCATION") + } + this.mode = mode + raw = "" + position = null + } + + constructor(position: Position) { + mode = Mode.POSITION + raw = position.toString() + this.position = position + } + + private constructor(mode: Mode, raw: String, position: Position?) { + this.mode = mode + this.raw = raw + this.position = position + } + + var mode: Mode + private set + val raw: String + var position: Position? + private set + + fun parse(context: Context) { + if (mode != Mode.UNKNOWN) { + return + } + /* TODO parse coordinates https://developer.android.com/reference/android/location/Location#convert(java.lang.String) + replace `"?` -> `` + replace `' ?` -> `:` + replace `° ?` -> `:` + split by space/locale-separator + if number has locale-digit-separator, replace with dot + if has E/S at end, set sign + */ + if (OpenLocationCode.isValidCode(raw)) { + val olc = OpenLocationCode(raw) + if (!olc.isFull) { + mode = Mode.LOCATION_PLUS_CODE + } else { + val area = olc.decode() + mode = Mode.POSITION + position = Position(area.centerLatitude, area.centerLongitude) + } + } else if (OpenLocationCode.isValidCode(raw.trim().split(" ").first().trim(',').trim())) { + mode = Mode.POSITION + geocode(context) + } else { + mode = Mode.NAME + } + } + + private fun geocode(context: Context) { + val split = raw.trim().split(" ") + val code = split.first().trim(',').trim() + val freePart = split.drop(1).joinToString(" ") + val location = findPlace(context, freePart) + if (location == null) { + throw GeocodingException() + } else { + val area = OpenLocationCode(code).recover(location.latitude, location.longitude).decode() + position = Position(area.centerLatitude, area.centerLongitude) + } + } + + override fun describeContents(): Int = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(mode, flags) + dest.writeString(raw) + dest.writeParcelable(position, flags) + } + + override fun toString(): String { + return when (mode) { + Mode.UNKNOWN -> raw + Mode.LOCATION -> "here" + Mode.POSITION -> "%.2f, %.2f".format(position!!.latitude, position!!.longitude) // TODO settings for position format + Mode.NAME -> raw + Mode.LOCATION_PLUS_CODE -> raw + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index e9200b17821fa4122cee1174b732f5d900ae75bb..5332c8d63e9a152ef1b83a4c2b5fd61d35407ea3 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt @@ -44,46 +44,20 @@ import xyz.apiote.bimba.czwek.repo.Position import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.Stop import xyz.apiote.bimba.czwek.repo.TrafficResponseException +import xyz.apiote.bimba.czwek.search.Query.Mode import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener { - enum class Mode { - MODE_LOCATION, MODE_SEARCH, MODE_POSITION, MODE_SHORT_CODE_LOCATION, MODE_SHORT_CODE - } - companion object { - const val MODE_KEY = "mode" const val QUERY_KEY = "query" - const val LATITUDE_KEY = "lat" - const val LONGITUDE_KEY = "lon" const val RETURN_KEY = "ret" fun getIntent( context: Context, - mode: Mode, - query: String, - latitude: Double, - longitude: Double + query: Query ) = Intent(context, ResultsActivity::class.java).apply { - putExtra(MODE_KEY, mode) putExtra(QUERY_KEY, query) - putExtra(LATITUDE_KEY, latitude) - putExtra(LONGITUDE_KEY, longitude) } - - fun getIntent(context: Context, mode: Mode, query: String) = - Intent(context, ResultsActivity::class.java).apply { - putExtra(MODE_KEY, mode) - putExtra(QUERY_KEY, query) - } - - fun getIntent(context: Context, mode: Mode, query: String, returnResult: Boolean) = - Intent(context, ResultsActivity::class.java).apply { - putExtra(MODE_KEY, mode) - putExtra(QUERY_KEY, query) - putExtra(RETURN_KEY, returnResult) - } - } private var _binding: ActivityResultsBinding? = null @@ -96,6 +70,7 @@ private var runnable = Runnable {} private var gravity: FloatArray? = null private var geomagnetic: FloatArray? = null private var shortOLC: OpenLocationCode? = null + private lateinit var query: Query override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -125,62 +100,54 @@ adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false, getReturnResults()) binding.resultsRecycler.adapter = adapter - when (getMode()) { - Mode.MODE_LOCATION -> { + query = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(QUERY_KEY, Query::class.java)!! + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(QUERY_KEY)!! + } + + useQuery() + } + + private fun useQuery() { + when (query.mode) { + Mode.LOCATION -> { binding.topAppBar.title = getString(R.string.stops_nearby) locate() } - Mode.MODE_SHORT_CODE_LOCATION -> { - val query = intent.extras?.getString(QUERY_KEY) - binding.topAppBar.title = getString(R.string.stops_near_code, query) - shortOLC = OpenLocationCode(query) + Mode.LOCATION_PLUS_CODE -> { + binding.topAppBar.title = getString(R.string.stops_near_code, query.raw) + shortOLC = OpenLocationCode(query.raw) locate() } - Mode.MODE_SHORT_CODE -> { - val query = intent.extras?.getString(QUERY_KEY) - binding.topAppBar.title = getString(R.string.stops_near_code, query) - val split = query!!.trim().split(" ") - val code = split.first().trim(',').trim() - val freePart = split.drop(1).joinToString(" ") - val location = findPlace(this, freePart) - if (location == null) { + Mode.UNKNOWN -> { + try { + query.parse(this) + if (query.mode != Mode.UNKNOWN) { + useQuery() + } else { + showError(Error(0, R.string.error_unknown, R.drawable.error_other)) + } + } catch (_: GeocodingException) { showError(Error(0, R.string.error_geocoding, R.drawable.geocoding)) - } else { - val area = OpenLocationCode(code).recover(location.latitude, location.longitude).decode() - getQueryablesByLocation(Location(null).apply { - latitude = area.centerLatitude - longitude = area.centerLongitude - }, this) } } - Mode.MODE_POSITION -> { - val query = intent.extras?.getString(QUERY_KEY) - val lat = intent.extras?.getDouble(LATITUDE_KEY) - val lon = intent.extras?.getDouble(LONGITUDE_KEY) - binding.topAppBar.title = getString(R.string.stops_near_code, query) + Mode.POSITION -> { + binding.topAppBar.title = getString(R.string.stops_near_code, query.raw) getQueryablesByLocation(Location(null).apply { - latitude = lat!! - longitude = lon!! + latitude = query.position!!.latitude + longitude = query.position!!.longitude }, this) } - Mode.MODE_SEARCH -> { - val query = intent.extras?.getString(QUERY_KEY)!! - binding.topAppBar.title = getString(R.string.results_for, query) - getQueryablesByQuery(query, this) + Mode.NAME -> { + binding.topAppBar.title = getString(R.string.results_for, query.raw) + getQueryablesByQuery(query.raw, this) } - } - } - - private fun getMode(): Mode { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.extras!!.getSerializable(MODE_KEY, Mode::class.java)!! - } else { - @Suppress("DEPRECATION") - intent.extras!!.get(MODE_KEY) as Mode } } @@ -243,7 +210,7 @@ } override fun onResume() { super.onResume() - if (getMode() == Mode.MODE_LOCATION) { + if (query.mode == Mode.LOCATION) { locate() } } @@ -340,7 +307,7 @@ R.drawable.error_search ) ) } else { - if (queryables.size == 1 && getMode() == Mode.MODE_SEARCH && !getReturnResults()) { + if (queryables.size == 1 && query.mode == Query.Mode.NAME && !getReturnResults()) { adapter.click(0) } binding.resultsOverlay.visibility = View.GONE diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt index 98ed6ec4d75edc50fdf0e7105d5bcf06ae28a047..98c152f66af7260e13c063ea7a2f1035595d216e 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt @@ -28,4 +28,6 @@ } cursor.close() db.close() return location -} \ No newline at end of file +} + +class GeocodingException : Exception() \ No newline at end of file diff --git a/app/src/main/res/drawable/position.xml b/app/src/main/res/drawable/position.xml new file mode 100644 index 0000000000000000000000000000000000000000..2256cca5d494cdf165812525602e2011399b9fe5 --- /dev/null +++ b/app/src/main/res/drawable/position.xml @@ -0,0 +1,15 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorOnSurface" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z" /> +</vector> diff --git a/app/src/main/res/layout/chip_suggestion.xml b/app/src/main/res/layout/chip_suggestion.xml new file mode 100644 index 0000000000000000000000000000000000000000..211f8a5261418b9a19dc6140094e16a5b16ca1db --- /dev/null +++ b/app/src/main/res/layout/chip_suggestion.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> + +<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/Widget.Material3.Chip.Suggestion" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + +</com.google.android.material.chip.Chip> \ No newline at end of file