Bimba.git

commit fdd702694012095403a2006af0ca476aee59e08d

Author: Adam Evyčędo <git@apiote.xyz>

turn arrow based on heading, not bearing

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
index a08bf4c2729a751497adeee3b29c5ec6c660c12d..3d6730f9a369a48fef475076e0746fb39a9b5dcc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
@@ -97,7 +97,7 @@ 		binding.searchBar.setNavigationOnClickListener {
 			(context as MainActivity).onNavigationClicked()
 		}
 		binding.suggestionsRecycler.layoutManager = LinearLayoutManager(activity)
-		adapter = BimbaResultsAdapter(layoutInflater, activity, listOf(), null, false)
+		adapter = BimbaResultsAdapter(layoutInflater, activity, listOf(), null, null, false)
 		binding.suggestionsRecycler.adapter = adapter
 
 		binding.searchView.editText.addTextChangedListener(




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 4c94db9e8d6f4bed71759e0240ae038ab93301bd..7ce6dc062432c68e176a752e609cc2b966dbef52 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
@@ -6,7 +6,6 @@ package xyz.apiote.bimba.czwek.search
 
 import android.content.Context
 import android.content.Intent
-import android.graphics.Matrix
 import android.location.Location
 import android.view.LayoutInflater
 import android.view.View
@@ -23,6 +22,9 @@ import xyz.apiote.bimba.czwek.repo.Queryable
 import xyz.apiote.bimba.czwek.repo.Stop
 import xyz.apiote.bimba.czwek.repo.StopStub
 import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
+import xyz.apiote.bimba.czwek.units.Metre
+import xyz.apiote.bimba.czwek.units.UnitSystem
+import kotlin.math.abs
 
 class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
 	val root: View = itemView.findViewById(R.id.suggestion)
@@ -42,10 +44,21 @@ 			feeds: Map?,
 			feedsSettings: FeedsSettings?,
 			onClickListener: (Queryable) -> Unit,
 			position: Location?,
+			heading: Float?,
 			showArrow: Boolean
 		) {
 			when (queryable) {
-				is Stop -> bindStop(queryable, holder, context, feeds, feedsSettings, position, showArrow)
+				is Stop -> bindStop(
+					queryable,
+					holder,
+					context,
+					feeds,
+					feedsSettings,
+					position,
+					heading,
+					showArrow
+				)
+
 				is Line -> bindLine(queryable, holder, context, feeds, feedsSettings)
 			}
 			holder?.root?.setOnClickListener {
@@ -90,27 +103,29 @@ 			context: Context?,
 			feeds: Map<String, FeedInfo>?,
 			feedsSettings: FeedsSettings?,
 			position: Location?,
+			heading: Float?,
 			showArrow: Boolean
 		) {
 
-			if (showArrow && position != null && position.hasBearing()) {
+			if (showArrow && position != null && heading != null) {
 				Location(null).apply {
 					latitude = stop.position.latitude
 					longitude = stop.position.longitude
 				}.let {
-					val rotation =
-						(360 + ((position.bearingTo(it) + 360).mod(360f)) - position.bearing).mod(360f)
+					val angle =
+						(360 + ((position.bearingTo(it) + 360).mod(360f)) - heading).mod(360f)
 					val distance = position.distanceTo(it)
 					holder?.arrow?.apply {
 						setImageResource(R.drawable.arrow)
-						scaleType = ImageView.ScaleType.MATRIX
-						val matrix = Matrix()
-						matrix.postRotate(rotation, width.toFloat() / 2, height.toFloat() / 2)
-						imageMatrix = matrix
+						rotation = angle
 						visibility = View.VISIBLE
+						contentDescription = "Arrow" // TODO
 					}
 					holder?.distance?.apply {
-						text = "$distance m" // TODO units
+						val us = UnitSystem.getSelected(context!!)
+						text = us.toString(context, us.distanceUnit(Metre(distance.toDouble())))
+						contentDescription =
+							us.distanceUnit(Metre(distance.toDouble())).contentDescription(context, us.base)
 						visibility = View.VISIBLE
 					}
 				}
@@ -185,6 +200,7 @@ 	private val inflater: LayoutInflater,
 	private val context: Context?,
 	private var queryables: List<Queryable>,
 	private var position: Location?,
+	private var heading: Float?,
 	private var showArrow: Boolean
 ) :
 	RecyclerView.Adapter<BimbaViewHolder>() {
@@ -193,6 +209,8 @@ 		private val oldQueryables: List,
 		private val newQueryables: List<Queryable>,
 		private val oldPosition: Location?,
 		private val newPosition: Location?,
+		private val oldHeading: Float?,
+		private val newHeading: Float?,
 		private val oldShowArrow: Boolean,
 		private val newShowArrow: Boolean
 	) : DiffUtil.Callback() {
@@ -243,7 +261,11 @@ 					val oldChangeOptions =
 						oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" }
 					val newChangeOptions =
 						(newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" }
-					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions && oldPosition == newPosition && oldShowArrow == newShowArrow
+					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions &&
+						oldPosition?.latitude == newPosition?.latitude &&
+						oldPosition?.longitude == newPosition?.longitude &&
+						oldHeading == newHeading &&
+						oldShowArrow == newShowArrow
 				}
 
 				else -> false // XXX unreachable
@@ -290,27 +312,80 @@ 			feeds,
 			feedsSettings,
 			onClickListener,
 			this.position,
+			heading,
 			showArrow
 		)
 	}
 
 	override fun getItemCount(): Int = queryables.size
 
-	fun update(queryables: List<Queryable>?, position: Location?, showArrow: Boolean) {
-		val newQueryables = queryables ?: emptyList()
+	fun update(
+		queryables: List<Queryable>?,
+		position: Location?,
+		heading: Float?,
+		showArrow: Boolean
+	) {
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				this.queryables,
+				queryables ?: emptyList(),
+				this.position,
+				position,
+				this.heading,
+				heading,
+				this.showArrow,
+				showArrow
+			)
+		)
+		this.position = position
+		this.heading = heading
+		this.showArrow = showArrow
+		this.queryables = queryables ?: emptyList()
+		diff.dispatchUpdatesTo(this)
+	}
+
+	fun update(
+		heading: Float?,
+	) {
+		if (abs((heading ?: 0f) - (this.heading ?: 0f)) < 15) {
+			return
+		}
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				queryables,
+				queryables,
+				position,
+				position,
+				this.heading,
+				heading,
+				showArrow,
+				showArrow
+			)
+		)
+		this.heading = heading
+		diff.dispatchUpdatesTo(this)
+	}
+
+	fun update(
+		queryables: List<Queryable>?,
+		position: Location?,
+		showArrow: Boolean
+	) {
 		val diff = DiffUtil.calculateDiff(
 			DiffUtilCallback(
 				this.queryables,
-				newQueryables,
+				queryables ?: emptyList(),
 				this.position,
 				position,
+				heading,
+				heading,
 				this.showArrow,
 				showArrow
 			)
 		)
 		this.position = position
 		this.showArrow = showArrow
-		this.queryables = newQueryables
+		this.queryables = queryables ?: emptyList()
 		diff.dispatchUpdatesTo(this)
 	}
 




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 066d95276405023a5d7f868e9c92f4c49c6c6886..b6790e7d43cb8f79a365e3bb9b3fa4d5f7bf7e25 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
@@ -5,6 +5,10 @@
 package xyz.apiote.bimba.czwek.search
 
 import android.content.Context
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
 import android.location.Location
 import android.location.LocationListener
 import android.location.LocationManager
