Bimba.git

commit 04d851320b846fc0fd18287b524639a1ced8826d

Author: Adam <git@apiote.xyz>

[wip] search journeys

%!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
new file mode 100644
index 0000000000000000000000000000000000000000..819fae616a88170d90479a661ab858448e44f5e8
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/DashboardViewModel.kt
@@ -0,0 +1,28 @@
+// 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 xyz.apiote.bimba.czwek.repo.Place
+
+class DashboardViewModel : ViewModel() {
+
+	private val _origin = MutableLiveData<Place>()
+	val origin: LiveData<Place> = _origin
+
+	fun setOrigin(o: Place) {
+		_origin.value = o
+	}
+
+	private val _destination = MutableLiveData<Place>()
+	val destination: LiveData<Place> = _destination
+
+	fun setDestination(d: Place) {
+		_destination.value = d
+	}
+
+}




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..b9a79811cceff3ef625fb7b1554a794a9c15e77a 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,44 @@ // 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.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
 import android.os.Bundle
+import android.text.Spanned
+import android.text.style.ImageSpan
+import android.util.Log
 import android.view.LayoutInflater
+import android.view.MotionEvent.ACTION_UP
 import android.view.View
 import android.view.ViewGroup
+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.Chip
+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.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
 
-class JourneyFragment : Fragment() {
+class JourneyFragment : Fragment(), LocationListener {
 
 	private var _binding: FragmentJourneyBinding? = null
 	private val binding get() = _binding!!
 
+	private lateinit var dashboard: MainActivity
+	private var hereChipRequester: Chip? = null
+
 	override fun onCreateView(
 		inflater: LayoutInflater,
 		container: ViewGroup?,
@@ -30,29 +50,184 @@ 	): 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
+		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()
+		dashboard.viewModel.origin.let {
+			chipifyOrigin(it.value)
+		}
+
+		dashboard.viewModel.destination.let {
+			chipifyDestination(it.value)
+		}
+
+		binding.originChipHere.setOnClickListener {
+			setHere(it as Chip)
+		}
+
+		binding.destinationChipHere.setOnClickListener {
+			setHere(it as Chip)
+		}
+
+		binding.goButton.isEnabled =
+			dashboard.viewModel.origin.value != null && dashboard.viewModel.destination.value != null
+
+		binding.goButton.setOnClickListener {
+			val intent = JourneysActivity.getIntent(
+				requireContext(),
+				dashboard.viewModel.origin.value!!,
+				dashboard.viewModel.destination.value!!
 			)
-		)
-		startActivity(intent)
+			startActivity(intent)
+		}
+
+		dashboard.viewModel.origin.observe(viewLifecycleOwner) {
+			chipifyOrigin(it)
+		}
+
+		dashboard.viewModel.destination.observe(viewLifecycleOwner) {
+			chipifyDestination(it)
+		}
+
+		// TODO listen to text changes
+		// TODO search by text input
 
 		return root
 	}
 
+	private fun chipifyOrigin(place: Place?) {
+		if (place != null) {
+			chipify(place, binding.origin)
+		}
+	}
+
+	private fun chipifyDestination(place: Place?) {
+		if (place != null) {
+			chipify(place, binding.destination)
+		}
+	}
+
+	private fun chipify(place: Place, textView: TextInputEditText) {
+		var chip: ChipDrawable? = ChipDrawable.createFromResource(requireContext(), R.xml.journey_chip)
+		val text = place.shortString()
+		textView.setText(text)
+		chip!!.text = text
+		chip.setBounds(0, 0, chip.intrinsicWidth, chip.intrinsicHeight)
+		val span = ImageSpan(chip)
+		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 + chipContentRect.right + 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(v: Chip) {
+		hereChipRequester = v
+		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) {
+		when (hereChipRequester?.id) {
+			R.id.origin_chip_here ->
+				dashboard.viewModel.setOrigin(Place(location.latitude, location.longitude))
+
+			R.id.destination_chip_here ->
+				dashboard.viewModel.setDestination(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..cd4df83f11c6a696a01e19ddba00fffaedadc2da 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,13 @@ 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.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 +60,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.setOrigin(place)
+			positionUsed = true
+			dismiss()
+		}
+		view.findViewById<Button>(R.id.use_as_destination).setOnClickListener {
+			(activity as MainActivity).viewModel.setDestination(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 +204,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 +225,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.setOrigin(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.setDestination(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..dd03572d986e0f0f4a47dcb6da195fa45bb46ee0 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,32 @@ 	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
 	)
 
 	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/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..6e5d145b67459d8320e9e653aefb401be76bd24f 100644
--- a/app/src/main/res/layout/fragment_journey.xml
+++ b/app/src/main/res/layout/fragment_journey.xml
@@ -7,346 +7,376 @@ 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" />
+	</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_marginLeft="16dp"
+		android:layout_marginTop="4dp"
+		android:layout_marginRight="16dp"
+		android:layout_width="match_parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@id/origin_input"
+		android:layout_height="wrap_content">
 
-    <!--  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"
+			app:chipIcon="@drawable/gps_black"
+			app:chipIconTint="?attr/colorOnSurface"
+			app:chipIconVisible="true"
+			android:checkable="false"
+			android:text="@string/here" />
 
-          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" />
+	</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_marginLeft="16dp"
+		android:layout_marginTop="4dp"
+		android:layout_marginRight="16dp"
+		android:layout_width="match_parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@id/destination_input"
+		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"
-            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"
+			app:chipIcon="@drawable/gps_black"
+			app:chipIconTint="?attr/colorOnSurface"
+			app:chipIconVisible="true"
+			android:checkable="false"
+			android:text="@string/here" />
 
-        <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: 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: maxMatchingDistance
+				required: true
+					Maximum matching distance in meters to match geo coordinates to the street network.
+				schema:
+					type: number
+					default: 25
+
+			- 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: 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: 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: 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: 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: 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: numItineraries
-          required: false
-            The minimum number of itineraries to compute.
-            This is only relevant if `timetableView=true`.
-          schema:
-            type: integer
-            default: 5
+			- name: numItineraries
+				required: false
+					The minimum number of itineraries to compute.
+					This is only relevant if `timetableView=true`.
+				schema:
+					type: integer
+					default: 5
 
-        - 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: 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: 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: 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: 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: 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: 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
-            -->
+			- 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 0480a5fed0a434e65ffaebf0d90a926db77025fe..3826ee91e7f57a78ad8f59137d82960edabe3322 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