Author: Adam Evyčędo <git@apiote.xyz>
search journeys
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6b0f42c7e6e64deaf93a50a8f605ac3ffbbd1af2..6ac1b9606241595454e56a100916975221693bc0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -101,8 +101,8 @@ implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
- implementation("androidx.navigation:navigation-fragment-ktx:2.8.4")
- implementation("androidx.navigation:navigation-ui-ktx:2.8.4")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.8.5")
+ implementation("androidx.navigation:navigation-ui-ktx:2.8.5")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.google.openlocationcode:openlocationcode:1.0.4")
@@ -111,21 +111,21 @@ implementation("org.yaml:snakeyaml:2.3")
implementation("androidx.activity:activity-ktx:1.9.3")
implementation("com.otaliastudios:zoomlayout:1.9.0")
implementation("dev.bandb.graphview:graphview:0.8.1")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0")
implementation("com.github.jershell:kbson:0.5.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.10.0")
implementation("commons-io:commons-io:2.18.0")
- implementation("com.google.guava:guava:33.3.1-android")
+ implementation("com.google.guava:guava:33.4.0-android")
implementation(project(":fruchtfleisch"))
implementation("ch.acra:acra-http:5.12.0")
implementation("ch.acra:acra-notification:5.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
- implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
+ implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("androidx.activity:activity:1.9.3")
- coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
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
new file mode 100644
index 0000000000000000000000000000000000000000..b06ba0824486a44e7fa53987d5a7a55a6bc0b50d
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.bimba.czwek.dashboard
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.google.android.material.textfield.TextInputEditText
+import xyz.apiote.bimba.czwek.repo.Place
+
+class DashboardViewModel : ViewModel() {
+ companion object {
+ const val ORIGIN_KEY = "ORIGIN"
+ const val DEST_KEY = "DESTINATION"
+
+ val keys = arrayOf(ORIGIN_KEY, DEST_KEY)
+ val indices = mapOf(ORIGIN_KEY to 0, DEST_KEY to 1)
+
+ fun otherSource(source: String): String = keys[(indices[source]!! + 1) % 2]
+ }
+
+ val mutableData = mapOf(
+ ORIGIN_KEY to MutableLiveData<Place>(),
+ DEST_KEY to MutableLiveData<Place>()
+ )
+
+ val data = mapOf<String, LiveData<Place>>(
+ ORIGIN_KEY to mutableData[ORIGIN_KEY]!!,
+ DEST_KEY to mutableData[DEST_KEY]!!
+ )
+
+ fun set(source: String, place: Place) {
+ mutableData[source]!!.value = place
+ }
+
+ val spans = mutableMapOf(
+ ORIGIN_KEY to "",
+ DEST_KEY to ""
+ )
+
+ 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 ad4d49d02249fe8df2290fa316556d0aaaf7d9a2..0ac2110a07cbac7e22c698867779985daae5ff9e 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
@@ -7,6 +7,7 @@
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
+import android.content.res.TypedArray
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -27,6 +28,7 @@ import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
+import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
@@ -53,12 +55,16 @@ private lateinit var locationPermissionRequest: ActivityResultLauncher>
private lateinit var permissionAsker: Fragment
private var locationPermissionDialogShown = false
+ private var lastFragment: Fragment? = null
+ lateinit var viewModel: DashboardViewModel
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+
+ viewModel = ViewModelProvider(this)[DashboardViewModel::class.java]
FirstRunActivity.setFirstRunDone(this)
@@ -191,7 +197,7 @@ binding.container.openDrawer(binding.navigationDrawer)
}
}
- fun onGpsClicked(fragment: Fragment) {
+ fun onGpsClicked(fragment: Fragment): Boolean {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
this,
@@ -205,7 +211,12 @@
is MapFragment -> {
fragment.showLocation()
}
+
+ is JourneyFragment -> {
+
+ }
}
+ return true
}
else -> {
@@ -216,6 +227,7 @@ Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
+ return false
}
}
}
@@ -250,7 +262,7 @@ text.toString()
)
}
.show()
- PreferenceManager.getDefaultSharedPreferences(applicationContext).edit{
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
putBoolean(NO_GEOCODING_DATA_SHOWN, true)
}
}
@@ -267,7 +279,15 @@ /* 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))
+ startActivity(
+ ResultsActivity.getIntent(
+ this,
+ ResultsActivity.Mode.MODE_POSITION,
+ query,
+ centerLatitude,
+ centerLongitude
+ )
+ )
}
private fun showResults(mode: ResultsActivity.Mode, query: String = "") {
@@ -278,25 +298,53 @@ */
startActivity(ResultsActivity.getIntent(this, mode, query))
}
- private fun setNavbarIcons(f: Fragment) {
+ fun showBadge(complete: Boolean = false) {
+ val colourID = if (complete) {
+ com.google.android.material.R.attr.colorPrimary
+ } else {
+ com.google.android.material.R.attr.colorOnSurfaceVariant
+ }
+ val badge = binding.bottomNavigation.getOrCreateBadge(R.id.navigation_journey)
+ val a: TypedArray = theme.obtainStyledAttributes(
+ R.style.Theme_Bimba, intArrayOf(colourID)
+ )
+ val colour = a.getColor(0, 0)
+ a.recycle()
+ badge.backgroundColor = colour
+ badge.isVisible = true
+ }
+
+ fun hideBadge() {
+ val badge = binding.bottomNavigation.getBadge(R.id.navigation_journey)
+ badge?.isVisible = false
+ }
+
+ private fun setNavbarIcons(f: Fragment?) {
binding.bottomNavigation.menu[2].setIcon(R.drawable.journey_outline)
binding.bottomNavigation.menu[1].setIcon(R.drawable.home_outline)
binding.bottomNavigation.menu[0].setIcon(R.drawable.map_outline)
when (f) {
is HomeFragment -> {
binding.bottomNavigation.menu[1].setIcon(R.drawable.home_black)
+ lastFragment = f
}
is JourneyFragment -> {
binding.bottomNavigation.menu[2].setIcon(R.drawable.journey_black)
+ lastFragment = f
}
is MapFragment -> {
binding.bottomNavigation.menu[0].setIcon(R.drawable.map_black)
+ lastFragment = f
+ }
+
+ null -> {
+ binding.bottomNavigation.menu[1].setIcon(R.drawable.home_black)
}
else -> {
- binding.bottomNavigation.menu[1].setIcon(R.drawable.home_black)
+ setNavbarIcons(lastFragment)
}
}
}
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 544558c0bcfc8f8cf128054e8e2f6daa9fcd17cb..1f7d529564f043535e3c99c29992875981811f89 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
@@ -4,24 +4,68 @@ // SPDX-License-Identifier: GPL-3.0-or-later
package xyz.apiote.bimba.czwek.dashboard.ui.journey
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.RectF
+import android.graphics.drawable.LayerDrawable
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Build
import android.os.Bundle
+import android.text.Editable
+import android.text.Spanned
+import android.text.TextWatcher
+import android.text.style.ImageSpan
+import android.util.Log
+import android.view.KeyEvent
import android.view.LayoutInflater
+import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.chip.ChipDrawable
+import com.google.android.material.textfield.TextInputEditText
+import xyz.apiote.bimba.czwek.R
+import xyz.apiote.bimba.czwek.dashboard.DashboardViewModel
+import xyz.apiote.bimba.czwek.dashboard.MainActivity
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.ChangeOption
import xyz.apiote.bimba.czwek.repo.Place
-import xyz.apiote.bimba.czwek.repo.Position
-import xyz.apiote.bimba.czwek.repo.Stop
+import xyz.apiote.bimba.czwek.search.ResultsActivity
-class JourneyFragment : Fragment() {
+class JourneyFragment : Fragment(), LocationListener {
+ companion object {
+ const val PLACE_KEY = "PLACE"
+ }
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 val activityLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val place = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
+ it.data?.extras?.getParcelable(PLACE_KEY, Place::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ it.data?.extras?.getParcelable(PLACE_KEY)
+ }
+ place?.let {
+ dashboard.viewModel.set(searchRequester!!, it)
+ }
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -30,29 +74,275 @@ ): View {
val journeyViewModel =
ViewModelProvider(this)[JourneyViewModel::class.java]
+ // TODO separate layout: two columns for horizontal
+
_binding = FragmentJourneyBinding.inflate(inflater, container, false)
val root: View = binding.root
- val intent = JourneysActivity.getIntent(
- requireContext(),
- Place(
- Stop("", "", "", "", "", Position(0.0, 0.0), emptyList<ChangeOption>(), ""),
- 52.402815,
- 16.911795
- ),
- Place(
- Stop("", "", "", "", "", Position(0.0, 0.0), emptyList<ChangeOption>(), ""),
- 52.445433,
- 17.079231
+ dashboard.viewModel.textInputs[DashboardViewModel.ORIGIN_KEY] = binding.origin
+ dashboard.viewModel.textInputs[DashboardViewModel.DEST_KEY] = binding.destination
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ binding.originInput.updatePadding(left = insets.left, right = insets.right)
+ binding.originInput.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ topMargin = insets.top + dpToPixelI(16f)
+ windowInsets.displayCutout?.safeInsetLeft?.let {
+ leftMargin = it + dpToPixelI(16f)
+ }
+ windowInsets.displayCutout?.safeInsetRight?.let {
+ rightMargin = it + dpToPixelI(16f)
+ }
+ }
+ binding.originSuggestions.updatePadding(left = insets.left, right = insets.right)
+ binding.originSuggestions.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ windowInsets.displayCutout?.safeInsetLeft?.let {
+ leftMargin = it + dpToPixelI(16f)
+ }
+ windowInsets.displayCutout?.safeInsetRight?.let {
+ rightMargin = it + dpToPixelI(16f)
+ }
+ }
+ binding.destinationInput.updatePadding(left = insets.left, right = insets.right)
+ binding.destinationInput.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ windowInsets.displayCutout?.safeInsetLeft?.let {
+ leftMargin = it + dpToPixelI(16f)
+ }
+ windowInsets.displayCutout?.safeInsetRight?.let {
+ rightMargin = it + dpToPixelI(16f)
+ }
+ }
+ binding.destinationSuggestions.updatePadding(left = insets.left, right = insets.right)
+ binding.destinationSuggestions.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ windowInsets.displayCutout?.safeInsetLeft?.let {
+ leftMargin = it + dpToPixelI(16f)
+ }
+ windowInsets.displayCutout?.safeInsetRight?.let {
+ rightMargin = it + dpToPixelI(16f)
+ }
+ }
+ 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 {
+ setHere(DashboardViewModel.ORIGIN_KEY)
+ }
+
+ binding.destinationChipHere.setOnClickListener {
+ setHere(DashboardViewModel.DEST_KEY)
+ }
+
+ binding.goButton.isEnabled =
+ dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value != null && dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value != null
+
+ binding.goButton.setOnClickListener {
+ val intent = JourneysActivity.getIntent(
+ requireContext(),
+ dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value!!,
+ dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value!!,
)
- )
- startActivity(intent)
+ startActivity(intent)
+ }
+
+ dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.observe(viewLifecycleOwner) {
+ chipifyOrigin(it)
+ }
+
+ dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.observe(viewLifecycleOwner) {
+ chipifyDestination(it)
+ }
+
+ binding.origin.setOnKeyListener { v, keyCode, event ->
+ when (keyCode) {
+ KeyEvent.KEYCODE_ENTER -> {
+ if (event.action == KeyEvent.ACTION_UP) {
+ searchText(DashboardViewModel.ORIGIN_KEY)
+ true
+ } else {
+ false
+ }
+ }
+
+ else -> false
+ }
+ }
+
+ binding.destination.setOnKeyListener { v, keyCode, event ->
+ when (keyCode) {
+ KeyEvent.KEYCODE_ENTER -> {
+ if (event.action == KeyEvent.ACTION_UP) {
+ searchText(DashboardViewModel.DEST_KEY)
+ true
+ } else {
+ false
+ }
+ }
+
+ else -> false
+ }
+ }
+
+ binding.origin.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ 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
+ }
+ })
+
+ binding.destination.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ 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
+ }
+ })
return root
}
+ fun searchText(source: String) {
+ // TODO or coordinates intent
+ searchRequester = source
+ activityLauncher.launch(
+ ResultsActivity.getIntent(
+ requireContext(),
+ ResultsActivity.Mode.MODE_SEARCH,
+ dashboard.viewModel.textInputs[source]!!.text.toString().replace(dashboard.viewModel.spans[source]!!, ""),
+ true
+ )
+ )
+ }
+
+ private fun isOriginClean(): Boolean = binding.origin.text.toString().let {
+ it.replace(dashboard.viewModel.spans[DashboardViewModel.ORIGIN_KEY]!!, "") == "" && it != ""
+ }
+
+ private fun isDestinationClean(): Boolean = binding.destination.text.toString().let {
+ it.replace(dashboard.viewModel.spans[DashboardViewModel.DEST_KEY]!!, "") == "" && it != ""
+ }
+
+ private fun chipifyOrigin(place: Place?) {
+ val source = DashboardViewModel.ORIGIN_KEY
+ val otherSource = DashboardViewModel.otherSource(source)
+ if (place != null) {
+ chipify(place, dashboard.viewModel.textInputs[source]!!)
+ dashboard.viewModel.spans[source] = place.shortString()
+ if (dashboard.viewModel.data[otherSource]!!.value != null && isDestinationClean()) {
+ binding.goButton.isEnabled = true
+ }
+ }
+ }
+
+ private fun chipifyDestination(place: Place?) {
+ val source = DashboardViewModel.DEST_KEY
+ val otherSource = DashboardViewModel.otherSource(source)
+ if (place != null) {
+ chipify(place, dashboard.viewModel.textInputs[source]!!)
+ dashboard.viewModel.spans[source] = place.shortString()
+ if (dashboard.viewModel.data[otherSource]!!.value != null && isOriginClean()) {
+ binding.goButton.isEnabled = true
+ }
+ }
+ }
+
+ private fun chipify(place: Place, textView: TextInputEditText) {
+ val text = place.shortString()
+ textView.setText(text)
+ var chip: ChipDrawable? = ChipDrawable.createFromResource(requireContext(), R.xml.journey_chip)
+ chip!!.text = text
+ val ld = LayerDrawable(arrayOf(chip)).apply {
+ setLayerInset(0, dpToPixelI(4f), 0, dpToPixelI(4f), 0)
+ setBounds(0, 0, chip.intrinsicWidth, chip.intrinsicHeight)
+ }
+ val span = ImageSpan(ld)
+ textView.text?.setSpan(span, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ @SuppressLint("ClickableViewAccessibility")
+ textView.setOnTouchListener { v, e ->
+ v.performClick()
+ if (chip != null) {
+ val chipContentRect = RectF()
+ val chipCloseRect = RectF()
+ chip!!.getChipTouchBounds(chipContentRect)
+ chip!!.getCloseIconTouchBounds(chipCloseRect)
+ if (e.x > textView.totalPaddingLeft && e.x < textView.totalPaddingLeft + chipContentRect.right
+ && e.y > textView.totalPaddingTop && e.y < textView.totalPaddingTop + chipContentRect.bottom
+ ) {
+ if (e.action == ACTION_UP) {
+ // TODO popup
+ Log.i("Touch", "content")
+ }
+ true
+ } 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("")
+ chip = null
+ }
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+ }
+
+ private fun setHere(source: String) {
+ hereChipRequester = source
+ if (dashboard.onGpsClicked(this)) {
+ try {
+ val locationManager =
+ requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ locationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER, 1000 * 60 * 10, 100f, this
+ )
+ locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ ?.let { onLocationChanged(it) }
+ } catch (_: SecurityException) {
+ Log.wtf(
+ "locate",
+ "this shouldn’t happen because we don’t run this without location permission"
+ )
+ }
+ }
+ }
+
override fun onDestroyView() {
super.onDestroyView()
_binding = null
+ }
+
+ override fun onLocationChanged(location: Location) {
+ hereChipRequester?.let { dashboard.viewModel.set(it, Place(location.latitude, location.longitude)) }
+ hereChipRequester = null
}
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt
index 65339fd2b8ef9eed82359fd09bd00e3bea0bce44..84abcd97aaf73ca976eba8b0614e9ae59e24d8df 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt
@@ -34,12 +34,14 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.snackbar.Snackbar
import org.osmdroid.config.Configuration
+import org.osmdroid.events.MapEventsReceiver
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
+import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
@@ -50,6 +52,7 @@ import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.databinding.FragmentMapBinding
import xyz.apiote.bimba.czwek.dpToPixelI
import xyz.apiote.bimba.czwek.repo.ErrorLocatable
+import xyz.apiote.bimba.czwek.repo.Place
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.Vehicle
@@ -62,6 +65,7 @@ const val PREFERENCES_NAME = "shp"
const val ZOOM_KEY = "mapZoom"
const val CENTRE_LATITUDE_KEY = "mapCentreLat"
const val CENTRE_LONGITUDE_KEY = "mapCentreLon"
+ const val PLACE_MARKER = "PLACE"
}
private var maybeBinding: FragmentMapBinding? = null
@@ -74,6 +78,7 @@ private val handler = Handler(Looper.getMainLooper())
private var workRunnable = Runnable {}
private var snack: Snackbar? = null
+ private lateinit var placeMarker: Marker
@SuppressLint("ClickableViewAccessibility")
override fun onCreateView(
@@ -133,6 +138,38 @@ return true
}
})
+
+ placeMarker = Marker(binding.map)
+ placeMarker.id = PLACE_MARKER
+ placeMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
+ placeMarker.icon = AppCompatResources.getDrawable(requireContext(), R.drawable.pin)
+
+ binding.map.overlays.add(MapEventsOverlay(object : MapEventsReceiver {
+ override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
+ return false
+ }
+
+ override fun longPressHelper(p: GeoPoint?): Boolean {
+ if (p == null) {
+ return false
+ }
+ binding.map.overlays.remove(placeMarker)
+ placeMarker.position = p
+ binding.map.overlays.add(placeMarker)
+ binding.map.invalidate()
+
+ val s = PlaceBottomSheet(requireContext(), Place(p.latitude, p.longitude)) {positionUsed ->
+ if (positionUsed) {
+ (activity as MainActivity).showBadge()
+ }
+ binding.map.overlays.remove(placeMarker)
+ binding.map.invalidate()
+ }
+ s.show((activity as MainActivity).supportFragmentManager, PlaceBottomSheet.TAG)
+ return true
+ }
+ }))
+
binding.map.setOnTouchListener { _, _ ->
binding.floatingActionButton.show()
false
@@ -188,7 +225,7 @@
private fun observeLocatables() {
mapViewModel.locatables.observe(viewLifecycleOwner) {
binding.map.overlays.removeAll { marker ->
- marker is Marker
+ marker is Marker && marker.id != PLACE_MARKER
}
if (it.size == 1 && it[0] is ErrorLocatable) {
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
index ceeb223f0fc4e4f66c0e9c81fda2a79c08c586f0..344edca75f0285430066d9d67abb57344df07dc5 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
@@ -6,6 +6,7 @@ package xyz.apiote.bimba.czwek.dashboard.ui.map
import android.content.ActivityNotFoundException
import android.content.Context
+import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -26,11 +27,14 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.launch
import org.osmdroid.views.MapView
import xyz.apiote.bimba.czwek.R
+import xyz.apiote.bimba.czwek.dashboard.DashboardViewModel
+import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.departures.DeparturesActivity
import xyz.apiote.bimba.czwek.repo.CongestionLevel
import xyz.apiote.bimba.czwek.repo.Locatable
import xyz.apiote.bimba.czwek.repo.OccupancyStatus
import xyz.apiote.bimba.czwek.repo.OnlineRepository
+import xyz.apiote.bimba.czwek.repo.Place
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.TrafficResponseException
@@ -57,6 +61,39 @@ }
}
}
+class PlaceBottomSheet(private val context: Context, private val place: Place, private val onDismiss: (Boolean) -> Unit) : BottomSheetDialogFragment() {
+ companion object {
+ const val TAG = "PlaceBottomSheet"
+ }
+
+ private var positionUsed = false
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.place_bottom_sheet, container, false)
+ view.findViewById<TextView>(R.id.coordinates).text = "${place.latitude}, ${place.longitude}"
+ view.findViewById<Button>(R.id.use_as_origin).setOnClickListener {
+ (activity as MainActivity).viewModel.set(DashboardViewModel.ORIGIN_KEY, place)
+ positionUsed = true
+ dismiss()
+ }
+ view.findViewById<Button>(R.id.use_as_destination).setOnClickListener {
+ (activity as MainActivity).viewModel.set(DashboardViewModel.DEST_KEY, place)
+ positionUsed = true
+ dismiss()
+ }
+ return view
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ onDismiss(positionUsed)
+ super.onDismiss(dialog)
+ }
+}
+
class MapBottomSheet(private val locatable: Locatable) : BottomSheetDialogFragment() {
companion object {
const val TAG = "MapBottomSheet"
@@ -168,7 +205,15 @@ private fun showStop(content: View, stop: Stop) {
context?.let { ctx ->
content.findViewById<TextView>(R.id.stop_name).text = stop.name
content.findViewById<Button>(R.id.departures_button).setOnClickListener {
- startActivity(DeparturesActivity.getIntent(requireContext(), stop.code, stop.name, stop.feedID!!, true))
+ startActivity(
+ DeparturesActivity.getIntent(
+ requireContext(),
+ stop.code,
+ stop.name,
+ stop.feedID!!,
+ true
+ )
+ )
}
content.findViewById<Button>(R.id.navigation_button).setOnClickListener {
try {
@@ -181,6 +226,18 @@ )
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, ctx.getString(R.string.no_map_app), Toast.LENGTH_SHORT).show()
}
+ }
+
+ content.findViewById<Button>(R.id.use_as_origin).setOnClickListener {
+ (activity as MainActivity).viewModel.set(DashboardViewModel.ORIGIN_KEY, Place(stop, stop.location().latitude, stop.location().longitude))
+ (ctx as MainActivity).showBadge()
+ dismiss()
+ }
+
+ content.findViewById<Button>(R.id.use_as_destination).setOnClickListener {
+ (activity as MainActivity).viewModel.set(DashboardViewModel.DEST_KEY, Place(stop, stop.location().latitude, stop.location().longitude))
+ (ctx as MainActivity).showBadge()
+ dismiss()
}
stop.changeOptions(ctx, Stop.LineDecoration.NONE).let { changeOptions ->
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Journey.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Journey.kt
index 90e2b6064503ef5cddf50cbcbd9be446256e856f..fe259f7d7cbf3b9822d9bc4dbb3f5575e48f9d13 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Journey.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Journey.kt
@@ -29,16 +29,34 @@ val steps: List?,*/
)
@Parcelize
-class Place(val stop: Stop, val latitude: Double, val longitude: Double): Parcelable {
+class Place(val stop: Stop, val latitude: Double, val longitude: Double) : Parcelable {
constructor(place: Place) : this(
stop = Stop(place),
latitude = place.lat.toDouble(),
longitude = place.lon.toDouble()
)
+ constructor(latitude: Double, longitude: Double) : this(
+ Stop(
+ "", "",
+ "", "", null, Position(latitude, longitude), emptyList<ChangeOption>(), null
+ ), latitude, longitude
+ )
+
+ constructor(stop: Stop) : this(stop, stop.position.latitude, stop.position.longitude)
+
fun planString(): String = if (stop.code == "") {
"${latitude},${longitude},0"
} else {
stop.code
+ }
+
+ fun shortString(): String = if (stop.name == "") {
+ "%.2f, %.2f".format(
+ latitude,
+ longitude
+ )
+ } else {
+ stop.name
}
}
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
index 2df5391da9da2b9bb1eddecfc5ba8bc2c4568ef9..36356cbe7e32da37a1451ea20fbfc507bc65292f 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
@@ -215,7 +215,8 @@ private val context: Context?,
private var queryables: List<Queryable>,
private var position: Location?,
private var heading: Float?,
- private var showArrow: Boolean
+ private var showArrow: Boolean,
+ private val returnResult: Boolean = false
) :
RecyclerView.Adapter<BimbaViewHolder>() {
class DiffUtilCallback(
@@ -291,15 +292,20 @@ var feedsSettings: FeedsSettings? = null
private val onClickListener: ((Queryable) -> Unit) = {
when (it) {
is Stop -> {
- val intent = Intent(context, DeparturesActivity::class.java).apply {
- putExtra("code", it.code)
- putExtra("name", it.name)
- putExtra("feedID", it.feedID)
+ if (returnResult) {
+ (context as ResultsActivity).returnResult(it)
+ } else {
+ val intent = Intent(context, DeparturesActivity::class.java).apply {
+ putExtra("code", it.code)
+ putExtra("name", it.name)
+ putExtra("feedID", it.feedID)
+ }
+ context!!.startActivity(intent)
}
- context!!.startActivity(intent)
}
is Line -> {
+ // TODO if returnResult -> shouldn't show lines
val intent = Intent(context, LineGraphActivity::class.java).apply {
putExtra("lineName", it.name)
putExtra("lineID", it.id)
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 0c87cba970f5cce61d4d0d45aec18eef3015dfa7..e9200b17821fa4122cee1174b732f5d900ae75bb 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
@@ -35,11 +35,14 @@ import kotlinx.coroutines.launch
import org.openapitools.client.infrastructure.ServerException
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.Error
+import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.databinding.ActivityResultsBinding
import xyz.apiote.bimba.czwek.repo.OfflineRepository
import xyz.apiote.bimba.czwek.repo.OnlineRepository
+import xyz.apiote.bimba.czwek.repo.Place
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.settings.feeds.FeedsSettings
@@ -53,6 +56,7 @@ 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,
@@ -73,6 +77,13 @@ 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
@@ -110,7 +121,8 @@ windowInsets
}
binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
- adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false)
+ adapter =
+ BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false, getReturnResults())
binding.resultsRecycler.adapter = adapter
when (getMode()) {
@@ -171,6 +183,8 @@ @Suppress("DEPRECATION")
intent.extras!!.get(MODE_KEY) as Mode
}
}
+
+ private fun getReturnResults(): Boolean = intent.extras?.getBoolean(RETURN_KEY) == true
private fun locate() {
val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
@@ -326,7 +340,7 @@ R.drawable.error_search
)
)
} else {
- if (queryables.size == 1 && getMode() == Mode.MODE_SEARCH) {
+ if (queryables.size == 1 && getMode() == Mode.MODE_SEARCH && !getReturnResults()) {
adapter.click(0)
}
binding.resultsOverlay.visibility = View.GONE
@@ -334,5 +348,12 @@ binding.errorImage.visibility = View.GONE
binding.errorText.visibility = View.GONE
binding.resultsRecycler.visibility = View.VISIBLE
}
+ }
+
+ fun returnResult(stop: Stop) {
+ setResult(RESULT_OK, Intent(this, MainActivity::class.java).apply {
+ putExtra("PLACE", Place(stop))
+ })
+ finish()
}
}
diff --git a/app/src/main/res/drawable/pin.xml b/app/src/main/res/drawable/pin.xml
new file mode 100644
index 0000000000000000000000000000000000000000..372ded410e5b785c0af89e45edf50e9457981691
--- /dev/null
+++ b/app/src/main/res/drawable/pin.xml
@@ -0,0 +1,17 @@
+<!--
+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:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ 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"
+ android:fillColor="?attr/colorOnSurface"/>
+ <path
+ android:pathData="M12,3.361L12,3.361C8.67,3.361 5.977,6.054 5.977,9.384c0,1.497 0.43,2.9 1.213,4.164 0.817,1.325 1.893,2.461 2.719,3.786 0.404,0.645 0.697,1.248 1.007,1.945C11.14,19.752 11.32,20.569 12,20.569l0,0c0.68,0 0.86,-0.817 1.076,-1.291 0.318,-0.697 0.602,-1.299 1.007,-1.945 0.826,-1.316 1.902,-2.452 2.719,-3.786C17.593,12.283 18.023,10.881 18.023,9.384 18.023,6.054 15.33,3.361 12,3.361ZM12,11.75c-1.187,0 -2.151,-0.964 -2.151,-2.151 0,-1.187 0.964,-2.151 2.151,-2.151 1.187,0 2.151,0.964 2.151,2.151 0,1.187 -0.964,2.151 -2.151,2.151z"
+ android:fillColor="?attr/colorPrimary"/>
+</vector>
diff --git a/app/src/main/res/drawable/send.xml b/app/src/main/res/drawable/send.xml
index 42caa11f3d69d9e70a3b73d5f03d617c349af2cc..db190a641930863725e6135f37f7a89a37fec9c5 100644
--- a/app/src/main/res/drawable/send.xml
+++ b/app/src/main/res/drawable/send.xml
@@ -8,7 +8,7 @@ android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
- android:tint="#000000"
+ android:tint="?attr/colorOnSurface"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/layout/fragment_journey.xml b/app/src/main/res/layout/fragment_journey.xml
index 74e1360a0b536a83402693f56156f6823dbbd843..649a53bbc3018e4aaf0ede4e62a3e1d7161495d6 100644
--- a/app/src/main/res/layout/fragment_journey.xml
+++ b/app/src/main/res/layout/fragment_journey.xml
@@ -7,346 +7,378 @@ SPDX-License-Identifier: GPL-3.0-or-later
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tool="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tool:context="xyz.apiote.bimba.czwek.dashboard.ui.journey.JourneyFragment">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tool="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tool:context="xyz.apiote.bimba.czwek.dashboard.ui.journey.JourneyFragment">
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/origin_input"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_marginTop="16dp"
- android:layout_marginRight="16dp"
- android:hint="@string/origin_input_hint"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:startIconDrawable="@drawable/origin">
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/origin_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginRight="16dp"
+ android:hint="@string/origin_input_hint"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:startIconDrawable="@drawable/origin">
- <com.google.android.material.textfield.TextInputEditText
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:imeOptions="actionSearch" />
- </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/origin"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionSearch"
+ android:inputType="text" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.chip.ChipGroup
- android:id="@+id/origin_chips"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- app:layout_constraintEnd_toEndOf="@+id/origin_input"
- app:layout_constraintStart_toStartOf="@+id/origin_input"
- app:layout_constraintTop_toBottomOf="@+id/origin_input"
- tool:layout_height="48dp" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/origin_suggestions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginRight="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/origin_input">
- <!-- via
- required: false
- List of via stops to visit (only stop IDs, no coordinates allowed for now).
- maxItems: 2
+ <com.google.android.material.chip.Chip
+ android:id="@+id/origin_chip_here"
+ style="@style/Widget.Material3.Chip.Suggestion"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checkable="false"
+ android:text="@string/here"
+ app:chipIcon="@drawable/gps_black"
+ app:chipIconTint="?attr/colorOnSurface"
+ app:chipIconVisible="true" />
- viaMinimumStay
- required: false
- Optional. If not set, the default is `0,0` - no stay required.
- For each `via` stop a minimum stay duration in minutes.
+ </com.google.android.material.chip.ChipGroup>
- The value `0` signals that it's allowed to stay in the same trip.
- This enables via stays without counting a transfer and can lead
- to better connections with less transfers. Transfer connections can
- still be found with `viaMinimumStay=0`.
- default: [ 0, 0 ]
- type: array
- maxItems: 2
- -->
+ <!-- via
+ required: false
+ List of via stops to visit (only stop IDs, no coordinates allowed for now).
+ maxItems: 2
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/destination_input"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_marginTop="16dp"
- android:layout_marginRight="16dp"
- android:hint="@string/destination_input_hint"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/origin_chips"
- app:startIconDrawable="@drawable/destination">
+ viaMinimumStay
+ required: false
+ Optional. If not set, the default is `0,0` - no stay required.
+ For each `via` stop a minimum stay duration in minutes.
- <com.google.android.material.textfield.TextInputEditText
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:imeOptions="actionSearch" />
- </com.google.android.material.textfield.TextInputLayout>
+ The value `0` signals that it's allowed to stay in the same trip.
+ This enables via stays without counting a transfer and can lead
+ to better connections with less transfers. Transfer connections can
+ still be found with `viaMinimumStay=0`.
+ default: [ 0, 0 ]
+ type: array
+ maxItems: 2
+ -->
- <com.google.android.material.chip.ChipGroup
- android:id="@+id/destination_chips"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- app:layout_constraintEnd_toEndOf="@+id/destination_input"
- app:layout_constraintStart_toStartOf="@+id/destination_input"
- app:layout_constraintTop_toBottomOf="@+id/destination_input"
- tool:layout_height="48dp" />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/destination_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginRight="16dp"
+ android:hint="@string/destination_input_hint"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/origin_suggestions"
+ app:startIconDrawable="@drawable/destination">
- <com.google.android.material.divider.MaterialDivider
- android:id="@+id/materialDivider"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- app:dividerInsetEnd="16dp"
- app:dividerInsetStart="16dp"
- app:layout_constraintTop_toBottomOf="@id/destination_chips" />
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/destination"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionSearch"
+ android:inputType="text" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.chip.ChipGroup
- android:id="@+id/chips_params_time"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/materialDivider">
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/destination_suggestions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginRight="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/destination_input">
- <!-- on click menu: https://stackoverflow.com/a/67764469 -->
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_time_reference"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/depart_after"
- app:checkedIconEnabled="false"
- app:closeIcon="@drawable/dropdown"
- tool:ignore="MissingConstraints"
- app:closeIconEnabled="true" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/destination_chip_here"
+ style="@style/Widget.Material3.Chip.Suggestion"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checkable="false"
+ android:text="@string/here"
+ app:chipIcon="@drawable/gps_black"
+ app:chipIconTint="?attr/colorOnSurface"
+ app:chipIconVisible="true" />
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_date"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/today"
- app:checkedIconEnabled="false"
- app:closeIcon="@drawable/dropdown"
- app:closeIconEnabled="true"
- tool:ignore="MissingConstraints" />
+ </com.google.android.material.chip.ChipGroup>
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_time"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/now"
- app:checkedIconEnabled="false"
- app:closeIcon="@drawable/dropdown"
- tool:ignore="MissingConstraints"
- app:closeIconEnabled="true" />
+ <com.google.android.material.divider.MaterialDivider
+ android:id="@+id/materialDivider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ app:dividerInsetEnd="16dp"
+ app:dividerInsetStart="16dp"
+ app:layout_constraintTop_toBottomOf="@id/destination_suggestions" />
- </com.google.android.material.chip.ChipGroup>
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chips_params_time"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/materialDivider">
- <com.google.android.material.chip.ChipGroup
- android:id="@+id/chips_params_accessible"
- android:layout_width="match_parent"
- android:layout_marginTop="8dp"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/chips_params_time"
- android:layout_height="wrap_content">
+ <!-- on click menu: https://stackoverflow.com/a/67764469 -->
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_time_reference"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/depart_after"
+ app:checkedIconEnabled="false"
+ app:closeIcon="@drawable/dropdown"
+ app:closeIconEnabled="true"
+ tool:ignore="MissingConstraints" />
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_wheelchair"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- app:chipIcon="@drawable/wheelchair"
- app:chipIconEnabled="true"
- android:text="@string/wheelchair_accessible"
- tool:ignore="MissingConstraints" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_date"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/today"
+ app:checkedIconEnabled="false"
+ app:closeIcon="@drawable/dropdown"
+ app:closeIconEnabled="true"
+ tool:ignore="MissingConstraints" />
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_bike"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- app:chipIcon="@drawable/bike"
- app:chipIconEnabled="true"
- android:text="@string/bike_transport"
- tool:ignore="MissingConstraints" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_time"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/now"
+ app:checkedIconEnabled="false"
+ app:closeIcon="@drawable/dropdown"
+ app:closeIconEnabled="true"
+ tool:ignore="MissingConstraints" />
- </com.google.android.material.chip.ChipGroup>
+ </com.google.android.material.chip.ChipGroup>
- <Button
- android:id="@+id/elevatedButton"
- style="@style/Widget.Material3.Button.Icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="16dp"
- app:icon="@drawable/journey_outline"
- android:text="@string/go"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chips_params_accessible"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/chips_params_time">
- <!--
- - name: maxTransfers
- required: false
- The maximum number of allowed transfers.
- If not provided, the routing uses the server-side default value
- which is hardcoded and very high to cover all use cases.
- schema:
- type: integer
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_wheelchair"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/wheelchair_accessible"
+ app:chipIcon="@drawable/wheelchair"
+ app:chipIconEnabled="true"
+ tool:ignore="MissingConstraints" />
- - name: maxHours
- required: false
- The maximum travel time in hours.
- If not provided, the routing to uses the value
- hardcoded in the server which is usually quite high.
- schema:
- type: number
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_bike"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/bike_transport"
+ app:chipIcon="@drawable/bike"
+ app:chipIconEnabled="true"
+ tool:ignore="MissingConstraints" />
- - name: minTransferTime
- required: false
- Minimum transfer time for each transfer in minutes.
- schema:
- type: integer
- default: 0
+ </com.google.android.material.chip.ChipGroup>
- - name: additionalTransferTime
- required: false
- Additional transfer time reserved for each transfer in minutes.
- schema:
- type: integer
- default: 0
+ <Button
+ android:id="@+id/goButton"
+ style="@style/Widget.Material3.Button.Icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/go"
+ app:icon="@drawable/journey_outline"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
- - name: transferTimeFactor
- required: false
- Factor to multiply minimum required transfer times with.
- Values smaller than 1.0 are not supported.
- schema:
- type: number
- default: 1.0
+ <!--
+ - name: maxTransfers
+ required: false
+ The maximum number of allowed transfers.
+ If not provided, the routing uses the server-side default value
+ which is hardcoded and very high to cover all use cases.
+ schema:
+ type: integer
- - name: maxMatchingDistance
- required: true
- Maximum matching distance in meters to match geo coordinates to the street network.
- schema:
- type: number
- default: 25
+ - name: maxHours
+ required: false
+ The maximum travel time in hours.
+ If not provided, the routing to uses the value
+ hardcoded in the server which is usually quite high.
+ schema:
+ type: number
- - name: transitModes
- required: false
- Optional. Default is `TRANSIT` which allows all transit modes (no restriction).
- Allowed modes for the transit part. If empty, no transit connections will be computed.
- For example, this can be used to allow only `METRO,SUBWAY,TRAM`.
- schema:
- default:
- - TRANSIT
- type: array
+ - name: minTransferTime
+ required: false
+ Minimum transfer time for each transfer in minutes.
+ schema:
+ type: integer
+ default: 0
- - name: directModes
- required: false
- Optional. Default is `WALK` which will compute walking routes as direct connections.
- Modes used for direction connections from start to destination without using transit.
- Results will be returned on the `direct` key.
- Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted.
- Note: Transit connections that are slower than the fastest direct connection will not show up.
- This is being used as a cut-off during transit routing to speed up the search.
- To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`).
- Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used.
- schema:
- default:
- - WALK
- type: array
+ - name: additionalTransferTime
+ required: false
+ Additional transfer time reserved for each transfer in minutes.
+ schema:
+ type: integer
+ default: 0
- - name: preTransitModes
- required: false
- Optional. Default is `WALK`. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`).
- A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`.
- schema:
- default:
- - WALK
- type: array
+ - name: transferTimeFactor
+ required: false
+ Factor to multiply minimum required transfer times with.
+ Values smaller than 1.0 are not supported.
+ schema:
+ type: number
+ default: 1.0
- - name: postTransitModes
- required: false
- Optional. Default is `WALK`. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`).
- A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.
- schema:
- default:
- - WALK
- type: array
+ - name: maxMatchingDistance
+ required: true
+ Maximum matching distance in meters to match geo coordinates to the street network.
+ schema:
+ type: number
+ default: 25
- - name: numItineraries
- required: false
- The minimum number of itineraries to compute.
- This is only relevant if `timetableView=true`.
- schema:
- type: integer
- default: 5
+ - name: transitModes
+ required: false
+ Optional. Default is `TRANSIT` which allows all transit modes (no restriction).
+ Allowed modes for the transit part. If empty, no transit connections will be computed.
+ For example, this can be used to allow only `METRO,SUBWAY,TRAM`.
+ schema:
+ default:
+ - TRANSIT
+ type: array
- - name: pageCursor
- required: false
- Use the cursor to go to the next "page" of itineraries.
- Copy the cursor from the last response and keep the original request as is.
- This will enable you to search for itineraries in the next or previous time-window.
+ - name: directModes
+ required: false
+ Optional. Default is `WALK` which will compute walking routes as direct connections.
+ Modes used for direction connections from start to destination without using transit.
+ Results will be returned on the `direct` key.
+ Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted.
+ Note: Transit connections that are slower than the fastest direct connection will not show up.
+ This is being used as a cut-off during transit routing to speed up the search.
+ To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`).
+ Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used.
+ schema:
+ default:
+ - WALK
+ type: array
- - name: timetableView
- required: false
- Search for the best trip options within a time window.
- If true two itineraries are considered optimal
- if one is better on arrival time (earliest wins)
- and the other is better on departure time (latest wins).
- In combination with arriveBy this parameter cover the following use cases:
- `timetable=false` = waiting for the first transit departure/arrival is considered travel time:
- - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am,
- compute the best journeys that arrive by that time (maximizes departure time)
- - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am,
- compute the best journeys that depart after that time
- `timetable=true` = optimize "later departure" + "earlier arrival" and give all options over a time window:
- - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window
- - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window
- schema:
- type: boolean
- default: true
+ - name: preTransitModes
+ required: false
+ Optional. Default is `WALK`. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`).
+ A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`.
+ schema:
+ default:
+ - WALK
+ type: array
- - name: searchWindow
- required: false
- Optional. Default is 2 hours which is `7200`.
- The length of the search-window in seconds. Default value two hours.
- - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time
- - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time
- schema:
- type: integer
- default: 7200
- minium: 0
+ - name: postTransitModes
+ required: false
+ Optional. Default is `WALK`. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`).
+ A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.
+ schema:
+ default:
+ - WALK
+ type: array
- - name: maxPreTransitTime
- required: false
- Maximum time in seconds for the first street leg.
- schema:
- type: integer
- default: 900
- minimum: 0
+ - name: numItineraries
+ required: false
+ The minimum number of itineraries to compute.
+ This is only relevant if `timetableView=true`.
+ schema:
+ type: integer
+ default: 5
- - name: maxPostTransitTime
- required: false
- Maximum time in seconds for the last street leg.
- schema:
- type: integer
- default: 900
- minimum: 0
+ - name: pageCursor
+ required: false
+ Use the cursor to go to the next "page" of itineraries.
+ Copy the cursor from the last response and keep the original request as is.
+ This will enable you to search for itineraries in the next or previous time-window.
- - name: maxDirectTime
- required: false
- Maximum time in seconds for direct connections.
- schema:
- type: integer
- default: 1800
- minimum: 0
- -->
+ - name: timetableView
+ required: false
+ Search for the best trip options within a time window.
+ If true two itineraries are considered optimal
+ if one is better on arrival time (earliest wins)
+ and the other is better on departure time (latest wins).
+ In combination with arriveBy this parameter cover the following use cases:
+ `timetable=false` = waiting for the first transit departure/arrival is considered travel time:
+ - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am,
+ compute the best journeys that arrive by that time (maximizes departure time)
+ - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am,
+ compute the best journeys that depart after that time
+ `timetable=true` = optimize "later departure" + "earlier arrival" and give all options over a time window:
+ - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window
+ - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window
+ schema:
+ type: boolean
+ default: true
+
+ - name: searchWindow
+ required: false
+ Optional. Default is 2 hours which is `7200`.
+ The length of the search-window in seconds. Default value two hours.
+ - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time
+ - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time
+ schema:
+ type: integer
+ default: 7200
+ minium: 0
+
+ - name: maxPreTransitTime
+ required: false
+ Maximum time in seconds for the first street leg.
+ schema:
+ type: integer
+ default: 900
+ minimum: 0
+
+ - name: maxPostTransitTime
+ required: false
+ Maximum time in seconds for the last street leg.
+ schema:
+ type: integer
+ default: 900
+ minimum: 0
+
+ - name: maxDirectTime
+ required: false
+ Maximum time in seconds for direct connections.
+ schema:
+ type: integer
+ default: 1800
+ minimum: 0
+ -->
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/place_bottom_sheet.xml b/app/src/main/res/layout/place_bottom_sheet.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8f32c1a27fd4d8f57a94510b0167f3ea8af24c4f
--- /dev/null
+++ b/app/src/main/res/layout/place_bottom_sheet.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:paddingBottom="16dp"
+ android:layout_height="match_parent"
+ xmlns:tool="http://schemas.android.com/tools">
+
+ <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+ android:id="@+id/drag_handle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/coordinates"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/drag_handle"
+ tool:text="17.151864867, 52.215648641" />
+
+ <androidx.constraintlayout.helper.widget.Flow
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ app:constraint_referenced_ids="use_as_origin,use_as_destination"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/coordinates" />
+
+ <Button
+ android:id="@+id/use_as_origin"
+ style="@style/Widget.Material3.Button.Icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/use_as_origin"
+ app:icon="@drawable/origin" />
+
+ <Button
+ android:id="@+id/use_as_destination"
+ style="@style/Widget.Material3.Button.Icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/use_as_destination"
+ app:icon="@drawable/destination" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/stop_bottom_sheet.xml b/app/src/main/res/layout/stop_bottom_sheet.xml
index 8b7419d9a9dfa5803e59a1c207e287a826656b2b..75a659e26de9b39f20a20c47da6839484a749294 100644
--- a/app/src/main/res/layout/stop_bottom_sheet.xml
+++ b/app/src/main/res/layout/stop_bottom_sheet.xml
@@ -66,4 +66,29 @@ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/departures_button" />
+ <androidx.constraintlayout.helper.widget.Flow
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ app:constraint_referenced_ids="use_as_origin,use_as_destination"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/navigation_button" />
+
+ <Button
+ android:id="@+id/use_as_origin"
+ style="@style/Widget.Material3.Button.TextButton.Icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/use_as_origin"
+ app:icon="@drawable/origin" />
+
+ <Button
+ android:id="@+id/use_as_destination"
+ style="@style/Widget.Material3.Button.TextButton.Icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/use_as_destination"
+ app:icon="@drawable/destination" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f796af4d2f8128f2c97dedefdb33ccc04e53508..6e8c3ac7e24ad1cb12e0698b034c734f6987bbbc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -309,4 +309,7 @@
<item quantity="one">%1$d stop</item>
<item quantity="other">%1$d stops</item>
</plurals>
+ <string name="use_as_origin">use as origin</string>
+ <string name="use_as_destination">use as destination</string>
+ <string name="here">here</string>
</resources>
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 4765792682fecbdd772286dfbad14fea2ee75081..77f81a9947ebe110ed6705e311104e34e5d1cc0f 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
+
+SPDX-License-Identifier: GPL-3.0-or-later
+--><resources>
<string name="error_403">Sinu sisestatud ligipääsutunnus pole õige</string>
<string name="error_404">Otsitavat ei leidu</string>
<string name="error_50x">Serveris tekkis viga. Palun proovi hiljem uuesti</string>
diff --git a/app/src/main/res/xml/journey_chip.xml b/app/src/main/res/xml/journey_chip.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0efc50369675e1c049426633e68f729fc77527cf
--- /dev/null
+++ b/app/src/main/res/xml/journey_chip.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<chip xmlns:android="http://schemas.android.com/apk/res/android"
+ android:text="-180.00, -90.00" />
\ No newline at end of file
diff --git a/fruchtfleisch/build.gradle.kts b/fruchtfleisch/build.gradle.kts
index 1cd1d02cb45b7590029e35f93a3e10b5468f1431..43fb515fc848c87a101c325f986d0cc4e2de095d 100644
--- a/fruchtfleisch/build.gradle.kts
+++ b/fruchtfleisch/build.gradle.kts
@@ -8,8 +8,8 @@ kotlin("jvm")
}
dependencies {
- testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
- testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
//implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
}