@@ -36,7 +40,7 @@ import xyz.apiote.bimba.czwek.repo.Queryable
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
 
-class ResultsActivity : AppCompatActivity(), LocationListener {
+class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener {
 	enum class Mode {
 		MODE_LOCATION, MODE_SEARCH, MODE_POSITION
 	}
@@ -48,6 +52,8 @@ 	private lateinit var adapter: BimbaResultsAdapter
 
 	private val handler = Handler(Looper.getMainLooper())
 	private var runnable = Runnable {}
+	private var gravity: FloatArray? = null
+	private var geomagnetic: FloatArray? = null
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		enableEdgeToEdge()
@@ -73,7 +79,7 @@ 			windowInsets
 		}
 
 		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
-		adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, false)
+		adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false)
 		binding.resultsRecycler.adapter = adapter
 
 		when (getMode()) {
@@ -111,6 +117,12 @@ 		}
 	}
 
 	private fun locate() {
+		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
+		val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+		val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
+		sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
+		sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL)
+
 		try {
 			val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 			locationManager.requestLocationUpdates(
@@ -133,6 +145,21 @@ 		handler.removeCallbacks(runnable)
 		getQueryablesByLocation(location, this, true)
 	}
 
+	override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
+	override fun onSensorChanged(event: SensorEvent?) {
+		if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) gravity = event.values
+		if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) geomagnetic = event.values
+		if (gravity != null && geomagnetic != null) {
+			val r = FloatArray(9)
+			val success = SensorManager.getRotationMatrix(r, FloatArray(9), gravity, geomagnetic)
+			if (success) {
+				val orientation = FloatArray(3)
+				SensorManager.getOrientation(r, orientation)
+				adapter.update((orientation[0]*180/Math.PI).toFloat())
+			}
+		}
+	}
+
 	override fun onResume() {
 		super.onResume()
 		if (getMode() == Mode.MODE_LOCATION) {
@@ -145,6 +172,8 @@ 		super.onPause()
 		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 		locationManager.removeUpdates(this)
 		handler.removeCallbacks(runnable)
+		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
+		sensorManager.unregisterListener(this)
 	}
 
 	override fun onDestroy() {
@@ -187,7 +216,8 @@ 	) {
 		MainScope().launch {
 			try {
 				val repository = OnlineRepository()
-				val result = repository.locateQueryables(Position(position.latitude, position.longitude), context)
+				val result =
+					repository.locateQueryables(Position(position.latitude, position.longitude), context)
 				getFeeds()
 				updateItems(result, position, showArrow)
 			} catch (e: TrafficResponseException) {




diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml
index 6d71f2fea7c9a4c4742a87913b9f1ea7aef6cb3d..e7dd45553043a5ffad0cfceb1ed5fc71ace8b0b6 100644
--- a/app/src/main/res/layout/result.xml
+++ b/app/src/main/res/layout/result.xml
@@ -28,27 +28,28 @@ 	 		android:id="@+id/arrow"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
+		android:visibility="gone"
+		android:layout_marginStart="11dp"
+		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
 		android:importantForAccessibility="no"
-		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintEnd_toEndOf="@+id/distance"
+		app:layout_constraintStart_toStartOf="@+id/distance"
 		app:layout_constraintTop_toTopOf="parent"
-		android:layout_marginEnd="8dp"
-		android:layout_marginTop="8dp"
-		android:visibility="gone"
 		tool:src="@drawable/arrow" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/distance"
+		android:visibility="gone"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_marginEnd="4dp"
-		android:visibility="gone"
-		app:layout_constraintBottom_toBottomOf="@+id/arrow"
-		app:layout_constraintEnd_toStartOf="@+id/arrow"
-		app:layout_constraintTop_toTopOf="@+id/arrow"
+		android:layout_marginTop="4dp"
+		android:layout_marginEnd="8dp"
 		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/arrow"
 		tool:text="650m" />
 
-	<!-- todo maxWidth or separate layout for graphView -->
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_title"
 		android:maxWidth="320dp"
@@ -71,14 +72,17 @@ 		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
 		app:layout_constraintTop_toBottomOf="@+id/suggestion_title"
 		tool:text="TfL London" />
 
+	<!-- todo maxWidth or separate layout for graphView -->
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_description"
 		style="@style/Theme.Bimba.SearchResult.Description"
-		android:layout_width="wrap_content"
+		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:maxWidth="360dp"
 		android:layout_marginTop="4dp"
+		android:layout_marginEnd="4dp"
 		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toStartOf="@+id/distance"
 		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
 		app:layout_constraintTop_toBottomOf="@+id/feed_name"
 		tool:text="Metropolitan » Baker Street, Tower Hill The Monument, Westminster, Piccadilly Circus, Oxford Street" />