Bimba.git

commit 149e2aca2c6e230e5c1b02d02e6dd91a5f691c82

Author: Adam <git@apiote.xyz>

show map in departure bottom sheet

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


diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9b645dd1acbad5e479804fda3f2c8d628011a5d..7de0435db26c3d4d5babfa875d01e60bf4614892 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@ 	
 
 	<!--suppress AndroidUnknownAttribute -->
 	<application
+		android:name=".Bimba"
 		android:allowBackup="true"
 		android:dataExtractionRules="@xml/data_extraction_rules"
 		android:fullBackupContent="@xml/backup_rules"




diff --git a/app/src/main/java/ml/adamsprogs/bimba/Bimba.kt b/app/src/main/java/ml/adamsprogs/bimba/Bimba.kt
new file mode 100644
index 0000000000000000000000000000000000000000..05e8f843852389cce329eb2f4ddd7f146eed4732
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/Bimba.kt
@@ -0,0 +1,20 @@
+package ml.adamsprogs.bimba
+
+import org.osmdroid.config.Configuration
+import java.io.File
+
+class Bimba : android.app.Application() {
+	override fun onCreate() {
+		super.onCreate()
+		Configuration.getInstance()
+			.let { config ->
+				config.load(
+					applicationContext,
+					applicationContext.getSharedPreferences("shp", MODE_PRIVATE)
+				)
+				config.osmdroidBasePath = File(applicationContext.cacheDir.absolutePath, "osmdroid")
+
+				config.osmdroidTileCache = File(config.osmdroidBasePath.absolutePath, "tile")
+			}
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
index 679095292cc90d1148e69e45088868d8f5a93ad4..d339afa3b5746d64e5e54d5f6bd7b349791bda5d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
@@ -21,7 +21,12 @@ data class Position(
 	val latitude: Double,
 	val longitude: Double
 ) {
+	fun isZero(): Boolean {
+		return latitude == 0.0 && longitude == 0.0
+	}
+
 	override fun toString(): String = "$latitude,$longitude"
+
 	companion object {
 		fun unmarshal(stream: InputStream): Position {
 			val reader = Reader(stream)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
index ac039dcfa0c61f36da8ad0c583c4871f14fe07a4..a94ead33ad554857e69409f65813f9240ee46759 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
@@ -65,7 +65,6 @@ 		mapViewModel =
 			ViewModelProvider(this)[MapViewModel::class.java]
 
 		observeLocatables()
-		configureMap()
 
 		maybeBinding = FragmentMapBinding.inflate(inflater, container, false)
 		val root: View = binding.root
@@ -116,18 +115,6 @@ 			false
 		}
 
 		return root
-	}
-
-	private fun configureMap() {
-		Configuration.getInstance().let { config ->
-			context?.let { ctx ->
-				ctx.getSharedPreferences("shp", MODE_PRIVATE).let {
-					config.load(context, it)
-				}
-				config.osmdroidBasePath = File(ctx.cacheDir.absolutePath, "osmdroid")
-			}
-			config.osmdroidTileCache = File(config.osmdroidBasePath.absolutePath, "tile")
-		}
 	}
 
 	private fun onMapMove(): Boolean {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt b/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
index 328a041df1a7b0809268d5cd76c994b4253aa42c..339010367fda02c73372000294cbf17b334ac190 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
@@ -2,11 +2,12 @@ package ml.adamsprogs.bimba.departures
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.content.DialogInterface
+import android.content.res.Configuration.*
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
@@ -16,7 +17,15 @@ import ml.adamsprogs.bimba.R
 import ml.adamsprogs.bimba.api.Departure
 import ml.adamsprogs.bimba.api.Vehicle
 import ml.adamsprogs.bimba.dpToPixelI
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.util.GeoPoint
+import org.osmdroid.views.CustomZoomButtonsController
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.Marker
+import org.osmdroid.views.overlay.TilesOverlay
+import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
 import java.util.*
+import kotlin.collections.HashMap
 
 
 class BimbaDepartureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -78,6 +87,30 @@ 	private var departures: List,
 	private val onClickListener: ((Departure) -> Unit)
 ) :
 	RecyclerView.Adapter<BimbaDepartureViewHolder>() {
+
+	private var departuresPositions: MutableMap<String, Int> = HashMap()
+
+	init {
+		departures = departures.map { // fixme (!!) does szczanieckiej not populate departure.vehicle.(line,headsign)?
+			Departure(
+				it.ID, it.line, it.headsign, it.time, it.status, it.isRealtime,
+				it.stopOrder, Vehicle(
+					it.vehicle.ID,
+					it.vehicle.Position,
+					it.vehicle.Capabilities,
+					it.vehicle.Speed,
+					it.line,
+					it.headsign,
+					it.vehicle.CongestionLevel,
+					it.vehicle.OccupancyStatus
+				),
+				it.boarding)
+		}
+		departures.forEachIndexed { i, departure ->
+			departuresPositions[departure.ID] = i
+		}
+	}
+
 	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaDepartureViewHolder {
 		val rowView = inflater.inflate(R.layout.departure, parent, false)
 		return BimbaDepartureViewHolder(rowView)
@@ -89,31 +122,83 @@ 	}
 
 	override fun getItemCount(): Int = departures.size
 
+	fun get(ID: String): Departure? {
+		val position = departuresPositions[ID]
+		return if (position == null) {
+			null
+		} else {
+			departures[position]
+		}
+	}
+
 	fun update(items: List<Departure>) {
-		departures = items
+		val newPositions: MutableMap<String, Int> = HashMap()
+		items.forEachIndexed { i, departure ->
+			newPositions[departure.ID] = i
+		}
+//		// fixme jumps
+//		departures.minus(items.toSet()).forEach {
+//			notifyItemRemoved(departuresPositions[it.ID]!!)
+//		}
+//		items.minus(departures.toSet()).forEach {
+//			notifyItemInserted(newPositions[it.ID]!!)
+//		}
+//		items.intersect(departures.toSet()).forEach {
+//			notifyItemChanged(newPositions[it.ID]!!)
+//		}
+		departures = items.map { // fixme (!!)
+			Departure(
+				it.ID, it.line, it.headsign, it.time, it.status, it.isRealtime,
+				it.stopOrder, Vehicle(
+					it.vehicle.ID,
+					it.vehicle.Position,
+					it.vehicle.Capabilities,
+					it.vehicle.Speed,
+					it.line,
+					it.headsign,
+					it.vehicle.CongestionLevel,
+					it.vehicle.OccupancyStatus
+				),
+				it.boarding)
+		}
+		departuresPositions = newPositions
 		notifyDataSetChanged()
 	}
 }
 
-class ModalBottomSheet(private val departure: Departure) : BottomSheetDialogFragment() {
+class ModalBottomSheet(private var departure: Departure) : BottomSheetDialogFragment() {
 	companion object {
 		const val TAG = "ModalBottomSheet"
 	}
 
-	override fun onCreateView(
-		inflater: LayoutInflater,
-		container: ViewGroup?,
-		savedInstanceState: Bundle?
-	): View {
-		val content = inflater.inflate(R.layout.departure_bottom_sheet, container, false)
+	private var cancelCallback: (() -> Unit)? = null
+
+	fun setOnCancel(callback: () -> Unit) {
+		cancelCallback = callback
+	}
+
+	override fun onCancel(dialog: DialogInterface) {
+		super.onCancel(dialog)
+		cancelCallback?.let { it() }
+	}
 
+	fun departureID(): String {
+		return departure.ID
+	}
+
+	fun update(departure: Departure) {
+		this.departure = departure
+		this.view?.let { setContent(it) }
+	}
+
+	private fun setContent(view: View) {
 		var timeText = "at ${departure.time.Hour.toString().padStart(2, '0')}:${
 			departure.time.Minute.toString().padStart(2, '0')
 		}"
 		if (departure.isRealtime) {
 			timeText += ":${departure.time.Second.toString().padStart(2, '0')}"
 		}
-		content.apply {
+		view.apply {
 			findViewById<TextView>(R.id.time).text = timeText
 
 			findViewById<ImageView>(R.id.rt_icon).apply {
@@ -140,12 +225,6 @@ 				contentDescription = "${departure.line.name} towards ${departure.headsign}"
 				text = "${departure.line.name} » ${departure.headsign}"
 			}
 
-			findViewById<Button>(R.id.map_button).apply {
-				setOnClickListener {
-					// todo show on map
-					// todo(szczanieckiej) vehicleID
-				}
-			}
 			findViewById<TextView>(R.id.boarding_text).apply {
 				text = if (departure.boarding.and(0b1010u) != (0b0).toUByte()) {
 					"on demand"
@@ -216,10 +295,64 @@ 					View.VISIBLE
 				} else {
 					View.GONE
 				}
+			findViewById<MapView>(R.id.map).let { map ->
+				if (departure.vehicle.Position.isZero()) {
+					map.visibility = View.GONE
+					return@let
+				}
+				map.controller.apply {
+					setZoom(19.0f.toDouble())
+					setCenter(
+						GeoPoint(
+							departure.vehicle.location().latitude,
+							departure.vehicle.location().longitude
+						)
+					)
+				}
+
+				map.overlays.removeAll { marker ->
+					marker is Marker
+				}
+				val marker = Marker(map).apply {
+					position =
+						GeoPoint(departure.vehicle.location().latitude, departure.vehicle.location().longitude)
+					setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
+					icon = context?.let { ctx -> departure.vehicle.icon(ctx, 2f) } // fixme colour?
+					setOnClickListener {}
+				}
+				map.overlays.add(marker)
+				map.invalidate()
+			}
 		}
+	}
 
-		(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(150f)
+	@SuppressLint("ClickableViewAccessibility")
+	override fun onCreateView(
+		inflater: LayoutInflater,
+		container: ViewGroup?,
+		savedInstanceState: Bundle?
+	): View {
+		val content = inflater.inflate(R.layout.departure_bottom_sheet, container, false)
 
-		return content
+		content.apply {
+			findViewById<MapView>(R.id.map).let { map ->
+				map.setTileSource(TileSourceFactory.MAPNIK)
+				if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
+									and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
+				) {
+					map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
+				}
+				map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
+				map.setOnTouchListener { _, _ ->  true}
+				map.setMultiTouchControls(true)
+				map.overlays.add(RotationGestureOverlay(map).apply { isEnabled = true })
+			}
+
+			setContent(this)
+
+			(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(180f)
+
+			return content
+		}
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
index aa1d82958e0319167f150132141f746f20f529a5..e2f12209ee96239ba701d37c4356c5946d81fdb2 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
@@ -23,8 +23,6 @@ import ml.adamsprogs.bimba.api.*
 import ml.adamsprogs.bimba.databinding.ActivityDeparturesBinding
 import java.io.InputStream
 
-// todo show stop on map
-
 class DeparturesActivity : AppCompatActivity() {
 	private var _binding: ActivityDeparturesBinding? = null
 	private val binding get() = _binding!!
@@ -33,6 +31,8 @@ 	private lateinit var adapter: BimbaDeparturesAdapter
 
 	private val handler = Handler(Looper.getMainLooper())
 	private var runnable = Runnable {}
+
+	private var openBottomSheet: ModalBottomSheet? = null
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
@@ -50,6 +50,8 @@ 		binding.departuresRecycler.layoutManager = LinearLayoutManager(this)
 		adapter = BimbaDeparturesAdapter(layoutInflater, this, listOf()) {
 			ModalBottomSheet(it).apply {
 				show(supportFragmentManager, ModalBottomSheet.TAG)
+				openBottomSheet = this
+				setOnCancel { openBottomSheet = null }
 			}
 		}
 		binding.departuresRecycler.adapter = adapter
@@ -100,6 +102,7 @@ 				Log.e("Departures", "$departuresResult")
 				Log.e("Departures", "$response")
 				showError(departuresResult.error)
 			} else {
+				openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { openBottomSheet?.update(it) }
 				updateItems((response as DeparturesSuccess))
 			}
 		}




diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml
index 7bf63e7734c5c3f5539a9e373b87f194b6c83ee8..589db79b452ae9e7b86c6784f5ef1d848ae9546e 100644
--- a/app/src/main/res/layout/departure_bottom_sheet.xml
+++ b/app/src/main/res/layout/departure_bottom_sheet.xml
@@ -69,18 +69,6 @@ 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toBottomOf="@id/time"
 		tool:text="Metropolitan » Aleje Marcinkowskiego" />
 
-	<Button
-		android:id="@+id/map_button"
-		style="@style/Widget.Material3.Button.TextButton.Icon"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_margin="8dp"
-		android:text="Show on map"
-		app:icon="@drawable/map_black"
-		app:layout_constraintEnd_toEndOf="parent"
-		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/line" />
-
 	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/boarding_icon"
@@ -186,6 +174,7 @@ 		android:orientation="vertical"
 		app:layout_constraintGuide_percent=".5" />
 
 	<androidx.constraintlayout.helper.widget.Flow
+		android:id="@+id/capabilities"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
@@ -239,4 +228,13 @@ 		android:layout_height="24dp"
 		android:contentDescription="USB charging"
 		app:srcCompat="@drawable/usb"
 		tool:ignore="MissingConstraints" />
+
+	<org.osmdroid.views.MapView
+		android:id="@+id/map"
+		android:layout_width="match_parent"
+		android:layout_height="250dp"
+		android:layout_margin="16dp"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/capabilities" />
 </androidx.constraintlayout.widget.ConstraintLayout>