Bimba.git

commit a7ff4d5e0d8eb6f5f2c1b7f5f2e7bc6fe3bcf729

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

improve journey map

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


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
index 2e42c505cc370dace2a41ec273a5921782b6c697..4044d0012f2868e09b4e2eca7e321d251da1a930 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
@@ -5,6 +5,7 @@
 package xyz.apiote.bimba.czwek.journeys
 
 import android.content.Context
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -14,6 +15,7 @@ import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.google.android.material.card.MaterialCardView
+import kotlinx.serialization.descriptors.PrimitiveKind
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.repo.Journey
 import xyz.apiote.bimba.czwek.units.Metre
@@ -30,18 +32,15 @@
 	companion object {
 		fun bind(
 			holder: JourneysViewHolder,
-			onClickListener: (Journey) -> Unit,
+			onClickListener: (Journey, Int) -> Unit,
 			journey: Journey,
 			context: Context,
-			inflater: LayoutInflater
+			inflater: LayoutInflater,
+			isOpen: Boolean,
+			position: Int
 		) {
 			holder.root.setOnClickListener {
-				if (holder.legs.visibility == View.GONE) {
-					holder.legs.visibility = View.VISIBLE
-				} else {
-					holder.legs.visibility = View.GONE
-				}
-				onClickListener(journey)
+				onClickListener(journey, position)
 			}
 			holder.startTime.text =
 				context.getString(R.string.time, journey.startTime.hour, journey.startTime.minute)
@@ -90,7 +89,11 @@ 				distance.text = if (it.start.vehicle.Line.name.isBlank() && it.distance != null) {
 					val us = UnitSystem.getSelected(context)
 					us.toString(context, it.distance)
 				} else {
-					context.resources.getQuantityString(R.plurals.number_stops, it.intermediateStops.size+1, it.intermediateStops.size+1)
+					context.resources.getQuantityString(
+						R.plurals.number_stops,
+						it.intermediateStops.size + 1,
+						it.intermediateStops.size + 1
+					)
 				}
 
 				val legDestination = legView.findViewById<TextView>(R.id.leg_destination)
@@ -105,10 +108,15 @@ 				}
 
 				val legDestinationTime = legView.findViewById<TextView>(R.id.leg_destination_time)
 				legDestinationTime.text =
-					context.getString(R.string.time, it.end.arrivalTime!!.Hour.toInt(), it.end.arrivalTime.Minute.toInt())
+					context.getString(
+						R.string.time,
+						it.end.arrivalTime!!.Hour.toInt(),
+						it.end.arrivalTime.Minute.toInt()
+					)
 
 				holder.legs.addView(legView)
 			}
+			holder.legs.visibility = if (isOpen) View.VISIBLE else View.GONE
 		}
 	}
 }
