Skip to content

Commit 33c5c73

Browse files
committed
Display associated areas on the map
1 parent 2760552 commit 33c5c73

44 files changed

Lines changed: 233 additions & 12 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ android {
7777
implementation(libs.maplibre)
7878
implementation(libs.qrgenerator)
7979
implementation(libs.colorpicker)
80+
implementation(libs.coil)
81+
implementation(libs.coil.network)
82+
implementation(libs.coil.svg)
8083

8184
androidTestImplementation(libs.androidx.test)
8285
androidTestImplementation(libs.androidx.test.runner)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.btcmap.api
2+
3+
import org.btcmap.http.httpClient
4+
import org.btcmap.json.toJsonArray
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.withContext
7+
import okhttp3.Request
8+
import okhttp3.coroutines.executeAsync
9+
import org.btcmap.settings.apiUrlV4
10+
import org.btcmap.settings.prefs
11+
import java.io.InputStream
12+
13+
data class GetAreasItem(
14+
val id: Long,
15+
val name: String,
16+
val type: String,
17+
val urlAlias: String,
18+
val icon: String?,
19+
val websiteUrl: String,
20+
)
21+
22+
object AreasApi {
23+
private const val ENDPOINT = "areas"
24+
25+
fun InputStream.toAreas(): List<GetAreasItem> {
26+
return toJsonArray().map {
27+
GetAreasItem(
28+
id = it.getLong("id"),
29+
name = it.getString("name"),
30+
type = it.getString("type"),
31+
urlAlias = it.getString("url_alias"),
32+
icon = it.optString("icon").ifBlank { null },
33+
websiteUrl = it.getString("website_url"),
34+
)
35+
}
36+
}
37+
38+
suspend fun getAreas(lat: Double, lon: Double): List<GetAreasItem> {
39+
val url = prefs.apiUrlV4(ENDPOINT).newBuilder().apply {
40+
addQueryParameter("lat", lat.toString())
41+
addQueryParameter("lon", lon.toString())
42+
}.build()
43+
44+
val res = httpClient.newCall(Request.Builder().url(url).build()).executeAsync()
45+
46+
if (!res.isSuccessful) {
47+
throw Exception("Unexpected HTTP response code: ${res.code}")
48+
}
49+
50+
return withContext(Dispatchers.IO) {
51+
res.body.byteStream().use { it.toAreas() }
52+
}
53+
}
54+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.btcmap.map
2+
3+
import android.view.LayoutInflater
4+
import android.view.ViewGroup
5+
import androidx.recyclerview.widget.DiffUtil
6+
import androidx.recyclerview.widget.ListAdapter
7+
import androidx.recyclerview.widget.RecyclerView
8+
import coil3.load
9+
import org.btcmap.api.GetAreasItem
10+
import org.btcmap.databinding.AreaItemBinding
11+
12+
class AreasAdapter(
13+
private val onItemClick: (GetAreasItem) -> Unit,
14+
) : ListAdapter<GetAreasItem, AreasAdapter.ItemViewHolder>(DiffCallback()) {
15+
16+
override fun onCreateViewHolder(
17+
parent: ViewGroup,
18+
viewType: Int,
19+
): ItemViewHolder {
20+
val binding = AreaItemBinding.inflate(
21+
LayoutInflater.from(parent.context),
22+
parent,
23+
false,
24+
)
25+
26+
return ItemViewHolder(binding)
27+
}
28+
29+
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
30+
holder.bind(getItem(position), onItemClick)
31+
}
32+
33+
class ItemViewHolder(
34+
private val binding: AreaItemBinding,
35+
) : RecyclerView.ViewHolder(binding.root) {
36+
37+
fun bind(area: GetAreasItem, onItemClick: (GetAreasItem) -> Unit) {
38+
binding.apply {
39+
icon.load(area.icon)
40+
root.setOnClickListener { onItemClick(area) }
41+
}
42+
}
43+
}
44+
45+
class DiffCallback : DiffUtil.ItemCallback<GetAreasItem>() {
46+
47+
override fun areItemsTheSame(oldItem: GetAreasItem, newItem: GetAreasItem): Boolean {
48+
return newItem.id == oldItem.id
49+
}
50+
51+
override fun areContentsTheSame(oldItem: GetAreasItem, newItem: GetAreasItem): Boolean {
52+
return newItem == oldItem
53+
}
54+
}
55+
}

app/src/main/kotlin/org/btcmap/map/MapFragment.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.content.res.Configuration
88
import android.graphics.Color
99
import android.location.Location
1010
import android.os.Bundle
11+
import android.util.Log
1112
import android.view.LayoutInflater
1213
import android.view.View
1314
import android.view.ViewGroup
@@ -42,6 +43,7 @@ import org.btcmap.db.table.event.EventQueries
4243
import org.btcmap.db.table.place.Place
4344
import org.btcmap.db.table.place.PlaceQueries
4445
import org.btcmap.place.PlaceFragment
46+
import org.btcmap.api.AreasApi
4547
import org.btcmap.http.httpClient
4648
import kotlinx.coroutines.delay
4749
import kotlinx.coroutines.flow.launchIn
@@ -226,6 +228,10 @@ class MapFragment : Fragment() {
226228
binding.map.getMapAsync {
227229
it.addOnCameraIdleListener {
228230
prefs.mapViewport = it.projection.visibleRegion.latLngBounds
231+
val center = it.projection.visibleRegion.latLngBounds.center
232+
viewLifecycleOwner.lifecycleScope.launch {
233+
loadAreas(center.latitude, center.longitude)
234+
}
229235
}
230236

231237
it.setStyle(Style.Builder().fromUri(prefs.mapStyle.uri(requireContext())))
@@ -397,6 +403,19 @@ class MapFragment : Fragment() {
397403
}
398404
}.launchIn(viewLifecycleOwner.lifecycleScope)
399405

406+
areasAdapter = AreasAdapter { area ->
407+
val intent = Intent(Intent.ACTION_VIEW, area.websiteUrl.toUri())
408+
startActivity(intent)
409+
}
410+
411+
binding.areas.layoutManager =
412+
LinearLayoutManager(
413+
requireContext(),
414+
LinearLayoutManager.VERTICAL,
415+
true,
416+
)
417+
binding.areas.adapter = areasAdapter
418+
400419
binding.searchView.editText.doAfterTextChanged { searchString ->
401420
binding.map.getMapAsync { map ->
402421
viewLifecycleOwner.lifecycleScope.launch {
@@ -493,7 +512,7 @@ class MapFragment : Fragment() {
493512
return@addOnMapClickListener true
494513
}
495514
} catch (e: Exception) {
496-
// Ignore
515+
Log.e(null, null, e)
497516
}
498517

499518
false
@@ -505,9 +524,6 @@ class MapFragment : Fragment() {
505524
override fun onStateChanged(bottomSheet: View, newState: Int) {
506525
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
507526
viewLifecycleOwner.lifecycleScope.launch { selectPlace(null) }
508-
binding.fab.isVisible = true
509-
} else {
510-
binding.fab.isVisible = false
511527
}
512528
}
513529

@@ -1173,6 +1189,8 @@ class MapFragment : Fragment() {
11731189
private val _searchResults = MutableStateFlow<List<SearchAdapterItem>>(emptyList())
11741190
val searchResults = _searchResults.asStateFlow()
11751191

1192+
private lateinit var areasAdapter: AreasAdapter
1193+
11761194
suspend fun search(referenceLocation: LatLng, searchString: String) {
11771195
if (searchString.length < MIN_QUERY_LENGTH) {
11781196
_searchResults.update { emptyList() }
@@ -1222,8 +1240,19 @@ class MapFragment : Fragment() {
12221240
return distance[0].toDouble()
12231241
}
12241242

1243+
private suspend fun loadAreas(lat: Double, lon: Double) {
1244+
try {
1245+
val areas = withContext(Dispatchers.IO) {
1246+
AreasApi.getAreas(lat, lon)
1247+
}
1248+
areasAdapter.submitList(areas)
1249+
} catch (_: Throwable) {
1250+
areasAdapter.submitList(emptyList())
1251+
}
1252+
}
1253+
12251254
private fun initInsets(binding: MapFragmentBinding) {
1226-
ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { v, windowInsets ->
1255+
ViewCompat.setOnApplyWindowInsetsListener(binding.fabContainer) { v, windowInsets ->
12271256
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
12281257

12291258
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:app="http://schemas.android.com/apk/res-auto"
5+
android:id="@+id/icon"
6+
android:layout_width="56dp"
7+
android:layout_height="56dp"
8+
android:layout_marginVertical="8dp"
9+
android:background="#f79413"
10+
android:contentDescription="@string/area_icon"
11+
android:scaleType="centerCrop"
12+
app:shapeAppearanceOverlay="@style/CircleImageView" />

app/src/main/res/layout/map_fragment.xml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,26 @@
110110

111111
</LinearLayout>
112112

113-
<org.btcmap.view.IconButton
114-
android:id="@+id/fab"
115-
android:layout_width="56dp"
116-
android:layout_gravity="end|bottom"
117-
android:layout_height="56dp"
113+
<LinearLayout
114+
android:id="@+id/fabContainer"
115+
android:layout_width="wrap_content"
116+
android:orientation="vertical"
118117
android:layout_margin="24dp"
119-
android:paddingBottom="200dp"
120-
app:iconSrc="@drawable/icon_my_location" />
118+
android:layout_gravity="end|bottom"
119+
android:layout_height="wrap_content" >
120+
121+
<androidx.recyclerview.widget.RecyclerView
122+
android:id="@+id/areas"
123+
android:layout_width="wrap_content"
124+
android:layout_height="wrap_content" />
125+
126+
<org.btcmap.view.IconButton
127+
android:id="@+id/fab"
128+
android:layout_marginTop="8dp"
129+
android:layout_width="56dp"
130+
android:layout_height="56dp"
131+
app:iconSrc="@drawable/icon_my_location" />
132+
133+
</LinearLayout>
121134

122135
</androidx.coordinatorlayout.widget.CoordinatorLayout>

app/src/main/res/values-af/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@
6363
<string name="button_border">Knoppie rand</string>
6464
<string name="use_adaptive_colors">Gebruik aanpassingskleuren</string>
6565
<string name="reset">Herstel</string>
66+
<string name="area_icon">Gebied-ikoon</string>
6667
</resources>

app/src/main/res/values-ar/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@
6363
<string name="button_border">حد الزر</string>
6464
<string name="use_adaptive_colors">استخدم الألوان التكيفية</string>
6565
<string name="reset">إعادة تعيين</string>
66+
<string name="area_icon">أيقونة المنطقة</string>
6667
</resources>

app/src/main/res/values-bg/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@
6363
<string name="button_border">Рамка на бутона</string>
6464
<string name="use_adaptive_colors">Използвайте адаптивни цветове</string>
6565
<string name="reset">Нулиране</string>
66+
<string name="area_icon">Икона на област</string>
6667
</resources>

app/src/main/res/values-bn/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@
6363
<string name="button_border">বোতাম সীমানা</string>
6464
<string name="use_adaptive_colors">অভিযোজক রঙ ব্যবহার করুন</string>
6565
<string name="reset">রিসেট</string>
66+
<string name="area_icon">এলাকা আইকন</string>
6667
</resources>

0 commit comments

Comments
 (0)