Skip to content

Commit c2ecc4f

Browse files
authored
Debug Tree Map (#1169)
* Add debug tree map * Add debug tree map * Add debug tree map
1 parent f8ff800 commit c2ecc4f

File tree

15 files changed

+707
-8
lines changed

15 files changed

+707
-8
lines changed

app/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ dependencies {
208208
implementation libs.bundles.compose
209209
implementation libs.accompanist.permissions
210210

211+
// MapLibre
212+
implementation libs.maplibre.android.sdk
213+
214+
// Image Loading
215+
implementation libs.coil.compose
216+
211217
// Kotlin
212218
implementation libs.kotlin.stdlib.jdk7
213219
implementation libs.kotlinx.coroutines.core

app/src/main/java/org/greenstand/android/TreeTracker/database/TreeTrackerDAO.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ interface TreeTrackerDAO {
187187
@Query("SELECT * FROM tree WHERE _id IN (:ids)")
188188
suspend fun getTreesByIds(ids: List<Long>): List<TreeEntity>
189189

190+
@Query("SELECT * FROM tree")
191+
suspend fun getAllTrees(): List<TreeEntity>
192+
190193
@Query("UPDATE tree_capture SET bundle_id = :bundleId WHERE _id IN (:ids)")
191194
suspend fun updateTreeCapturesBundleIds(ids: List<Long>, bundleId: String)
192195

app/src/main/java/org/greenstand/android/TreeTracker/di/AppModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.greenstand.android.TreeTracker.dashboard.TreesToSyncHelper
3636
import org.greenstand.android.TreeTracker.devoptions.Configurator
3737
import org.greenstand.android.TreeTracker.devoptions.DevOptionsViewModel
3838
import org.greenstand.android.TreeTracker.languagepicker.LanguagePickerViewModel
39+
import org.greenstand.android.TreeTracker.map.MapViewModel
3940
import org.greenstand.android.TreeTracker.messages.ChatViewModel
4041
import org.greenstand.android.TreeTracker.messages.announcementmessage.AnnouncementViewModel
4142
import org.greenstand.android.TreeTracker.messages.individualmeassagelist.IndividualMessageListViewModel
@@ -128,6 +129,8 @@ val appModule = module {
128129

129130
viewModel { SettingsViewModel(get()) }
130131

132+
viewModel { MapViewModel(get()) }
133+
131134
single { UserRepo(get(), get(), get(), get(), get(), get()) }
132135

133136
factory<TreeCapturer> { CaptureFlowScopeManager.getData().get() }
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package org.greenstand.android.TreeTracker.map
2+
3+
import androidx.compose.foundation.layout.fillMaxSize
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.remember
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.platform.LocalContext
9+
import androidx.compose.ui.platform.LocalLifecycleOwner
10+
import androidx.compose.ui.viewinterop.AndroidView
11+
import androidx.lifecycle.Lifecycle
12+
import androidx.lifecycle.LifecycleEventObserver
13+
import com.google.gson.JsonArray
14+
import com.google.gson.JsonObject
15+
import org.maplibre.android.camera.CameraPosition
16+
import org.maplibre.android.camera.CameraUpdateFactory
17+
import org.maplibre.android.geometry.LatLng
18+
import org.maplibre.android.geometry.LatLngBounds
19+
import org.maplibre.android.location.LocationComponentActivationOptions
20+
import org.maplibre.android.location.LocationComponentOptions
21+
import org.maplibre.android.maps.MapView
22+
import org.maplibre.android.style.expressions.Expression
23+
import org.maplibre.android.style.layers.CircleLayer
24+
import org.maplibre.android.style.layers.PropertyFactory
25+
import org.maplibre.android.style.sources.GeoJsonSource
26+
27+
@androidx.annotation.RequiresPermission(allOf = [android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION])
28+
@Composable
29+
fun LibreMap(
30+
markers: List<MapMarker>,
31+
selectedMarkerId: String?,
32+
modifier: Modifier = Modifier,
33+
styleUrl: String = "https://demotiles.maplibre.org/style.json",
34+
onMarkerClick: (String) -> Unit = {}
35+
) {
36+
val context = LocalContext.current
37+
val lifecycleOwner = LocalLifecycleOwner.current
38+
39+
val mapView = remember {
40+
MapView(context).apply {
41+
getMapAsync { mapLibreMap ->
42+
mapLibreMap.setStyle(styleUrl) { style ->
43+
// Set initial camera position (centered on equator with moderate zoom)
44+
val initialPosition = CameraPosition.Builder()
45+
.target(LatLng(0.0, 0.0))
46+
.zoom(2.0)
47+
.build()
48+
mapLibreMap.cameraPosition = initialPosition
49+
val locationComponent = mapLibreMap.locationComponent
50+
locationComponent.activateLocationComponent(
51+
LocationComponentActivationOptions
52+
.builder(context, style)
53+
.locationComponentOptions(
54+
LocationComponentOptions.builder(context)
55+
.pulseEnabled(true)
56+
.build()
57+
)
58+
.build()
59+
)
60+
locationComponent.isLocationComponentEnabled = true
61+
}
62+
}
63+
}
64+
}
65+
66+
// Handle lifecycle events
67+
DisposableEffect(lifecycleOwner) {
68+
val observer = LifecycleEventObserver { _, event ->
69+
when (event) {
70+
Lifecycle.Event.ON_START -> mapView.onStart()
71+
Lifecycle.Event.ON_RESUME -> mapView.onResume()
72+
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
73+
Lifecycle.Event.ON_STOP -> mapView.onStop()
74+
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
75+
else -> {}
76+
}
77+
}
78+
lifecycleOwner.lifecycle.addObserver(observer)
79+
onDispose {
80+
lifecycleOwner.lifecycle.removeObserver(observer)
81+
mapView.onDestroy()
82+
}
83+
}
84+
85+
AndroidView(
86+
modifier = modifier.fillMaxSize(),
87+
factory = { mapView },
88+
update = { view ->
89+
view.getMapAsync { mapLibreMap ->
90+
mapLibreMap.getStyle { style ->
91+
// Remove existing source and layer if they exist
92+
style.getLayer("tree-markers-layer")?.let { style.removeLayer(it) }
93+
style.getSource("tree-markers-source")?.let { style.removeSource(it) }
94+
95+
// Add markers in bulk as GeoJSON
96+
if (markers.isNotEmpty()) {
97+
// Create GeoJSON FeatureCollection
98+
val features = JsonArray()
99+
markers.forEach { marker ->
100+
val feature = JsonObject().apply {
101+
addProperty("type", "Feature")
102+
add("geometry", JsonObject().apply {
103+
addProperty("type", "Point")
104+
add("coordinates", JsonArray().apply {
105+
add(marker.longitude)
106+
add(marker.latitude)
107+
})
108+
})
109+
add("properties", JsonObject().apply {
110+
addProperty("id", marker.id)
111+
})
112+
}
113+
features.add(feature)
114+
}
115+
116+
val featureCollection = JsonObject().apply {
117+
addProperty("type", "FeatureCollection")
118+
add("features", features)
119+
}
120+
121+
// Add GeoJSON source
122+
val source = GeoJsonSource("tree-markers-source", featureCollection.toString())
123+
style.addSource(source)
124+
125+
// Add circle layer with green dots
126+
val radiusExpression = if (selectedMarkerId != null) {
127+
Expression.switchCase(
128+
Expression.eq(Expression.get("id"), Expression.literal(selectedMarkerId)),
129+
Expression.literal(12f), // Selected marker radius
130+
Expression.literal(8f) // Default marker radius
131+
)
132+
} else {
133+
Expression.literal(8f)
134+
}
135+
136+
val circleLayer = CircleLayer("tree-markers-layer", "tree-markers-source").apply {
137+
setProperties(
138+
PropertyFactory.circleRadius(radiusExpression),
139+
PropertyFactory.circleColor("#4CAF50"),
140+
PropertyFactory.circleStrokeWidth(2f),
141+
PropertyFactory.circleStrokeColor("#FFFFFF")
142+
)
143+
}
144+
style.addLayer(circleLayer)
145+
146+
// Add click listener for markers
147+
mapLibreMap.addOnMapClickListener { latLng ->
148+
// Convert map coordinates to screen point
149+
val screenPoint = mapLibreMap.projection.toScreenLocation(latLng)
150+
151+
// Query features at click point from marker layer
152+
val features = mapLibreMap.queryRenderedFeatures(screenPoint, "tree-markers-layer")
153+
154+
// If marker clicked, extract ID and notify
155+
if (features.isNotEmpty()) {
156+
val markerId = features.first().getStringProperty("id")
157+
if (markerId != null) {
158+
onMarkerClick(markerId)
159+
return@addOnMapClickListener true // Consume event
160+
}
161+
}
162+
false // Don't consume - allow map pan/zoom
163+
}
164+
165+
// Handle selected marker zoom
166+
if (selectedMarkerId != null) {
167+
val selectedMarker = markers.find { it.id == selectedMarkerId }
168+
selectedMarker?.let { marker ->
169+
val position = CameraPosition.Builder()
170+
.target(LatLng(marker.latitude, marker.longitude))
171+
.zoom(14.0)
172+
.build()
173+
mapLibreMap.animateCamera(CameraUpdateFactory.newCameraPosition(position))
174+
}
175+
} else {
176+
// Calculate bounds to fit all markers
177+
val boundsBuilder = LatLngBounds.Builder()
178+
markers.forEach { marker ->
179+
boundsBuilder.include(LatLng(marker.latitude, marker.longitude))
180+
}
181+
182+
val bounds = boundsBuilder.build()
183+
val padding = 100 // padding in pixels
184+
185+
// Animate camera to show all markers
186+
mapLibreMap.animateCamera(
187+
CameraUpdateFactory.newLatLngBounds(bounds, padding)
188+
)
189+
}
190+
}
191+
}
192+
}
193+
}
194+
)
195+
}

0 commit comments

Comments
 (0)