@@ -117,9 +125,21 @@ class JourneysAdapter(
 	private val inflater: LayoutInflater,
 	private val context: Context,
 	private var items: List<Journey>,
-	private val onClickListener: ((Journey) -> Unit),
+	private val onClickListener: ((Journey, Boolean) -> Unit),
 ) :
 	RecyclerView.Adapter<JourneysViewHolder>() {
+	var openCard: Int = -1
+
+	val onClickListener2: ((Journey, Int) -> Unit) = { journey, position ->
+		Log.i("Journey", "open: $openCard, clicked: $position")
+		val previouslyOpen = openCard
+		openCard = if (position == openCard) -1 else position
+		Log.i("Journey", "open: $openCard, notifying: $previouslyOpen, $position, hide: ${openCard == -1}")
+		notifyItemChanged(previouslyOpen)
+		notifyItemChanged(position)
+		onClickListener(journey, openCard == -1)
+	}
+
 	override fun onCreateViewHolder(
 		parent: ViewGroup,
 		viewType: Int
@@ -132,7 +152,15 @@ 	override fun onBindViewHolder(
 		holder: JourneysViewHolder,
 		position: Int
 	) {
-		JourneysViewHolder.bind(holder, onClickListener, items[position], context, inflater)
+		JourneysViewHolder.bind(
+			holder,
+			onClickListener2,
+			items[position],
+			context,
+			inflater,
+			openCard == position,
+			position
+		)
 	}
 
 	override fun getItemCount(): Int = items.size




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysActivity.kt
index df585e19e87fbc83d7dac59de1f5af37351ec5e5..db4beeaad871f5e4bb57fdd0bb349199f2cc36cf 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysActivity.kt
@@ -32,8 +32,10 @@ import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.databinding.ActivityJourneysBinding
 import xyz.apiote.bimba.czwek.dpToPixelI
+import xyz.apiote.bimba.czwek.repo.Colour
 import xyz.apiote.bimba.czwek.repo.JourneyParams
 import xyz.apiote.bimba.czwek.repo.Place
+import xyz.apiote.bimba.czwek.repo.Position
 import kotlin.math.max
 import kotlin.math.min
 
@@ -46,10 +48,7 @@ 		const val DESTINATION_PARAM = "destination"
 		const val PARAMS_PARAM = "params"
 
 		fun getIntent(
-			context: Context,
-			origin: Place,
-			destination: Place,
-			params: JourneyParams
+			context: Context, origin: Place, destination: Place, params: JourneyParams
 		) = Intent(context, JourneysActivity::class.java).apply {
 			putExtra(ORIGIN_PARAM, origin)
 			putExtra(DESTINATION_PARAM, destination)
@@ -60,31 +59,28 @@
 	private fun getOrigin(): Place = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
 		intent.getParcelableExtra(ORIGIN_PARAM, Place::class.java)
 	} else {
-		@Suppress("DEPRECATION")
-		intent.getParcelableExtra(ORIGIN_PARAM)
+		@Suppress("DEPRECATION") intent.getParcelableExtra(ORIGIN_PARAM)
 	} ?: throw Exception("Origin not given")
 
 	private fun getDestination(): Place = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
 		intent.getParcelableExtra(DESTINATION_PARAM, Place::class.java)
 	} else {
-		@Suppress("DEPRECATION")
-		intent.getParcelableExtra(DESTINATION_PARAM)
+		@Suppress("DEPRECATION") intent.getParcelableExtra(DESTINATION_PARAM)
 	} ?: throw Exception("Destination not given")
 
-	private fun getJourneyParams(): JourneyParams = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
-		intent.getParcelableExtra(PARAMS_PARAM, JourneyParams::class.java)
-	} else {
-		@Suppress("DEPRECATION")
-		intent.getParcelableExtra(PARAMS_PARAM)
-	} ?: throw Exception("Params not given")
+	private fun getJourneyParams(): JourneyParams =
+		if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
+			intent.getParcelableExtra(PARAMS_PARAM, JourneyParams::class.java)
+		} else {
+			@Suppress("DEPRECATION") intent.getParcelableExtra(PARAMS_PARAM)
+		} ?: throw Exception("Params not given")
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		enableEdgeToEdge()
 		super.onCreate(savedInstanceState)
 		binding = ActivityJourneysBinding.inflate(layoutInflater)
 		setContentView(binding.root)
-		val journeysViewModel =
-			ViewModelProvider(this)[JourneysViewModel::class.java]
+		val journeysViewModel = ViewModelProvider(this)[JourneysViewModel::class.java]
 
 		// TODO check upside-down
 		// TODO for horizontal make side sheet
@@ -95,8 +91,8 @@ 			WindowInsetsCompat.CONSUMED
 		}
 
 		binding.map.setTileSource(TileSourceFactory.MAPNIK)
