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>