-		if (((resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
-				and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
+		if (((resources?.configuration?.uiMode
+				?: UI_MODE_NIGHT_UNDEFINED) and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
 		) {
 			binding.map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
 		}
@@ -113,63 +109,63 @@ 		val destination = getDestination()
 		val params = getJourneyParams()
 
 		journeysViewModel.journeys.observe(this) {
-			zoomMap(100)
+			zoomMap(dpToPixelI(100f))
 			binding.journeysProgress.visibility = View.GONE
 			if (it.isEmpty()) {
 				binding.emptyText.visibility = View.VISIBLE
 				binding.emptyImage.visibility = View.VISIBLE
 			} else {
+				showMarkers(origin.position(), destination.position())
+
 				binding.journeys.visibility = View.VISIBLE
-				binding.journeys.adapter = JourneysAdapter(layoutInflater, this, it) {
+				binding.journeys.adapter = JourneysAdapter(layoutInflater, this, it) { journey, hide ->
 					binding.map.overlays.removeAll { true }
 
-					// TODO show depending on open/close card
-					// TODO close other cards
-
-					// TODO show without journey
-					val originMarker = Marker(binding.map).apply {
-						position = GeoPoint(it.legs[0].origin.latitude, it.legs[0].origin.longitude)
-						setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
-						icon = AppCompatResources.getDrawable(
-							this@JourneysActivity,
-							R.drawable.pin // TODO R.drawable.legOrigin
-						) // TODO colour
-						setOnMarkerClickListener { marker, map ->
-							true
-						}
+					if (hide) {
+						showMarkers(origin.position(), destination.position())
+						binding.map.invalidate()
+						return@JourneysAdapter
 					}
-					binding.map.overlays.add(originMarker)
 
-					// TODO show without journey
-					val destinationMarker = Marker(binding.map).apply {
-						position = GeoPoint(it.legs.last().destination.latitude, it.legs.last().destination.longitude)
-						setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
-						icon = AppCompatResources.getDrawable(
-							this@JourneysActivity,
-							R.drawable.pin // TODO R.drawable.legDestination
-						) // TODO colour
-						setOnMarkerClickListener { marker, map ->
-							true
-						}
-					}
-					binding.map.overlays.add(destinationMarker)
+					showMarkers(journey.legs[0].origin.position(), journey.legs[0].origin.position())
+					showMarkers(
+						journey.legs.last().destination.position(), journey.legs.last().destination.position()
+					)
 
-					it.legs.forEachIndexed { i, leg ->
+					journey.legs.forEachIndexed { i, leg ->
 						val shapePoints = leg.shape.map {
 							GeoPoint(it.latitude, it.longitude)
 						}
+						val contrastShape = Polyline()
+						val contrastPaint = contrastShape.outlinePaint
+						contrastPaint.color =
+							Colour.getThemeColour(com.google.android.material.R.attr.colorOnBackground, this)
+						contrastPaint.strokeWidth = contrastPaint.strokeWidth * 1.5f
+						contrastShape.setPoints(shapePoints)
+						binding.map.overlays.add(contrastShape)
+
 						val shape = Polyline()
 						val paint = shape.outlinePaint
-						paint.color = leg.start.vehicle.Line.colour.toInt() // TODO contrast
+						paint.color = leg.start.vehicle.Line.colour.toInt()
 						if (leg.start.vehicle.Line.kind.isActive()) {
 							paint.setPathEffect(DashPathEffect(floatArrayOf(10f, 10f), 0f))
+							paint.color = Colour.getThemeColour(
+								com.google.android.material.R.attr.colorSurfaceContainer, this
+							)
 						}
 						shape.setPoints(shapePoints)
-						shape.isVisible = true
 						binding.map.overlays.add(shape)
 					}
 					binding.map.invalidate()
-					// todo move map accordingly
+					zoomMap(
+						dpToPixelI(100f),
+						BoundingBox(
+							max(journey.legs[0].origin.latitude, journey.legs.last().destination.latitude),
+							max(journey.legs[0].origin.longitude, journey.legs.last().destination.longitude),
+							min(journey.legs[0].origin.latitude, journey.legs.last().destination.latitude),
+							min(journey.legs[0].origin.longitude, journey.legs.last().destination.longitude),
+						)
+					)
 				}
 			}
 		}
@@ -184,13 +180,38 @@
 	fun zoomMap(margin: Int = 0, box: BoundingBox? = null) {
 		val origin = getOrigin()
 		val destination = getDestination()
-		// TODO offset bottom sheet
 		val bb = box ?: BoundingBox(
 			max(origin.latitude, destination.latitude),
 			max(origin.longitude, destination.longitude),
-			min(origin.latitude, destination.latitude),
+			min(origin.latitude, destination.latitude), // TODO offset bottom sheet
 			min(origin.longitude, destination.longitude),
 		)
 		binding.map.zoomToBoundingBox(bb, false, margin)
+	}
+
+	fun showMarkers(origin: Position, destination: Position) {
+		val originMarker = Marker(binding.map).apply {
+			position = GeoPoint(origin.latitude, origin.longitude)
+			setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
+			icon = AppCompatResources.getDrawable(
+				this@JourneysActivity, R.drawable.pin // TODO R.drawable.legOrigin
+			)
+			setOnMarkerClickListener { marker, map ->
+				true
+			}
+		}
+		binding.map.overlays.add(originMarker)
+
+		val destinationMarker = Marker(binding.map).apply {
+			position = GeoPoint(destination.latitude, destination.longitude)
+			setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
+			icon = AppCompatResources.getDrawable(
+				this@JourneysActivity, R.drawable.pin // TODO R.drawable.legDestination
+			)
+			setOnMarkerClickListener { marker, map ->
+				true
+			}
+		}
+		binding.map.overlays.add(destinationMarker)
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
index 5b33a56f8235911686e8e41586e7fcac87c39a40..24d3cdd91d221e201e1e3ce228981dfb7ca29983 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
@@ -4,8 +4,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.repo
 
+import android.content.Context
+import android.content.res.TypedArray
 import android.os.Parcelable
 import kotlinx.parcelize.Parcelize
+import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.ColourV1
 
 @Parcelize
@@ -33,6 +36,15 @@ 					val b = it.substring(4 until 6).toUByte(16)
 					Colour(r, g, b)
 				}
 			}
+		}
+
+		fun getThemeColour(resource: Int, context: Context): Int {
+			val a: TypedArray = context.theme.obtainStyledAttributes(
+				R.style.Theme_Bimba, intArrayOf(resource)
+			)
+			val intColor = a.getColor(0, 0)
+			a.recycle()
+			return intColor
 		}
 	}
 }
\ No newline at end of file




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 116985190dd65e64ffc9a0f26c3d3652a5cfbe40..b12a8c50db61de66887a5322dbde71871070a92e 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
@@ -45,6 +45,10 @@ 	)
 
 	constructor(stop: Stop) : this(stop, stop.position.latitude, stop.position.longitude)
 
+	fun position(): Position {
+		return Position(latitude, longitude)
+	}
+
 	fun planString(): String = if (stop.code == "") {
 		"${latitude},${longitude},0"
 	} else {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt
index ffb32d9486fbf7056c3629add6fdf2d58550898e..6cfe3be6d45ada1868d0abdd9b635815c77a2719 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt
@@ -26,6 +26,7 @@ import dev.bandb.graphview.layouts.layered.SugiyamaLayoutManager
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.databinding.FragmentLineGraphBinding
 import xyz.apiote.bimba.czwek.departures.DeparturesActivity
+import xyz.apiote.bimba.czwek.repo.Colour
 import xyz.apiote.bimba.czwek.repo.LineGraph
 import xyz.apiote.bimba.czwek.repo.StopStub
 import xyz.apiote.bimba.czwek.search.BimbaViewHolder
@@ -57,7 +58,12 @@ 		_binding = FragmentLineGraphBinding.inflate(inflater, container, false)
 
 		ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, windowInsets ->
 			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
-			v.updatePadding(right = insets.right, left = insets.left, top = insets.top, bottom = insets.bottom)
+			v.updatePadding(
+				right = insets.right,
+				left = insets.left,
+				top = insets.top,
+				bottom = insets.bottom
+			)
 			windowInsets
 		}
 
@@ -68,12 +74,9 @@
 		binding.recycler.layoutManager = SugiyamaLayoutManager(requireContext(), configuration)
 		binding.recycler.addItemDecoration(SugiyamaArrowEdgeDecoration(Paint(Paint.ANTI_ALIAS_FLAG).apply {
 			strokeWidth = 5f
-			val a: TypedArray? = context?.theme?.obtainStyledAttributes(
-				R.style.Theme_Bimba, intArrayOf(com.google.android.material.R.attr.colorOnBackground)
-			)
-			val intColor = a?.getColor(0, 0)
-			a?.recycle()
-			color = intColor ?: 0
+			color = context?.let {
+				Colour.getThemeColour(com.google.android.material.R.attr.colorOnBackground, it)
+			} ?: 0
 			style = Paint.Style.STROKE
 			strokeJoin = Paint.Join.ROUND
 			pathEffect = CornerPathEffect(10f)




diff --git a/app/src/main/res/layout/activity_journeys.xml b/app/src/main/res/layout/activity_journeys.xml
index c1ab83ad33ed4df004b224065382d003a5a0a836..b1124d955dc98e2b9d1bdcc66051f11bb3b59701 100644
--- a/app/src/main/res/layout/activity_journeys.xml
+++ b/app/src/main/res/layout/activity_journeys.xml
@@ -12,10 +12,9 @@ 	tools:context=".journeys.JourneysActivity">
 
 	<org.osmdroid.views.MapView
 		android:id="@+id/map"
+		android:layout_marginBottom="200dp"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent">
-
-	</org.osmdroid.views.MapView>
+		android:layout_height="match_parent" />
 
 	<androidx.constraintlayout.widget.ConstraintLayout
 		android:id="@+id/journeys_bottom_sheet"
@@ -57,28 +56,28 @@ 		 			android:id="@+id/empty_image"
 			android:layout_width="92dp"
 			android:layout_height="92dp"
+			android:layout_marginTop="36dp"
+			android:src="@drawable/error_journeys"
 			android:visibility="gone"
-			android:layout_marginTop="36dp"
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toBottomOf="@+id/drag_handle"
-			tools:ignore="ContentDescription"
-			android:src="@drawable/error_journeys" />
+			tools:ignore="ContentDescription" />
 
 		<com.google.android.material.textview.MaterialTextView
 			android:id="@+id/empty_text"
 			android:layout_width="0dp"
 			android:layout_height="wrap_content"
 			android:layout_marginStart="16dp"
-			android:visibility="gone"
 			android:layout_marginTop="8dp"
 			android:layout_marginEnd="16dp"
+			android:text="@string/no_journeys_found"
 			android:textAlignment="center"
 			android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+			android:visibility="gone"
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
-			app:layout_constraintTop_toBottomOf="@+id/empty_image"
-			android:text="@string/no_journeys_found" />
+			app:layout_constraintTop_toBottomOf="@+id/empty_image" />
 
 	</androidx.constraintlayout.widget.ConstraintLayout>
 




diff --git a/build.gradle.kts b/build.gradle.kts
index c746a3ec056432afbc9711d8b48250689c0d939c..0e6fb37fe7f621022fb39098802726a647d6f19d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,8 +4,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 plugins {
-    id("com.android.application") version "8.7.3" apply false
-    id("com.android.library") version "8.7.3" apply false
+    id("com.android.application") version "8.8.0" apply false
+    id("com.android.library") version "8.8.0" apply false
     id("org.openapi.generator") version "7.9.0" apply false
     id("de.undercouch.download") version "5.6.0" apply false
     kotlin("android") version "2.0.10" apply false




diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index dfec7cc51514aaa2b56c69a2e03179fbb0370502..46c68e716d3fedc11efcf72d7e37efef71a31362 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later
 
 #Tue Aug 09 15:48:25 CEST 2022
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME