15 | get() {
16 | val routes = PolylineEncoderDecoder.decode(depokJakartaRoutesPolyline)
17 | return routes.map { GeoPoint(it.lat, it.lng) }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.SideEffect
6 | import androidx.compose.runtime.remember
7 |
8 | class Ref(var value: Int)
9 | // Note the inline function below which ensures that this function is essentially
10 | // copied at the call site to ensure that its logging only recompositions from the
11 | // original call site.
12 | @Composable
13 | inline fun LogCompositions(tag: String, msg: String) {
14 | if (BuildConfig.DEBUG) {
15 | val ref = remember { Ref(0) }
16 | SideEffect { ref.value++ }
17 | Log.d(tag, "Compositions: $msg ${ref.value}")
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import android.graphics.drawable.Drawable
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.Button
17 | import androidx.compose.material.MaterialTheme
18 | import androidx.compose.material.Surface
19 | import androidx.compose.material.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.CompositionLocalProvider
22 | import androidx.compose.runtime.SideEffect
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.runtime.mutableStateOf
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.runtime.rememberCoroutineScope
27 | import androidx.compose.runtime.setValue
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.platform.LocalContext
32 | import androidx.compose.ui.tooling.preview.Preview
33 | import androidx.compose.ui.unit.dp
34 | import androidx.navigation.NavHostController
35 | import androidx.navigation.compose.NavHost
36 | import androidx.navigation.compose.composable
37 | import androidx.navigation.compose.rememberNavController
38 | import com.utsman.osmandcompose.MapProperties
39 | import com.utsman.osmandcompose.Marker
40 | import com.utsman.osmandcompose.OpenStreetMap
41 | import com.utsman.osmandcompose.Polygon
42 | import com.utsman.osmandcompose.Polyline
43 | import com.utsman.osmandcompose.PolylineCap
44 | import com.utsman.osmandcompose.ZoomButtonVisibility
45 | import com.utsman.osmandcompose.rememberCameraState
46 | import com.utsman.osmandcompose.rememberMarkerState
47 | import com.utsman.osmandcompose.rememberOverlayManagerState
48 | import com.utsman.osmapp.navigation.LocalNavigation
49 | import com.utsman.osmapp.navigation.Navigation
50 | import com.utsman.osmapp.navigation.Route
51 | import com.utsman.osmapp.ui.theme.OsmAndroidComposeTheme
52 | import org.osmdroid.tileprovider.MapTileProviderBasic
53 | import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
54 | import org.osmdroid.tileprovider.tilesource.TileSourceFactory
55 | import org.osmdroid.util.GeoPoint
56 | import org.osmdroid.util.MapTileIndex
57 | import org.osmdroid.views.overlay.CopyrightOverlay
58 | import org.osmdroid.views.overlay.TilesOverlay
59 |
60 | class MainActivity : ComponentActivity() {
61 | override fun onCreate(savedInstanceState: Bundle?) {
62 | super.onCreate(savedInstanceState)
63 | setContent {
64 | OsmAndroidComposeTheme {
65 | // A surface container using the 'background' color from the theme
66 | Surface(
67 | modifier = Modifier.fillMaxSize(),
68 | color = MaterialTheme.colors.background
69 | ) {
70 | val navHostController = rememberNavController()
71 |
72 | val navigation = remember {
73 | Navigation(navHostController)
74 | }
75 |
76 | CompositionLocalProvider(LocalNavigation provides navigation) {
77 | MainGraph(navHostController = navHostController)
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | @Composable
86 | fun MainGraph(
87 | navHostController: NavHostController
88 | ) {
89 |
90 | NavHost(navController = navHostController, startDestination = Route.Main.routeArg) {
91 | composable(route = Route.Main.routeArg) {
92 | Main()
93 | }
94 |
95 | composable(route = Route.Simple.routeArg) {
96 | SimplePage()
97 | }
98 |
99 | composable(route = Route.Marker.routeArg) {
100 | MarkerPage()
101 | }
102 |
103 | composable(route = Route.Polyline.routeArg) {
104 | PolylinePage()
105 | }
106 |
107 | composable(route = Route.Polygon.routeArg) {
108 | PolygonPage()
109 | }
110 | }
111 | }
112 |
113 | @Composable
114 | fun Main() {
115 |
116 | val navigation = LocalNavigation.current
117 |
118 | Column(
119 | modifier = Modifier.fillMaxSize(),
120 | verticalArrangement = Arrangement.Center,
121 | horizontalAlignment = Alignment.CenterHorizontally
122 | ) {
123 | Button(onClick = {
124 | navigation.goToSimpleNode()
125 | }) {
126 | Text(text = "Simple maps")
127 | }
128 |
129 | Button(onClick = {
130 | navigation.goToMarker()
131 | }) {
132 | Text(text = "Marker")
133 | }
134 |
135 | Button(onClick = {
136 | navigation.goToPolyline()
137 | }) {
138 | Text(text = "Polyline")
139 | }
140 |
141 | Button(onClick = {
142 | navigation.goToPolygon()
143 | }) {
144 | Text(text = "Polygon")
145 | }
146 | }
147 | }
148 |
149 |
150 | /**
151 | * Playground
152 | * */
153 |
154 | @Composable
155 | fun MarkerPage1() {
156 | val depokState = rememberMarkerState(geoPoint = GeoPoint(-6.3970066, 106.8224316))
157 | val jakartaState = rememberMarkerState(geoPoint = GeoPoint(-6.1907982, 106.8315909))
158 | val depokState2 = rememberMarkerState(geoPoint = GeoPoint(-6.3729963,106.75806))
159 |
160 | val cameraState = rememberCameraState {
161 | geoPoint = depokState.geoPoint
162 | zoom = 12.0
163 | }
164 |
165 | val overlayManagerState = rememberOverlayManagerState()
166 |
167 | val context = LocalContext.current
168 |
169 | var depokIcon: Drawable? by remember {
170 | mutableStateOf(context.getDrawable(R.drawable.round_eject_24))
171 | }
172 |
173 | var depokVisible by remember {
174 | mutableStateOf(false)
175 | }
176 |
177 | val scope = rememberCoroutineScope()
178 |
179 | var mapProperties by remember {
180 | mutableStateOf(MapProperties())
181 | }
182 |
183 | val tileOverlay = remember {
184 | val tileUrl = "https://osm.rrze.fau.de/osmhd/"
185 |
186 | val rrzeSource = object : OnlineTileSourceBase(
187 | "RRZE",
188 | 0,
189 | 20,
190 | 256,
191 | "",
192 | arrayOf(tileUrl)
193 | ) {
194 | override fun getTileURLString(pMapTileIndex: Long): String {
195 | val url = baseUrl + MapTileIndex.getZoom(pMapTileIndex) +
196 | "/" + MapTileIndex.getX(pMapTileIndex) +
197 | "/" + MapTileIndex.getY(pMapTileIndex) +
198 | ".png"
199 | return url
200 | }
201 | }
202 |
203 | val tileProvider = MapTileProviderBasic(
204 | context,
205 | rrzeSource
206 | )
207 |
208 | TilesOverlay(tileProvider, context)
209 | }
210 |
211 | val polygonHoles = remember {
212 | val hole1 = listOf(
213 | GeoPoint(-6.3690298,106.7791744),
214 | GeoPoint(-6.3393337,106.8030781),
215 | GeoPoint(-6.3537767,106.7629521)
216 | )
217 |
218 | val hole2 = listOf(
219 | GeoPoint(-6.3083577,106.7829421),
220 | GeoPoint(-6.3105647,106.7866221)
221 | )
222 |
223 | listOf(hole1, hole2)
224 | }
225 |
226 | SideEffect {
227 | mapProperties = mapProperties
228 | .copy(isTilesScaledToDpi = true)
229 | .copy(tileSources = TileSourceFactory.MAPNIK)
230 | .copy(isEnableRotationGesture = true)
231 | .copy(zoomButtonVisibility = ZoomButtonVisibility.NEVER)
232 | }
233 |
234 | Box {
235 | OpenStreetMap(
236 | modifier = Modifier.fillMaxSize(),
237 | cameraState = cameraState,
238 | overlayManagerState = overlayManagerState,
239 | properties = mapProperties,
240 | onMapClick = {
241 | println("on click -> $it")
242 | },
243 | onMapLongClick = {
244 | depokState.geoPoint = it
245 | println("on long click -> ${it.latitude}, ${it.longitude}")
246 |
247 | },
248 | onFirstLoadListener = {
249 | println("on loaded ... ")
250 | overlayManagerState.overlayManager.add(CopyrightOverlay(context))
251 | }
252 | ) {
253 |
254 | Marker(
255 | state = depokState,
256 | icon = depokIcon,
257 | title = "anuan",
258 | snippet = "haah"
259 | ) {
260 | Column(
261 | modifier = Modifier
262 | .size(100.dp)
263 | .background(color = Color.Gray, shape = RoundedCornerShape(12.dp))
264 | ) {
265 | Text(text = it.title)
266 | Text(text = it.snippet)
267 | }
268 | }
269 |
270 | Polyline(
271 | geoPoints = listOf(depokState.geoPoint, jakartaState.geoPoint),
272 | color = Color.Cyan,
273 | cap = PolylineCap.ROUND
274 | ) {
275 | Column(
276 | modifier = Modifier
277 | .size(100.dp)
278 | .background(color = Color.Red, shape = RoundedCornerShape(6.dp))
279 | ) {
280 | Text(text = it.title)
281 | Text(text = it.snippet)
282 | }
283 | }
284 |
285 | Polygon(
286 | geoPoints = listOf(depokState.geoPoint, GeoPoint(-6.2076517,106.7439701), depokState2.geoPoint),
287 | geoPointHoles = polygonHoles,
288 | color = Color.Blue,
289 | outlineColor = Color.Green
290 | )
291 | }
292 |
293 | Column(
294 | modifier = Modifier
295 | .align(Alignment.BottomCenter)
296 | .fillMaxWidth()
297 | .padding(horizontal = 100.dp)
298 | ) {
299 | Button(
300 | onClick = {
301 | cameraState.geoPoint = depokState.geoPoint
302 | cameraState.speed = 1000
303 | cameraState.zoom = 16.0
304 | }) {
305 | Text(text = "marker visible")
306 | }
307 |
308 | Button(
309 | onClick = {
310 | depokState.rotation = depokState.rotation + 90f
311 | }) {
312 | Text(text = "rotasi")
313 | }
314 |
315 | Button(
316 | onClick = {
317 | cameraState.normalizeRotation()
318 | }) {
319 | Text(text = "rotasi normal")
320 | }
321 | }
322 |
323 | }
324 | }
325 |
326 | @Preview(showBackground = true)
327 | @Composable
328 | fun DefaultPreview() {
329 | OsmAndroidComposeTheme {
330 | MarkerPage()
331 | }
332 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/MarkerPage.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import android.graphics.Color
4 | import android.graphics.Paint
5 | import android.graphics.drawable.Drawable
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.platform.LocalContext
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import com.utsman.osmandcompose.Marker
23 | import com.utsman.osmandcompose.MarkerLabeled
24 | import com.utsman.osmandcompose.OpenStreetMap
25 | import com.utsman.osmandcompose.model.LabelProperties
26 | import com.utsman.osmandcompose.rememberCameraState
27 | import com.utsman.osmandcompose.rememberMarkerState
28 |
29 | @Composable
30 | fun MarkerPage() {
31 | val context = LocalContext.current
32 |
33 | val cameraState = rememberCameraState {
34 | geoPoint = Coordinates.depok
35 | zoom = 12.0
36 | }
37 |
38 | val depokMarkerState = rememberMarkerState(
39 | geoPoint = Coordinates.depok,
40 | rotation = 90f
41 | )
42 |
43 | val jakartaMarkerState = rememberMarkerState(
44 | geoPoint = Coordinates.jakarta,
45 | rotation = 90f
46 | )
47 |
48 | val depokIcon: Drawable? by remember {
49 | mutableStateOf(context.getDrawable(R.drawable.round_eject_24))
50 | }
51 |
52 | val jakartaLabelProperties = remember {
53 | mutableStateOf(
54 | LabelProperties(
55 | labelColor = Color.RED,
56 | labelTextSize = 40f,
57 | labelAlign = Paint.Align.CENTER,
58 | labelTextOffset = 30f
59 | )
60 | )
61 | }
62 |
63 | OpenStreetMap(
64 | modifier = Modifier.fillMaxSize(),
65 | cameraState = cameraState
66 | ) {
67 | Marker(
68 | state = depokMarkerState,
69 | icon = depokIcon,
70 | title = "Depok",
71 | snippet = "Jawa barat"
72 | ) {
73 | Column(
74 | modifier = Modifier
75 | .size(100.dp)
76 | .background(color = androidx.compose.ui.graphics.Color.Gray, shape = RoundedCornerShape(7.dp)),
77 | verticalArrangement = Arrangement.Center,
78 | horizontalAlignment = Alignment.CenterHorizontally
79 | ) {
80 | Text(text = it.title)
81 | Text(text = it.snippet, fontSize = 10.sp)
82 | }
83 | }
84 |
85 |
86 | MarkerLabeled (
87 | state = jakartaMarkerState,
88 | icon = depokIcon,
89 | title = "Jakarta",
90 | snippet = "DKI Jakarta",
91 | label = "Jakarta",
92 | labelProperties = jakartaLabelProperties.value
93 | ){
94 | Column(
95 | modifier = Modifier
96 | .size(100.dp)
97 | .background(color = androidx.compose.ui.graphics.Color.Gray, shape = RoundedCornerShape(7.dp)),
98 | verticalArrangement = Arrangement.Center,
99 | horizontalAlignment = Alignment.CenterHorizontally
100 | ) {
101 | Text(text = it.title)
102 | Text(text = it.snippet, fontSize = 10.sp)
103 | }
104 | }
105 | }
106 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/PolygonPage.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import com.utsman.osmandcompose.OpenStreetMap
9 | import com.utsman.osmandcompose.Polygon
10 | import com.utsman.osmandcompose.rememberCameraState
11 |
12 | @Composable
13 | fun PolygonPage() {
14 |
15 | val cameraState = rememberCameraState {
16 | geoPoint = Coordinates.depok
17 | zoom = 12.0
18 | }
19 |
20 | val geoPoint = remember {
21 | listOf(Coordinates.bekasi, Coordinates.depok, Coordinates.tangerang)
22 | }
23 |
24 | OpenStreetMap(
25 | modifier = Modifier.fillMaxSize(),
26 | cameraState = cameraState
27 | ) {
28 | Polygon(
29 | geoPoints = geoPoint,
30 | color = Color.Red,
31 | width = 18f,
32 | onPolygonLoaded = { outlinePaint, fillPaint ->
33 |
34 | }
35 | )
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/PolylineEncoderDecoder.java:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Iterator;
5 | import java.util.List;
6 | import java.util.concurrent.atomic.AtomicInteger;
7 | import java.util.concurrent.atomic.AtomicLong;
8 | import java.util.concurrent.atomic.AtomicReference;
9 |
10 | /**
11 | * The polyline encoding is a lossy compressed representation of a list of coordinate pairs or coordinate triples.
12 | * It achieves that by:
13 | *
14 | * - Reducing the decimal digits of each value.
15 | *
- Encoding only the offset from the previous point.
16 | *
- Using variable length for each coordinate delta.
17 | *
- Using 64 URL-safe characters to display the result.
18 | *
19 | *
20 | * The advantage of this encoding are the following:
21 | *
22 | * - Output string is composed by only URL-safe characters
23 | *
- Floating point precision is configurable
24 | *
- It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, elevation or some other custom value
25 | *
26 | */
27 | public class PolylineEncoderDecoder {
28 |
29 | /**
30 | * Header version
31 | * A change in the version may affect the logic to encode and decode the rest of the header and data
32 | */
33 | public static final byte FORMAT_VERSION = 1;
34 |
35 | //Base64 URL-safe characters
36 | public static final char[] ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray();
37 |
38 | public static final int[] DECODING_TABLE = {
39 | 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1,
40 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
41 | 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
42 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
43 | };
44 | /**
45 | * Encode the list of coordinate triples.
46 | * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT.
47 | * This is lossy compression based on precision accuracy.
48 | *
49 | * @param coordinates {@link List} of coordinate triples that to be encoded.
50 | * @param precision Floating point precision of the coordinate to be encoded.
51 | * @param thirdDimension {@link ThirdDimension} which may be a level, altitude, elevation or some other custom value
52 | * @param thirdDimPrecision Floating point precision for thirdDimension value
53 | * @return URL-safe encoded {@link String} for the given coordinates.
54 | */
55 | public static String encode(List coordinates, int precision, ThirdDimension thirdDimension, int thirdDimPrecision) {
56 | if (coordinates == null || coordinates.size() == 0) {
57 | throw new IllegalArgumentException("Invalid coordinates!");
58 | }
59 | if (thirdDimension == null) {
60 | throw new IllegalArgumentException("Invalid thirdDimension");
61 | }
62 | Encoder enc = new Encoder(precision, thirdDimension, thirdDimPrecision);
63 | Iterator iter = coordinates.iterator();
64 | while (iter.hasNext()) {
65 | enc.add(iter.next());
66 | }
67 | return enc.getEncoded();
68 | }
69 |
70 | /**
71 | * Decode the encoded input {@link String} to {@link List} of coordinate triples.
72 | * @param encoded URL-safe encoded {@link String}
73 | * @return {@link List} of coordinate triples that are decoded from input
74 | *
75 | * @see LatLngZ
76 | */
77 | public static final List decode(String encoded) {
78 |
79 | if (encoded == null || encoded.trim().isEmpty()) {
80 | throw new IllegalArgumentException("Invalid argument!");
81 | }
82 | List result = new ArrayList<>();
83 | Decoder dec = new Decoder(encoded);
84 | AtomicReference lat = new AtomicReference<>(0d);
85 | AtomicReference lng = new AtomicReference<>(0d);
86 | AtomicReference z = new AtomicReference<>(0d);
87 |
88 | while (dec.decodeOne(lat, lng, z)) {
89 | result.add(new LatLngZ(lat.get(), lng.get(), z.get()));
90 | lat = new AtomicReference<>(0d);
91 | lng = new AtomicReference<>(0d);
92 | z = new AtomicReference<>(0d);
93 | }
94 | return result;
95 | }
96 |
97 | /**
98 | * ThirdDimension type from the encoded input {@link String}
99 | * @param encoded URL-safe encoded coordinate triples {@link String}
100 | * @return type of {@link ThirdDimension}
101 | */
102 | public static ThirdDimension getThirdDimension(String encoded) {
103 | AtomicInteger index = new AtomicInteger(0);
104 | AtomicLong header = new AtomicLong(0);
105 | Decoder.decodeHeaderFromString(encoded, index, header);
106 | return ThirdDimension.fromNum((header.get() >> 4) & 7);
107 | }
108 |
109 | public byte getVersion() {
110 | return FORMAT_VERSION;
111 | }
112 |
113 | /*
114 | * Single instance for configuration, validation and encoding for an input request.
115 | */
116 | private static class Encoder {
117 |
118 | private final StringBuilder result;
119 | private final Converter latConveter;
120 | private final Converter lngConveter;
121 | private final Converter zConveter;
122 | private final ThirdDimension thirdDimension;
123 |
124 | public Encoder(int precision, ThirdDimension thirdDimension, int thirdDimPrecision) {
125 | this.latConveter = new Converter(precision);
126 | this.lngConveter = new Converter(precision);
127 | this.zConveter = new Converter(thirdDimPrecision);
128 | this.thirdDimension = thirdDimension;
129 | this.result = new StringBuilder();
130 | encodeHeader(precision, this.thirdDimension.getNum(), thirdDimPrecision);
131 | }
132 |
133 | private void encodeHeader(int precision, int thirdDimensionValue, int thirdDimPrecision) {
134 | /*
135 | * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char
136 | */
137 | if (precision < 0 || precision > 15) {
138 | throw new IllegalArgumentException("precision out of range");
139 | }
140 |
141 | if (thirdDimPrecision < 0 || thirdDimPrecision > 15) {
142 | throw new IllegalArgumentException("thirdDimPrecision out of range");
143 | }
144 |
145 | if (thirdDimensionValue < 0 || thirdDimensionValue > 7) {
146 | throw new IllegalArgumentException("thirdDimensionValue out of range");
147 | }
148 | long res = (thirdDimPrecision << 7) | (thirdDimensionValue << 4) | precision;
149 | Converter.encodeUnsignedVarint(PolylineEncoderDecoder.FORMAT_VERSION, result);
150 | Converter.encodeUnsignedVarint(res, result);
151 | }
152 |
153 | private void add(double lat, double lng) {
154 | latConveter.encodeValue(lat, result);
155 | lngConveter.encodeValue(lng, result);
156 | }
157 |
158 | private void add(double lat, double lng, double z) {
159 | add(lat, lng);
160 | if (this.thirdDimension != ThirdDimension.ABSENT) {
161 | zConveter.encodeValue(z, result);
162 | }
163 | }
164 |
165 | private void add(LatLngZ tuple) {
166 | if(tuple == null) {
167 | throw new IllegalArgumentException("Invalid LatLngZ tuple");
168 | }
169 | add(tuple.lat, tuple.lng, tuple.z);
170 | }
171 |
172 | private String getEncoded() {
173 | return this.result.toString();
174 | }
175 | }
176 |
177 | /*
178 | * Single instance for decoding an input request.
179 | */
180 | private static class Decoder {
181 |
182 | private final String encoded;
183 | private final AtomicInteger index;
184 | private final Converter latConveter;
185 | private final Converter lngConveter;
186 | private final Converter zConveter;
187 |
188 | private int precision;
189 | private int thirdDimPrecision;
190 | private ThirdDimension thirdDimension;
191 |
192 |
193 | public Decoder(String encoded) {
194 | this.encoded = encoded;
195 | this.index = new AtomicInteger(0);
196 | decodeHeader();
197 | this.latConveter = new Converter(precision);
198 | this.lngConveter = new Converter(precision);
199 | this.zConveter = new Converter(thirdDimPrecision);
200 | }
201 |
202 | private boolean hasThirdDimension() {
203 | return thirdDimension != ThirdDimension.ABSENT;
204 | }
205 |
206 | private void decodeHeader() {
207 | AtomicLong header = new AtomicLong(0);
208 | decodeHeaderFromString(encoded, index, header);
209 | precision = (int) (header.get() & 15); // we pick the first 4 bits only
210 | header.set(header.get() >> 4);
211 | thirdDimension = ThirdDimension.fromNum(header.get() & 7); // we pick the first 3 bits only
212 | thirdDimPrecision = (int) ((header.get() >> 3) & 15);
213 | }
214 |
215 | private static void decodeHeaderFromString(String encoded, AtomicInteger index, AtomicLong header) {
216 | AtomicLong value = new AtomicLong(0);
217 |
218 | // Decode the header version
219 | if(!Converter.decodeUnsignedVarint(encoded.toCharArray(), index, value)) {
220 | throw new IllegalArgumentException("Invalid encoding");
221 | }
222 | if (value.get() != FORMAT_VERSION) {
223 | throw new IllegalArgumentException("Invalid format version");
224 | }
225 | // Decode the polyline header
226 | if(!Converter.decodeUnsignedVarint(encoded.toCharArray(), index, value)) {
227 | throw new IllegalArgumentException("Invalid encoding");
228 | }
229 | header.set(value.get());
230 | }
231 |
232 |
233 | private boolean decodeOne(AtomicReference lat,
234 | AtomicReference lng,
235 | AtomicReference z) {
236 | if (index.get() == encoded.length()) {
237 | return false;
238 | }
239 | if (!latConveter.decodeValue(encoded, index, lat)) {
240 | throw new IllegalArgumentException("Invalid encoding");
241 | }
242 | if (!lngConveter.decodeValue(encoded, index, lng)) {
243 | throw new IllegalArgumentException("Invalid encoding");
244 | }
245 | if (hasThirdDimension()) {
246 | if (!zConveter.decodeValue(encoded, index, z)) {
247 | throw new IllegalArgumentException("Invalid encoding");
248 | }
249 | }
250 | return true;
251 | }
252 | }
253 |
254 | //Decode a single char to the corresponding value
255 | private static int decodeChar(char charValue) {
256 | int pos = charValue - 45;
257 | if (pos < 0 || pos > 77) {
258 | return -1;
259 | }
260 | return DECODING_TABLE[pos];
261 | }
262 |
263 | /*
264 | * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request.
265 | * Instance should be specific to type of coordinates (e.g. Lat, Lng)
266 | * so that specific type delta is computed for encoding.
267 | * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0)
268 | */
269 | public static class Converter {
270 |
271 | private long multiplier = 0;
272 | private long lastValue = 0;
273 |
274 | public Converter(int precision) {
275 | setPrecision(precision);
276 | }
277 |
278 | private void setPrecision(int precision) {
279 | multiplier = (long) Math.pow(10, Double.valueOf(precision));
280 | }
281 |
282 | private static void encodeUnsignedVarint(long value, StringBuilder result) {
283 | while (value > 0x1F) {
284 | byte pos = (byte) ((value & 0x1F) | 0x20);
285 | result.append(ENCODING_TABLE[pos]);
286 | value >>= 5;
287 | }
288 | result.append(ENCODING_TABLE[(byte) value]);
289 | }
290 |
291 | void encodeValue(double value, StringBuilder result) {
292 | /*
293 | * Round-half-up
294 | * round(-1.4) --> -1
295 | * round(-1.5) --> -2
296 | * round(-2.5) --> -3
297 | */
298 | long scaledValue = (long) Math.round(Math.abs(value * multiplier)) * Math.round(Math.signum(value));
299 | long delta = scaledValue - lastValue;
300 | boolean negative = delta < 0;
301 |
302 | lastValue = scaledValue;
303 |
304 | // make room on lowest bit
305 | delta <<= 1;
306 |
307 | // invert bits if the value is negative
308 | if (negative) {
309 | delta = ~delta;
310 | }
311 | encodeUnsignedVarint(delta, result);
312 | }
313 |
314 | private static boolean decodeUnsignedVarint(char[] encoded,
315 | AtomicInteger index,
316 | AtomicLong result) {
317 | short shift = 0;
318 | long delta = 0;
319 | long value;
320 |
321 | while (index.get() < encoded.length) {
322 | value = decodeChar(encoded[index.get()]);
323 | if (value < 0) {
324 | return false;
325 | }
326 | index.incrementAndGet();
327 | delta |= (value & 0x1F) << shift;
328 | if ((value & 0x20) == 0) {
329 | result.set(delta);
330 | return true;
331 | } else {
332 | shift += 5;
333 | }
334 | }
335 |
336 | if (shift > 0) {
337 | return false;
338 | }
339 | return true;
340 | }
341 |
342 | //Decode single coordinate (say lat|lng|z) starting at index
343 | boolean decodeValue(String encoded,
344 | AtomicInteger index,
345 | AtomicReference coordinate) {
346 | AtomicLong delta = new AtomicLong();
347 | if (!decodeUnsignedVarint(encoded.toCharArray(), index, delta)) {
348 | return false;
349 | }
350 | if ((delta.get() & 1) != 0) {
351 | delta.set(~delta.get());
352 | }
353 | delta.set(delta.get()>>1);
354 | lastValue += delta.get();
355 | coordinate.set(((double)lastValue / multiplier));
356 | return true;
357 | }
358 | }
359 |
360 | /**
361 | * 3rd dimension specification.
362 | * Example a level, altitude, elevation or some other custom value.
363 | * ABSENT is default when there is no third dimension en/decoding required.
364 | */
365 | public static enum ThirdDimension {
366 | ABSENT(0),
367 | LEVEL(1),
368 | ALTITUDE(2),
369 | ELEVATION(3),
370 | RESERVED1(4),
371 | RESERVED2(5),
372 | CUSTOM1(6),
373 | CUSTOM2(7);
374 |
375 | private int num;
376 |
377 | ThirdDimension(int num) {
378 | this.num = num;
379 | }
380 |
381 | public int getNum() {
382 | return num;
383 | }
384 |
385 | public static ThirdDimension fromNum(long value) {
386 | for (ThirdDimension dim : ThirdDimension.values()) {
387 | if (dim.getNum() == value) {
388 | return dim;
389 | }
390 | }
391 | return null;
392 | }
393 | }
394 |
395 | /**
396 | * Coordinate triple
397 | */
398 | public static class LatLngZ {
399 | public final double lat;
400 | public final double lng;
401 | public final double z;
402 |
403 | public LatLngZ (double latitude, double longitude) {
404 | this(latitude, longitude, 0);
405 | }
406 |
407 | public LatLngZ (double latitude, double longitude, double thirdDimension) {
408 | this.lat = latitude;
409 | this.lng = longitude;
410 | this.z = thirdDimension;
411 | }
412 |
413 | @Override
414 | public String toString() {
415 | return "LatLngZ [lat=" + lat + ", lng=" + lng + ", z=" + z + "]";
416 | }
417 |
418 | @Override
419 | public boolean equals(Object anObject) {
420 | if (this == anObject) {
421 | return true;
422 | }
423 | if (anObject instanceof LatLngZ) {
424 | LatLngZ passed = (LatLngZ)anObject;
425 | if(passed.lat == this.lat && passed.lng == this.lng && passed.z == this.z) {
426 | return true;
427 | }
428 | }
429 | return false;
430 | }
431 | }
432 | }
433 |
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/PolylinePage.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import android.graphics.BlendModeColorFilter
4 | import android.graphics.PorterDuff
5 | import android.graphics.PorterDuffColorFilter
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.toArgb
12 | import com.utsman.osmandcompose.OpenStreetMap
13 | import com.utsman.osmandcompose.Polyline
14 | import com.utsman.osmandcompose.PolylineCap
15 | import com.utsman.osmandcompose.rememberCameraState
16 |
17 | @Composable
18 | fun PolylinePage() {
19 |
20 | val cameraState = rememberCameraState {
21 | geoPoint = Coordinates.depok
22 | zoom = 12.0
23 | }
24 |
25 | val geoPoint = remember {
26 | listOf(Coordinates.bekasi, Coordinates.depok, Coordinates.tangerang)
27 | }
28 |
29 | OpenStreetMap(
30 | modifier = Modifier.fillMaxSize(),
31 | cameraState = cameraState
32 | ) {
33 | Polyline(
34 | geoPoints = geoPoint,
35 | color = Color.Red,
36 | cap = PolylineCap.ROUND,
37 | width = 18f,
38 | onPolylineLoaded = { paint ->
39 | // customize here
40 | }
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/SimplePage.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.SideEffect
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.LocalContext
12 | import com.utsman.osmandcompose.DefaultMapProperties
13 | import com.utsman.osmandcompose.MapProperties
14 | import com.utsman.osmandcompose.OpenStreetMap
15 | import com.utsman.osmandcompose.ZoomButtonVisibility
16 | import com.utsman.osmandcompose.rememberCameraState
17 | import com.utsman.osmandcompose.rememberOverlayManagerState
18 | import org.osmdroid.tileprovider.tilesource.TileSourceFactory
19 | import org.osmdroid.views.overlay.CopyrightOverlay
20 |
21 | @Composable
22 | fun SimplePage() {
23 | val context = LocalContext.current
24 |
25 | val cameraState = rememberCameraState {
26 | geoPoint = Coordinates.depok
27 | zoom = 12.0
28 | }
29 |
30 | var mapProperties by remember {
31 | mutableStateOf(DefaultMapProperties)
32 | }
33 |
34 | val overlayManagerState = rememberOverlayManagerState()
35 |
36 | SideEffect {
37 | mapProperties = mapProperties
38 | .copy(isTilesScaledToDpi = true)
39 | .copy(tileSources = TileSourceFactory.MAPNIK)
40 | .copy(isEnableRotationGesture = true)
41 | .copy(zoomButtonVisibility = ZoomButtonVisibility.NEVER)
42 | }
43 |
44 | OpenStreetMap(
45 | modifier = Modifier.fillMaxSize(),
46 | cameraState = cameraState,
47 | properties = mapProperties,
48 | overlayManagerState = overlayManagerState,
49 | onFirstLoadListener = {
50 | val copyright = CopyrightOverlay(context)
51 | overlayManagerState.overlayManager.add(copyright)
52 | }
53 | )
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/navigation/Navigation.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.navigation
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import androidx.navigation.NavHostController
5 |
6 | class Navigation(
7 | private val navHostController: NavHostController
8 | ) {
9 | fun goToSimpleNode() = navHostController launch Route.Simple
10 |
11 | fun goToMarker() = navHostController launch Route.Marker
12 |
13 | fun goToPolyline() = navHostController launch Route.Polyline
14 |
15 | fun goToPolygon() = navHostController launch Route.Polygon
16 | }
17 |
18 | private infix fun NavHostController.launch(navigationRoute: NavigationRoute) {
19 | navigate(route = navigationRoute.routeArg)
20 | }
21 |
22 | val LocalNavigation = compositionLocalOf { error("navigation") }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/navigation/NavigationRoute.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.navigation
2 |
3 | sealed class NavigationRoute(
4 | private val route: String = String.Empty,
5 | private val keyArg: String = String.Empty
6 | ) {
7 |
8 | val routeArg: String
9 | get() {
10 | return if (keyArg.isNotEmpty()) {
11 | "$route{$keyArg}"
12 | } else {
13 | route
14 | }
15 | }
16 | }
17 |
18 | val String.Companion.Empty get() = ""
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/navigation/Route.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.navigation
2 |
3 | object Route {
4 | object Main : NavigationRoute("main")
5 | object Simple : NavigationRoute("simple_node")
6 | object Marker : NavigationRoute("marker")
7 | object Polyline : NavigationRoute("polyline")
8 | object Polygon : NavigationRoute("Polygon")
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun OsmAndroidComposeTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/utsman/osmapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_eject_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Osm Android Compose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/utsman/osmapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_ui_version = '1.2.0'
4 | }
5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id 'com.android.application' version '7.4.2' apply false
8 | id 'com.android.library' version '7.4.2' apply false
9 | id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
10 | id 'com.vanniktech.maven.publish' version '0.25.2' apply false
11 | }
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Installation
4 |
5 | ### Latest version
6 | 
7 |
8 | ### Dependencies
9 | ```groovy
10 | // origin version of osm android. You may be able to customize the version.
11 | implementation 'org.osmdroid:osmdroid-android:6.1.16'
12 |
13 | // This library dependencies
14 | implementation "tech.utsmankece:osm-androd-compose:${latest_version}"
15 | ```
16 |
17 | ## Example app
18 | For see fully example, visit [app module](https://github.com/utsmannn/osm-android-compose/tree/main/app/src/main/java/com/utsman/osmapp)
19 |
20 | ---
--------------------------------------------------------------------------------
/docs/images/info-window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/info-window.png
--------------------------------------------------------------------------------
/docs/images/marker-custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/marker-custom.png
--------------------------------------------------------------------------------
/docs/images/marker-default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/marker-default.png
--------------------------------------------------------------------------------
/docs/images/polygon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/polygon.png
--------------------------------------------------------------------------------
/docs/images/polyline-custom-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/polyline-custom-1.png
--------------------------------------------------------------------------------
/docs/images/polyline-paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/polyline-paint.png
--------------------------------------------------------------------------------
/docs/images/polyline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/polyline.png
--------------------------------------------------------------------------------
/docs/images/simple-maps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/docs/images/simple-maps.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Welcome
2 |
3 | The origin OpenStreetMaps Android visit [https://osmdroid.github.io/osmdroid/](https://osmdroid.github.io/osmdroid/) or [github wiki](https://github.com/osmdroid/osmdroid/wiki)
4 |
5 | This is a simple OpenStreetMap library for Android Compose. There are several basic functions commonly used, such as markers, polylines, and polygons. You can also add custom tiles. For more details, please refer to the sample project.
6 |
7 | ## Contributing
8 | This library may not always be maintained, and I am open to anyone who wants to contribute by reporting bugs, making pull requests, or requesting new features in the future.
9 |
10 | ## License
11 | ```
12 | Copyright 2023 Muhammad Utsman
13 |
14 | Licensed under the Apache License, Version 2.0 (the "License");
15 | you may not use this file except in compliance with the License.
16 | You may obtain a copy of the License at
17 |
18 | http://www.apache.org/licenses/LICENSE-2.0
19 |
20 | Unless required by applicable law or agreed to in writing, software
21 | distributed under the License is distributed on an "AS IS" BASIS,
22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 | See the License for the specific language governing permissions and
24 | limitations under the License.
25 | ```
--------------------------------------------------------------------------------
/docs/marker.md:
--------------------------------------------------------------------------------
1 | # Marker
2 |
3 | `Marker` is an overlay in OSM, [see official](https://github.com/osmdroid/osmdroid/wiki/Markers,-Lines-and-Polygons-(Kotlin)#marker). You can add a marker with simple code like the example below.
4 |
5 | ```kotlin
6 | @Composable
7 | fun MarkerPage() {
8 | // define marker state
9 | val depokMarkerState = rememberMarkerState(
10 | geoPoint = GeoPoint(-6.3970066, 106.8224316)
11 | )
12 |
13 | OpenStreetMap(
14 | modifier = Modifier.fillMaxSize(),
15 | cameraState = cameraState
16 | ) {
17 | // add marker here
18 | Marker(
19 | state = depokMarkerState // add marker state
20 | )
21 | }
22 | }
23 | ```
24 |
25 | { width=500 }
26 |
27 | ---
28 |
29 | ## MarkerState
30 | Is a state that can control the position and rotation of the marker.
31 |
32 | ```kotlin
33 | val depokMarkerState = rememberMarkerState(
34 | geoPoint = Coordinates.depok,
35 | rotation = 90f // default is 0f
36 | )
37 | ```
38 |
39 | ## Icon
40 | By default, `Marker` already has an icon from OSM. However, you can change the icon with a drawable.
41 |
42 | ```kotlin
43 | @Composable
44 | fun MarkerPage() {
45 |
46 | // define marker icon
47 | val depokIcon: Drawable? by remember {
48 | mutableStateOf(context.getDrawable(R.drawable.custom_marker_icon))
49 | }
50 |
51 | OpenStreetMap(
52 | modifier = Modifier.fillMaxSize(),
53 | cameraState = cameraState
54 | ) {
55 | Marker(
56 | state = depokMarkerState,
57 | icon = depokIcon
58 | )
59 | }
60 | }
61 |
62 | ```
63 |
64 | { width=500 }
65 |
66 | ## InfoWindow
67 | OSM supports InfoWindow, see the [official javadoc](https://osmdroid.github.io/osmdroid/javadocs/osmdroid-android/debug/index.html?org/osmdroid/views/overlay/infowindow/InfoWindow.html). For OSM for android compose, it also supports InfoWindow with Compose node. You can create InfoWindow in various shapes using Compose.
68 |
69 | ```kotlin
70 | val depokMarkerState = rememberMarkerState(
71 | geoPoint = Coordinates.depok,
72 | rotation = 90f
73 | )
74 |
75 | val depokIcon: Drawable? by remember {
76 | mutableStateOf(context.getDrawable(R.drawable.round_eject_24))
77 | }
78 |
79 | OpenStreetMap(
80 | modifier = Modifier.fillMaxSize(),
81 | cameraState = cameraState
82 | ) {
83 | Marker(
84 | state = depokMarkerState,
85 | icon = depokIcon,
86 | title = "Depok", // add title
87 | snippet = "Jawa barat" // add snippet
88 | ) {
89 |
90 | // create info window node
91 | Column(
92 | modifier = Modifier
93 | .size(100.dp)
94 | .background(color = Color.Gray, shape = RoundedCornerShape(7.dp)),
95 | verticalArrangement = Arrangement.Center,
96 | horizontalAlignment = Alignment.CenterHorizontally
97 | ) {
98 | // setup content of info window
99 | Text(text = it.title)
100 | Text(text = it.snippet, fontSize = 10.sp)
101 | }
102 | }
103 | }
104 | ```
105 |
106 | { width=500 }
107 | ---
--------------------------------------------------------------------------------
/docs/polyline-polygon.md:
--------------------------------------------------------------------------------
1 | # Polyline and Polygon
2 |
3 | ## Polyline
4 | See official doc [here](https://github.com/osmdroid/osmdroid/wiki/Markers,-Lines-and-Polygons-(Java)#polylines)
5 |
6 | ```kotlin
7 | @Composable
8 | fun PolylinePage() {
9 |
10 | val cameraState = rememberCameraState {
11 | geoPoint = Coordinates.depok
12 | zoom = 12.0
13 | }
14 |
15 | // define polyline
16 | val geoPoint = remember {
17 | listOf(Coordinates.bekasi, Coordinates.depok, Coordinates.tangerang)
18 | }
19 |
20 | OpenStreetMap(
21 | modifier = Modifier.fillMaxSize(),
22 | cameraState = cameraState
23 | ) {
24 | // add polyline
25 | Polyline(geoPoints = geoPoint)
26 | }
27 | }
28 | ```
29 |
30 | { width=500 }
31 |
32 | ### Caps and color polyline
33 |
34 | ```kotlin
35 | OpenStreetMap(
36 | modifier = Modifier.fillMaxSize(),
37 | cameraState = cameraState
38 | ) {
39 | Polyline(
40 | geoPoints = geoPoint,
41 | color = Color.Red, // line color
42 | cap = PolylineCap.ROUND, // end and start cap
43 | width = 18f // width
44 | )
45 | }
46 | ```
47 |
48 | { width=500 }
49 |
50 | ### Fully customizable with `android.graphics.Paint`
51 |
52 | ```kotlin
53 | OpenStreetMap(
54 | modifier = Modifier.fillMaxSize(),
55 | cameraState = cameraState
56 | ) {
57 | Polyline(
58 | geoPoints = geoPoint,
59 | color = Color.Red,
60 | cap = PolylineCap.ROUND,
61 | width = 18f,
62 | onPolylineLoaded = { paint ->
63 | // customize here (optional)
64 | }
65 | )
66 | }
67 | ```
68 |
69 | ## Polygon
70 | See official doc [here](https://github.com/osmdroid/osmdroid/wiki/Markers,-Lines-and-Polygons-(Java)#polygons)
71 |
72 | ```kotlin
73 | OpenStreetMap(
74 | modifier = Modifier.fillMaxSize(),
75 | cameraState = cameraState
76 | ) {
77 | Polygon(
78 | geoPoints = geoPoint,
79 | color = Color.Red,
80 | width = 18f,
81 | onPolygonLoaded = { outlinePaint, fillPaint ->
82 | // customize here (optional)
83 | }
84 | )
85 | }
86 | ```
87 |
88 | { width=500 }
89 |
90 | ---
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Maps Node
2 |
3 | ## Manifest permission
4 |
5 | ```xml
6 |
7 | ```
8 |
9 | ## Maps Node
10 | ```kotlin
11 | class MainActivity : ComponentActivity() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContent {
16 | OsmAndroidComposeTheme {
17 | // A surface container using the 'background' color from the theme
18 | Surface(
19 | modifier = Modifier.fillMaxSize(),
20 | color = MaterialTheme.colors.background
21 | ) {
22 |
23 | // define camera state
24 | val cameraState = rememberCameraState {
25 | geoPoint = GeoPoint(-6.3970066, 106.8224316)
26 | zoom = 12.0 // optional, default is 5.0
27 | }
28 |
29 | // add node
30 | OpenStreetMap(
31 | modifier = Modifier.fillMaxSize(),
32 | cameraState = cameraState
33 | )
34 | }
35 | }
36 | }
37 | }
38 | }
39 | ```
40 |
41 | Result
42 |
43 | { width=500 }
44 |
45 | ---
46 |
47 | ## Camera State
48 | Camera state refers to the state that controls the camera projection for maps. It supports the position, zoom level, and animation duration.
49 |
50 | ```kotlin
51 | val cameraState = rememberCameraState {
52 | geoPoint = GeoPoint(-6.3970066, 106.8224316)
53 | zoom = 12.0 // optional, default is 5.0
54 | }
55 | ```
56 |
57 | ## Map Properties
58 | These are properties that affect the display of maps, such as map orientation, min and max zoom levels, setting multi touch controls and others, see reference
59 |
60 | ```kotlin
61 | // define properties with remember with default value
62 | var mapProperties by remember {
63 | mutableStateOf(DefaultMapProperties)
64 | }
65 |
66 | // setup mapProperties in side effect
67 | SideEffect {
68 | mapProperties = mapProperties
69 | .copy(isTilesScaledToDpi = true)
70 | .copy(tileSources = TileSourceFactory.MAPNIK)
71 | .copy(isEnableRotationGesture = true)
72 | .copy(zoomButtonVisibility = ZoomButtonVisibility.NEVER)
73 | }
74 |
75 | OpenStreetMap(
76 | modifier = Modifier.fillMaxSize(),
77 | cameraState = cameraState,
78 | properties = mapProperties // add properties
79 | )
80 | ```
81 |
82 | ## Overlay Manager State
83 | Overlay is an additional layer on OSM. With `OverlayManagerState`, you can obtain an `OverlayManager` to add other overlays.
84 |
85 | ```kotlin
86 | val overlayManagerState = rememberOverlayManagerState()
87 |
88 | OpenStreetMap(
89 | modifier = Modifier.fillMaxSize(),
90 | cameraState = cameraState,
91 | overlayManagerState = overlayManagerState, // setup overlay manager state
92 | onFirstLoadListener = {
93 | val copyright = CopyrightOverlay(context)
94 | overlayManagerState.overlayManager.add(copyright) // add another overlay in this listener
95 | }
96 | )
97 | ```
98 |
99 | If you want to add an overlay, you must do it in the `onFirstLoadListener` because `OverlayManagerState` must obtain the `MapView` instance first before having an `OverlayManager`.
100 |
101 | ## Others parameters
102 |
103 | `onMapClick`
104 | : when the user clicks maps at any location, this listener will display the geopoints
105 |
106 | `onMapLongClick`
107 | : same as onMapClick, but this is for long clicks.
108 |
109 | `onFirstLoadListener`
110 | : when the map is first loaded
111 |
112 | `content`
113 | : a block that contains nodes from OSM such as markers, polylines and polygons if you want to add them
114 |
115 | ---
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 16 01:02:41 WIB 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: OpenStreetMap for Android Compose
2 |
3 | theme:
4 | name: material
5 | features:
6 | - navigation.sections
7 | - navigation.footer
8 | - navigation.top
9 | palette:
10 | - scheme: slate
11 | primary: teal
12 | accent: teal
13 | toggle:
14 | icon: material/brightness-4
15 | name: Switch to light mode
16 | - scheme: default
17 | primary: teal
18 | accent: teal
19 | toggle:
20 | icon: material/brightness-7
21 | name: Switch to dark mode
22 |
23 | repo_name: utsmannn/osm-android-compose
24 | repo_url: https://github.com/utsmannn/osm-android-compose
25 |
26 | markdown_extensions:
27 | - pymdownx.highlight:
28 | anchor_linenums: true
29 | line_spans: __span
30 | pygments_lang_class: true
31 | - pymdownx.inlinehilite
32 | - pymdownx.snippets
33 | - pymdownx.superfences
34 | - attr_list
35 | - md_in_html
36 | - def_list
37 |
38 | plugins:
39 | - glightbox
40 |
41 | nav:
42 | - Home: index.md
43 | - Getting started: getting-started.md
44 | - Usage:
45 | - Maps Node: usage.md
46 | - Marker: marker.md
47 | - Polyline and polygon: polyline-polygon.md
--------------------------------------------------------------------------------
/osm-compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/osm-compose/build.gradle:
--------------------------------------------------------------------------------
1 | import com.vanniktech.maven.publish.SonatypeHost
2 |
3 | plugins {
4 | id 'com.android.library'
5 | id 'org.jetbrains.kotlin.android'
6 | id 'kotlin-parcelize'
7 | id 'com.vanniktech.maven.publish'
8 | }
9 |
10 | android {
11 | namespace 'com.utsman.osmandcompose'
12 | compileSdk 33
13 |
14 | defaultConfig {
15 | minSdk 24
16 | targetSdk 33
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | consumerProguardFiles "consumer-rules.pro"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | kotlinOptions {
33 | jvmTarget = '1.8'
34 | }
35 | buildFeatures {
36 | compose true
37 | }
38 | composeOptions {
39 | kotlinCompilerExtensionVersion '1.2.0'
40 | }
41 | packagingOptions {
42 | resources {
43 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
44 | }
45 | }
46 | }
47 |
48 | dependencies {
49 |
50 | implementation 'androidx.core:core-ktx:1.7.0'
51 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
52 | implementation 'androidx.activity:activity-compose:1.3.1'
53 | implementation "androidx.compose.ui:ui:$compose_ui_version"
54 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
55 | implementation 'androidx.compose.material:material:1.2.0'
56 |
57 | implementation 'org.osmdroid:osmdroid-android:6.1.16'
58 | implementation 'androidx.interpolator:interpolator:1.0.0'
59 | }
60 |
61 | mavenPublishing {
62 | publishToMavenCentral(SonatypeHost.S01)
63 | signAllPublications()
64 |
65 | coordinates("tech.utsmankece", "osm-android-compose", "0.0.5")
66 |
67 | pom {
68 | name = "OpenStreetMap Android Compose"
69 | description = "OpenStreetMap for android compose"
70 | inceptionYear = "2023"
71 | url = "https://github.com/utsmannn/osm-android-compose"
72 | licenses {
73 | license {
74 | name = "The Apache License, Version 2.0"
75 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
76 | distribution = "http://www.apache.org/licenses/LICENSE-2.0.txt"
77 | }
78 | }
79 | developers {
80 | developer {
81 | id = "utsmannn"
82 | name = "Utsman Muhammad"
83 | url = "https://github.com/utsmannn/"
84 | }
85 | }
86 | scm {
87 | url = "https://github.com/utsmannn/osm-android-compose"
88 | connection = "scm:git:git://github.com/utsmannn/osm-android-compose.git"
89 | developerConnection = "scm:git:ssh://git@github.com/utsmannn/osm-android-compose.git"
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/osm-compose/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/osm-android-compose/cb7484d4d6b2e277602f7a462d04dda6e4b58ecd/osm-compose/consumer-rules.pro
--------------------------------------------------------------------------------
/osm-compose/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/osm-compose/src/androidTest/java/com/utsman/osmandcompose/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.utsman.osmandcompose.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/CameraState.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.os.Parcelable
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.saveable.Saver
8 | import androidx.compose.runtime.saveable.rememberSaveable
9 | import androidx.compose.runtime.setValue
10 | import kotlinx.parcelize.Parcelize
11 | import org.osmdroid.api.IGeoPoint
12 | import org.osmdroid.api.IMapController
13 | import org.osmdroid.util.BoundingBox
14 | import org.osmdroid.util.GeoPoint
15 |
16 | @Parcelize
17 | data class CameraProperty(
18 | var geoPoint: GeoPoint = GeoPoint(0.0, 0.0),
19 | var zoom: Double = 5.0,
20 | var speed: Long = 1000L
21 | ) : Parcelable
22 |
23 | class CameraState(cameraProperty: CameraProperty) {
24 |
25 | var geoPoint: GeoPoint by mutableStateOf(cameraProperty.geoPoint)
26 | var zoom: Double by mutableStateOf(cameraProperty.zoom)
27 | var speed: Long by mutableStateOf(cameraProperty.speed)
28 |
29 | private var map: OsmMapView? = null
30 |
31 | private var prop: CameraProperty
32 | get() {
33 | val currentGeoPoint =
34 | map?.let { GeoPoint(it.mapCenter.latitude, it.mapCenter.longitude) } ?: geoPoint
35 | val currentZoom = map?.zoomLevelDouble ?: zoom
36 | return CameraProperty(currentGeoPoint, currentZoom, speed)
37 | }
38 | set(value) {
39 | synchronized(Unit) {
40 | geoPoint = value.geoPoint
41 | zoom = value.zoom
42 | speed = value.speed
43 | }
44 | }
45 |
46 | internal fun setMap(osmMapView: OsmMapView) {
47 | map = osmMapView
48 | }
49 |
50 | private fun getController(): IMapController {
51 | return map?.controller ?: throw IllegalStateException("Invalid Map attached!")
52 | }
53 |
54 | fun animateTo(geoPoint: GeoPoint) = getController().animateTo(geoPoint)
55 | fun animateTo(x: Int, y: Int) = getController().animateTo(x, y)
56 | fun scrollBy(x: Int, y: Int) = getController().scrollBy(x, y)
57 | fun setCenter(point: GeoPoint) = getController().setCenter(point)
58 | fun setZoom(pZoomLevel: Double): Double = getController().setZoom(pZoomLevel)
59 | fun stopAnimation(jumpToFinish: Boolean) = getController().stopAnimation(jumpToFinish)
60 | fun stopPanning() = getController().stopPanning()
61 | fun zoomIn(animationSpeed: Long? = null) = getController().zoomIn(animationSpeed)
62 | fun zoomInFixing(xPixel: Int, yPixel: Int, zoomAnimation: Long?): Boolean =
63 | getController().zoomInFixing(xPixel, yPixel, zoomAnimation)
64 |
65 | fun zoomInFixing(xPixel: Int, yPixel: Int): Boolean =
66 | getController().zoomInFixing(xPixel, yPixel)
67 |
68 | fun zoomOut(animationSpeed: Long? = null) = getController().zoomOut(animationSpeed)
69 | fun zoomOutFixing(xPixel: Int, yPixel: Int): Boolean =
70 | getController().zoomOutFixing(xPixel, yPixel)
71 |
72 | fun zoomToFixing(zoomLevel: Int, xPixel: Int, yPixel: Int, zoomAnimationSpeed: Long?): Boolean =
73 | getController().zoomToFixing(zoomLevel, xPixel, yPixel, zoomAnimationSpeed)
74 |
75 | fun zoomTo(pZoomLevel: Double, animationSpeed: Long? = null): Boolean =
76 | getController().zoomTo(pZoomLevel, animationSpeed)
77 |
78 | fun zoomToSpan(latSpan: Double, lonSpan: Double) = getController().zoomToSpan(latSpan, lonSpan)
79 |
80 | fun animateTo(point: GeoPoint, pZoom: Double? = null, pSpeed: Long? = null) =
81 | getController().animateTo(point, pZoom, pSpeed)
82 |
83 | fun animateTo(point: GeoPoint, pZoom: Double? = null, pSpeed: Long? = null, pOrientation: Float = 0f) =
84 | getController().animateTo(point, pZoom, pSpeed, pOrientation)
85 |
86 | fun zoomToBoundingBox(boundingBox: BoundingBox, animated: Boolean) =
87 | map?.zoomToBoundingBox(boundingBox, animated)
88 |
89 | fun animateTo(
90 | point: GeoPoint,
91 | pZoom: Double? = null,
92 | pSpeed: Long? = null,
93 | pOrientation: Float = 0f,
94 | pClockwise: Boolean = false
95 | ) = getController().animateTo(point, pZoom, pSpeed, pOrientation, pClockwise)
96 |
97 | fun normalizeRotation() {
98 | getController().animateTo(geoPoint, zoom, null, 0f)
99 | }
100 |
101 | companion object {
102 | val Saver: Saver = Saver(
103 | save = { it.prop },
104 | restore = { CameraState(it) }
105 | )
106 | }
107 | }
108 |
109 | @Composable
110 | fun rememberCameraState(
111 | key: String? = null,
112 | cameraProperty: CameraProperty.() -> Unit = {}
113 | ): CameraState = rememberSaveable(key = key, saver = CameraState.Saver) {
114 | val prop = CameraProperty().apply(cameraProperty)
115 | CameraState(prop)
116 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MapApplier.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.AbstractApplier
4 |
5 | internal class MapApplier(
6 | val mapView: OsmMapView
7 | ) : AbstractApplier(OsmNodeRoot) {
8 |
9 | private val decorations = mutableListOf()
10 |
11 | override fun insertBottomUp(index: Int, instance: OsmAndNode) {
12 | decorations.add(index, instance)
13 | instance.onAttached()
14 | }
15 |
16 | override fun insertTopDown(index: Int, instance: OsmAndNode) {
17 | }
18 |
19 | override fun move(from: Int, to: Int, count: Int) {
20 | decorations.move(from, to, count)
21 | }
22 |
23 | override fun onClear() {
24 | mapView.overlayManager.clear()
25 | decorations.forEach { it.onCleared() }
26 | decorations.clear()
27 | }
28 |
29 | override fun remove(index: Int, count: Int) {
30 | repeat(count) {
31 | decorations[index + it].onRemoved()
32 | }
33 | decorations.remove(index, count)
34 | }
35 |
36 | internal fun invalidate() = mapView.postInvalidate()
37 |
38 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MapListeners.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import org.osmdroid.util.GeoPoint
7 |
8 | internal class MapListeners {
9 | var onMapClick: (GeoPoint) -> Unit by mutableStateOf({})
10 | var onMapLongClick: (GeoPoint) -> Unit by mutableStateOf({})
11 | var onFirstLoadListener: (String) -> Unit by mutableStateOf({})
12 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MapProperties.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import org.osmdroid.tileprovider.tilesource.ITileSource
4 | import org.osmdroid.views.overlay.OverlayManager
5 |
6 | data class MapProperties(
7 | val mapOrientation: Float = 0f,
8 | val isMultiTouchControls: Boolean = true,
9 | val isAnimating: Boolean = true,
10 | val minZoomLevel: Double = 6.0,
11 | val maxZoomLevel: Double = 29.0,
12 | val isFlingEnable: Boolean = true,
13 | val isEnableRotationGesture: Boolean = false,
14 | val isUseDataConnection: Boolean = true,
15 | val isTilesScaledToDpi: Boolean = false,
16 | val tileSources: ITileSource? = null,
17 | val overlayManager: OverlayManager? = null,
18 | val zoomButtonVisibility: ZoomButtonVisibility = ZoomButtonVisibility.ALWAYS
19 | )
20 |
21 | val DefaultMapProperties = MapProperties()
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MapPropertiesNode.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import org.osmdroid.events.DelayedMapListener
4 | import org.osmdroid.events.MapEventsReceiver
5 | import org.osmdroid.events.MapListener
6 | import org.osmdroid.events.ScrollEvent
7 | import org.osmdroid.events.ZoomEvent
8 | import org.osmdroid.util.GeoPoint
9 | import org.osmdroid.views.overlay.MapEventsOverlay
10 |
11 | internal class MapPropertiesNode(
12 | val mapViewComposed: OsmMapView,
13 | val mapListeners: MapListeners,
14 | private val cameraState: CameraState,
15 | overlayManagerState: OverlayManagerState
16 | ) : OsmAndNode {
17 |
18 | private var delayedMapListener: DelayedMapListener? = null
19 | private var eventOverlay: MapEventsOverlay? = null
20 |
21 | init {
22 | overlayManagerState.setMap(mapViewComposed)
23 | cameraState.setMap(mapViewComposed)
24 | }
25 |
26 | override fun onAttached() {
27 | mapViewComposed.controller.setCenter(cameraState.geoPoint)
28 | mapViewComposed.controller.setZoom(cameraState.zoom)
29 |
30 | delayedMapListener = DelayedMapListener(object : MapListener {
31 | override fun onScroll(event: ScrollEvent?): Boolean {
32 | val currentGeoPoint =
33 | mapViewComposed.let { GeoPoint(it.mapCenter.latitude, it.mapCenter.longitude) }
34 | cameraState.geoPoint = currentGeoPoint
35 | return false
36 | }
37 |
38 | override fun onZoom(event: ZoomEvent?): Boolean {
39 | val currentZoom = mapViewComposed.zoomLevelDouble
40 | cameraState.zoom = currentZoom
41 | return false
42 | }
43 | }, 1000L)
44 |
45 | mapViewComposed.addMapListener(delayedMapListener)
46 |
47 | val eventsReceiver = object : MapEventsReceiver {
48 | override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
49 | p?.let { mapListeners.onMapClick.invoke(it) }
50 | return true
51 | }
52 |
53 | override fun longPressHelper(p: GeoPoint?): Boolean {
54 | p?.let { mapListeners.onMapLongClick.invoke(it) }
55 | return true
56 | }
57 | }
58 |
59 | eventOverlay = MapEventsOverlay(eventsReceiver)
60 |
61 | mapViewComposed.overlayManager.add(eventOverlay)
62 |
63 | if (mapViewComposed.isLayoutOccurred) {
64 | mapListeners.onFirstLoadListener.invoke("")
65 | }
66 | }
67 |
68 | override fun onCleared() {
69 | super.onCleared()
70 | delayedMapListener?.let { mapViewComposed.removeMapListener(it) }
71 | eventOverlay?.let { mapViewComposed.overlayManager.remove(eventOverlay) }
72 | }
73 |
74 | override fun onRemoved() {
75 | super.onRemoved()
76 | delayedMapListener?.let { mapViewComposed.removeMapListener(it) }
77 | eventOverlay?.let { mapViewComposed.overlayManager.remove(eventOverlay) }
78 | }
79 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MapViewUpdater.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import androidx.compose.runtime.currentComposer
6 | import org.osmdroid.views.CustomZoomButtonsController
7 | import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
8 |
9 | @Composable
10 | internal fun MapViewUpdater(
11 | mapProperties: MapProperties,
12 | mapListeners: MapListeners,
13 | cameraState: CameraState,
14 | overlayManagerState: OverlayManagerState
15 | ) {
16 | val mapViewComposed = (currentComposer.applier as MapApplier).mapView
17 |
18 | ComposeNode(factory = {
19 | MapPropertiesNode(mapViewComposed, mapListeners, cameraState, overlayManagerState)
20 | }, update = {
21 |
22 | set(mapProperties.mapOrientation) { mapViewComposed.mapOrientation = it }
23 | set(mapProperties.isMultiTouchControls) { mapViewComposed.setMultiTouchControls(it) }
24 | set(mapProperties.minZoomLevel) { mapViewComposed.minZoomLevel = it }
25 | set(mapProperties.maxZoomLevel) { mapViewComposed.maxZoomLevel = it }
26 | set(mapProperties.isFlingEnable) { mapViewComposed.isFlingEnabled = it }
27 | set(mapProperties.isUseDataConnection) { mapViewComposed.setUseDataConnection(it) }
28 | set(mapProperties.isTilesScaledToDpi) { mapViewComposed.isTilesScaledToDpi = it }
29 | set(mapProperties.tileSources) { if (it != null) mapViewComposed.setTileSource(it) }
30 | set(mapProperties.overlayManager) { if (it != null) mapViewComposed.overlayManager = it }
31 |
32 | set(mapProperties.isEnableRotationGesture) {
33 | val rotationGesture = RotationGestureOverlay(mapViewComposed)
34 | rotationGesture.isEnabled = it
35 | mapViewComposed.overlayManager.add(rotationGesture)
36 | }
37 |
38 | set(mapProperties.zoomButtonVisibility) {
39 | val visibility = when (it) {
40 | ZoomButtonVisibility.ALWAYS -> CustomZoomButtonsController.Visibility.ALWAYS
41 | ZoomButtonVisibility.SHOW_AND_FADEOUT -> CustomZoomButtonsController.Visibility.SHOW_AND_FADEOUT
42 | ZoomButtonVisibility.NEVER -> CustomZoomButtonsController.Visibility.NEVER
43 | }
44 |
45 | mapViewComposed.zoomController.setVisibility(visibility)
46 | }
47 | })
48 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/Marker.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ComposeNode
6 | import androidx.compose.runtime.currentComposer
7 | import androidx.compose.ui.platform.ComposeView
8 | import androidx.compose.ui.platform.LocalContext
9 | import org.osmdroid.views.overlay.Marker
10 |
11 | data class InfoWindowData(
12 | val title: String,
13 | val snippet: String
14 | )
15 |
16 | @Composable
17 | @OsmAndroidComposable
18 | fun Marker(
19 | state: MarkerState = rememberMarkerState(),
20 | icon: Drawable? = null,
21 | visible: Boolean = true,
22 | title: String? = null,
23 | snippet: String? = null,
24 | onClick: (Marker) -> Boolean = { false },
25 | id: String? = null,
26 | infoWindowContent: @Composable (InfoWindowData) -> Unit = {}
27 | ) {
28 |
29 | val context = LocalContext.current
30 | val applier = currentComposer.applier as? MapApplier ?: throw IllegalStateException("Invalid Applier")
31 |
32 | ComposeNode(
33 | factory = {
34 | val mapView = applier.mapView
35 | val marker = Marker(mapView).apply {
36 | position = state.geoPoint
37 | rotation = state.rotation
38 |
39 | setVisible(visible)
40 | icon?.let { this.icon = it }
41 | id?.let { this.id = it }
42 | }
43 |
44 | mapView.overlayManager.add(marker)
45 |
46 | val composeView = ComposeView(context)
47 | .apply {
48 | setContent {
49 | infoWindowContent.invoke(InfoWindowData(title.orEmpty(), snippet.orEmpty()))
50 | }
51 | }
52 |
53 | val infoWindow = OsmInfoWindow(composeView, mapView)
54 | infoWindow.view.setOnClickListener {
55 | if (infoWindow.isOpen) infoWindow.close()
56 | }
57 | marker.infoWindow = infoWindow
58 |
59 | MarkerNode(
60 | mapView = mapView,
61 | markerState = state,
62 | marker = marker,
63 | onMarkerClick = onClick
64 | ).also { it.setupListeners() }
65 | },
66 | update = {
67 | update(state.geoPoint) {
68 | marker.position = it
69 | }
70 | update(state.rotation) {
71 | marker.rotation = it
72 | }
73 | update(icon) {
74 | if (it == null) {
75 | marker.setDefaultIcon()
76 | } else {
77 | marker.icon = it
78 | }
79 | }
80 | update(visible) {
81 | marker.setVisible(it)
82 | }
83 | applier.invalidate()
84 | })
85 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MarkerLabeled.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ComposeNode
6 | import androidx.compose.runtime.currentComposer
7 | import androidx.compose.ui.platform.ComposeView
8 | import androidx.compose.ui.platform.LocalContext
9 | import com.utsman.osmandcompose.extendedosm.MarkerWithLabel
10 | import com.utsman.osmandcompose.model.LabelProperties
11 | import org.osmdroid.views.overlay.Marker
12 |
13 | /**
14 | * Marker with label with default parameters that can be customized
15 | *
16 | * Parameters:
17 | * - state: MarkerState = rememberMarkerState()
18 | * - icon: Drawable? = null
19 | * - visible: Boolean = true
20 | * - title: String? = null
21 | * - snippet: String? = null
22 | * - onClick: (Marker) -> Boolean = { false }
23 | * - id: String? = null
24 | * - label : String? = null
25 | * - labelProperties: LabelProperties = LabelProperties()
26 | * - infoWindowContent: @Composable (InfoWindowData) -> Unit = {}
27 | * */
28 |
29 | @Composable
30 | @OsmAndroidComposable
31 | fun MarkerLabeled(
32 | state: MarkerState = rememberMarkerState(),
33 | icon: Drawable? = null,
34 | visible: Boolean = true,
35 | title: String? = null,
36 | snippet: String? = null,
37 | onClick: (Marker) -> Boolean = { false },
38 | id: String? = null,
39 | label : String? = null,
40 | labelProperties: LabelProperties = LabelProperties(),
41 | infoWindowContent: @Composable (InfoWindowData) -> Unit = {}
42 | ) {
43 | val context = LocalContext.current
44 | val applier = currentComposer.applier as? MapApplier ?: throw IllegalStateException("Invalid Applier")
45 |
46 | ComposeNode(
47 | factory = {
48 | val mapView = applier.mapView
49 | val marker = MarkerWithLabel(
50 | mapView,
51 | label,
52 | labelProperties
53 | ).apply {
54 | position = state.geoPoint
55 | rotation = state.rotation
56 |
57 | setVisible(visible)
58 | icon?.let { this.icon = it }
59 | id?.let { this.id = it }
60 | if(icon == null)
61 | setTextIcon(title)
62 | else{
63 | this.icon = icon
64 | }
65 | }
66 |
67 | mapView.overlayManager.add(marker)
68 |
69 | val composeView = ComposeView(context)
70 | .apply {
71 | setContent {
72 | infoWindowContent.invoke(InfoWindowData(title.orEmpty(), snippet.orEmpty()))
73 | }
74 | }
75 |
76 | val infoWindow = OsmInfoWindow(composeView, mapView)
77 | infoWindow.view.setOnClickListener {
78 | if (infoWindow.isOpen) infoWindow.close()
79 | }
80 | marker.infoWindow = infoWindow
81 | MarkerNode(
82 | mapView = mapView,
83 | markerState = state,
84 | marker = marker,
85 | onMarkerClick = onClick
86 | ).also { it.setupListeners() }
87 | },
88 | update = {
89 | update(state.geoPoint) {
90 | marker.position = it
91 | }
92 | update(state.rotation) {
93 | marker.rotation = it
94 | }
95 | update(icon) {
96 | if (it == null) {
97 | marker.setDefaultIcon()
98 | } else {
99 | marker.icon = it
100 | }
101 | }
102 | update(visible) {
103 | marker.setVisible(it)
104 | }
105 | applier.invalidate()
106 | })
107 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MarkerNode.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.CompositionContext
4 | import org.osmdroid.views.overlay.Marker
5 |
6 | internal class MarkerNode(
7 | val mapView: OsmMapView,
8 | val markerState: MarkerState,
9 | val marker: Marker,
10 | var onMarkerClick: (Marker) -> Boolean
11 | ) : OsmAndNode {
12 |
13 | override fun onAttached() {
14 | markerState.marker = marker
15 | }
16 |
17 | override fun onRemoved() {
18 | markerState.marker = null
19 | marker.remove(mapView)
20 | }
21 |
22 | override fun onCleared() {
23 | markerState.marker = null
24 | marker.remove(mapView)
25 | }
26 |
27 | fun setupListeners() {
28 | marker.setOnMarkerClickListener { marker, _ ->
29 | val click = onMarkerClick.invoke(marker)
30 | if (marker.isInfoWindowShown) {
31 | marker.closeInfoWindow()
32 | } else {
33 | marker.showInfoWindow()
34 | }
35 | click
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/MarkerState.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.saveable.Saver
8 | import androidx.compose.runtime.saveable.rememberSaveable
9 | import androidx.compose.runtime.setValue
10 | import org.osmdroid.util.GeoPoint
11 | import org.osmdroid.views.overlay.Marker
12 |
13 | class MarkerState(geoPoint: GeoPoint = GeoPoint(0.0, 0.0), rotation: Float = 0f) {
14 | var geoPoint: GeoPoint by mutableStateOf(geoPoint)
15 | var rotation: Float by mutableStateOf(rotation)
16 |
17 | private val markerState: MutableState = mutableStateOf(null)
18 |
19 | var marker: Marker?
20 | get() = markerState.value
21 | set(value) {
22 | if (markerState.value == null && value == null) return
23 | if (markerState.value != null && value != null) {
24 | error("MarkerState may only be associated with one Marker at a time.")
25 | }
26 | markerState.value = value
27 | }
28 |
29 | companion object {
30 | val Saver: Saver> = Saver(
31 | save = {
32 | Pair(it.geoPoint, it.rotation)
33 | },
34 | restore = { MarkerState(it.first, it.second) }
35 | )
36 | }
37 | }
38 |
39 | @Composable
40 | fun rememberMarkerState(
41 | key: String? = null,
42 | geoPoint: GeoPoint = GeoPoint(0.0, 0.0),
43 | rotation: Float = 0f
44 | ): MarkerState = rememberSaveable(key = key, saver = MarkerState.Saver) {
45 | MarkerState(geoPoint, rotation)
46 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/OpenStreetMap.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.content.Context
4 | import androidx.compose.foundation.layout.LayoutScopeMarker
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.Composition
7 | import androidx.compose.runtime.CompositionContext
8 | import androidx.compose.runtime.DisposableEffect
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.SideEffect
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.rememberCompositionContext
14 | import androidx.compose.runtime.rememberUpdatedState
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.platform.LocalLifecycleOwner
18 | import androidx.compose.ui.viewinterop.AndroidView
19 | import androidx.lifecycle.Lifecycle
20 | import androidx.lifecycle.LifecycleEventObserver
21 | import kotlinx.coroutines.awaitCancellation
22 | import org.osmdroid.events.MapListener
23 | import org.osmdroid.tileprovider.MapTileProviderBasic
24 | import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
25 | import org.osmdroid.tileprovider.tilesource.TileSourceFactory
26 | import org.osmdroid.util.GeoPoint
27 | import org.osmdroid.util.MapTileIndex
28 | import org.osmdroid.views.CustomZoomButtonsController
29 | import org.osmdroid.views.MapView
30 | import org.osmdroid.views.overlay.TilesOverlay
31 |
32 | internal typealias OsmMapView = MapView
33 |
34 | @Composable
35 | fun rememberMapViewWithLifecycle(vararg mapListener: MapListener): OsmMapView {
36 | val context = LocalContext.current
37 | val mapView = remember {
38 | OsmMapView(context)
39 | }
40 |
41 | val lifecycleObserver = rememberMapLifecycleObserver(context, mapView, *mapListener)
42 | val lifecycle = LocalLifecycleOwner.current.lifecycle
43 | DisposableEffect(lifecycle) {
44 | lifecycle.addObserver(lifecycleObserver)
45 | onDispose {
46 | lifecycle.removeObserver(lifecycleObserver)
47 | }
48 | }
49 |
50 | return mapView
51 | }
52 |
53 | @Composable
54 | fun rememberMapLifecycleObserver(
55 | context: Context,
56 | mapView: OsmMapView,
57 | vararg mapListener: MapListener
58 | ): LifecycleEventObserver =
59 | remember(mapView) {
60 | LifecycleEventObserver { _, event ->
61 | when (event) {
62 | Lifecycle.Event.ON_CREATE -> {
63 | org.osmdroid.config.Configuration.getInstance()
64 | .load(context, context.getSharedPreferences("osm", Context.MODE_PRIVATE))
65 | }
66 |
67 | Lifecycle.Event.ON_RESUME -> mapView.onResume()
68 | Lifecycle.Event.ON_PAUSE -> mapView.onPause()
69 | Lifecycle.Event.ON_DESTROY -> {
70 | mapListener.onEach { mapView.removeMapListener(it) }
71 | }
72 |
73 | else -> {}
74 | }
75 | }
76 | }
77 |
78 | @LayoutScopeMarker
79 | interface OsmAndroidScope
80 |
81 | // public enum Visibility {ALWAYS, NEVER, SHOW_AND_FADEOUT}
82 |
83 | enum class ZoomButtonVisibility {
84 | ALWAYS, NEVER, SHOW_AND_FADEOUT
85 | }
86 |
87 | @Composable
88 | fun OpenStreetMap(
89 | modifier: Modifier = Modifier,
90 | cameraState: CameraState = rememberCameraState(),
91 | overlayManagerState: OverlayManagerState = rememberOverlayManagerState(),
92 | properties: MapProperties = DefaultMapProperties,
93 | onMapClick: (GeoPoint) -> Unit = {},
94 | onMapLongClick: (GeoPoint) -> Unit = {},
95 | onFirstLoadListener: () -> Unit = {},
96 | content: (@Composable @OsmAndroidComposable OsmAndroidScope.() -> Unit)? = null
97 | ) {
98 |
99 | val mapView = rememberMapViewWithLifecycle()
100 |
101 | val mapListeners = remember {
102 | MapListeners()
103 | }.also {
104 | it.onMapClick = onMapClick
105 | it.onMapLongClick = onMapLongClick
106 | it.onFirstLoadListener = {
107 | onFirstLoadListener.invoke()
108 | }
109 | }
110 |
111 | val mapProperties by rememberUpdatedState(properties)
112 |
113 | val parentComposition = rememberCompositionContext()
114 | val currentContent by rememberUpdatedState(content)
115 |
116 | LaunchedEffect(Unit) {
117 | disposingComposition {
118 | mapView.newComposition(parentComposition) {
119 | MapViewUpdater(mapProperties, mapListeners, cameraState, overlayManagerState)
120 | currentContent?.invoke(object : OsmAndroidScope {})
121 | }
122 | }
123 | }
124 |
125 | AndroidView(
126 | modifier = modifier,
127 | factory = {
128 | mapView
129 | },
130 | update = {
131 | it.controller.animateTo(
132 | cameraState.geoPoint,
133 | cameraState.zoom,
134 | cameraState.speed
135 | )
136 | }
137 | )
138 | }
139 |
140 | internal suspend inline fun disposingComposition(factory: () -> Composition) {
141 | val composition = factory()
142 | try {
143 | awaitCancellation()
144 | } finally {
145 | composition.dispose()
146 | }
147 | }
148 |
149 | private fun OsmMapView.newComposition(
150 | parent: CompositionContext,
151 | content: @Composable () -> Unit
152 | ): Composition {
153 | return Composition(
154 | MapApplier(this), parent
155 | ).apply {
156 | setContent(content)
157 | }
158 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/OsmAndNode.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | internal interface OsmAndNode {
4 | fun onAttached() {}
5 | fun onRemoved() {}
6 | fun onCleared() {}
7 | }
8 |
9 | internal object OsmNodeRoot : OsmAndNode
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/OsmAndroidComposable.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import androidx.compose.runtime.ComposableTargetMarker
4 |
5 | @Retention(AnnotationRetention.BINARY)
6 | @ComposableTargetMarker(description = "OsmAnd Composable")
7 | @Target(
8 | AnnotationTarget.FILE,
9 | AnnotationTarget.FUNCTION,
10 | AnnotationTarget.PROPERTY_GETTER,
11 | AnnotationTarget.TYPE,
12 | AnnotationTarget.TYPE_PARAMETER,
13 | )
14 | annotation class OsmAndroidComposable
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/OsmInfoWindow.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.view.View
4 | import org.osmdroid.views.overlay.infowindow.InfoWindow
5 |
6 | class OsmInfoWindow(view: View, mapView: OsmMapView) : InfoWindow(view, mapView) {
7 | override fun onOpen(item: Any?) {
8 | }
9 |
10 | override fun onClose() {
11 | }
12 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/OverlayManagerState.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.runtime.saveable.Saver
7 | import org.osmdroid.views.MapView
8 | import org.osmdroid.views.overlay.OverlayManager
9 |
10 | @SuppressLint("MutableCollectionMutableState")
11 | class OverlayManagerState(private var _overlayManager: OverlayManager?) {
12 |
13 | val overlayManager: OverlayManager
14 | get() = _overlayManager
15 | ?: throw IllegalStateException("Invalid Map attached!, please add other overlay in OpenStreetMap#onFirstLoadListener")
16 |
17 | private var _mapView: MapView? = null
18 | fun setMap(mapView: MapView) {
19 | _overlayManager = mapView.overlayManager
20 | _mapView = mapView
21 | }
22 |
23 | fun getMap(): MapView {
24 | return _mapView ?: throw IllegalStateException("Invalid Map attached!")
25 | }
26 | }
27 |
28 | @Composable
29 | fun rememberOverlayManagerState() = remember {
30 | OverlayManagerState(null)
31 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/Polygon.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.graphics.Paint
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ComposeNode
6 | import androidx.compose.runtime.currentComposer
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.graphics.toArgb
10 | import androidx.compose.ui.platform.ComposeView
11 | import androidx.compose.ui.platform.LocalContext
12 | import org.osmdroid.util.GeoPoint
13 | import org.osmdroid.views.overlay.Polygon
14 |
15 | @Composable
16 | @OsmAndroidComposable
17 | fun Polygon(
18 | geoPoints: List,
19 | geoPointHoles: List> = emptyList(),
20 | color: Color = Color.Black,
21 | width: Float = 12f,
22 | outlineColor: Color = Color.Gray,
23 | visible: Boolean = true,
24 | onClick: (Polygon) -> Unit = {},
25 | title: String? = null,
26 | snippet: String? = null,
27 | id: String? = null,
28 | onPolygonLoaded: (outlinePaint: Paint, fillPaint: Paint) -> Unit = {_, _ ->},
29 | infoWindowContent: @Composable (InfoWindowData) -> Unit = {}
30 | ) {
31 |
32 | val context = LocalContext.current
33 | val applier =
34 | currentComposer.applier as? MapApplier ?: throw IllegalStateException("Invalid Applier")
35 |
36 | val point = remember {
37 | geoPoints + geoPoints[0]
38 | }
39 |
40 | val holes = remember {
41 | if (geoPointHoles.isNotEmpty()) {
42 | geoPointHoles.map {
43 | val newHole = if (it.isNotEmpty()) {
44 | it + it[0]
45 | } else {
46 | it
47 | }
48 | newHole
49 | }
50 | } else {
51 | geoPointHoles
52 | }
53 | }
54 |
55 | ComposeNode(
56 | factory = {
57 | val mapView = applier.mapView
58 | val polygon = Polygon(mapView)
59 | polygon.apply {
60 | points = point
61 | outlinePaint.color = outlineColor.toArgb()
62 | fillPaint.color = color.toArgb()
63 |
64 | outlinePaint.strokeWidth = width
65 |
66 | isVisible = visible
67 | id?.let { this.id = id }
68 |
69 | mapView.overlayManager.add(this)
70 | onPolygonLoaded.invoke(outlinePaint, fillPaint)
71 |
72 | infoWindow = null
73 | setHoles(holes)
74 | }
75 |
76 | val composeView = ComposeView(context)
77 | .apply {
78 | setContent {
79 | infoWindowContent.invoke(InfoWindowData(title.orEmpty(), snippet.orEmpty()))
80 | }
81 | }
82 |
83 | val infoWindow = OsmInfoWindow(composeView, mapView)
84 | infoWindow.view.setOnClickListener {
85 | if (infoWindow.isOpen) infoWindow.close()
86 | }
87 | polygon.infoWindow = infoWindow
88 |
89 | PolygonNode(mapView, polygon, onClick).also { it.setupListeners() }
90 | }, update = {
91 | set(geoPoints) { polygon.points = it }
92 | set(color) { polygon.fillPaint.color = it.toArgb() }
93 | set(outlineColor) { polygon.outlinePaint.color = it.toArgb() }
94 |
95 | update(visible) { polygon.isVisible = visible }
96 | })
97 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/PolygonNode.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import org.osmdroid.views.overlay.Polygon
4 |
5 | internal class PolygonNode(
6 | private val mapView: OsmMapView,
7 | val polygon: Polygon,
8 | var onPolylineClick: (Polygon) -> Unit
9 | ) : OsmAndNode {
10 |
11 | override fun onRemoved() {
12 | super.onRemoved()
13 | mapView.overlayManager.remove(polygon)
14 | }
15 |
16 | fun setupListeners() {
17 | polygon.setOnClickListener { polygon, _, _ ->
18 | onPolylineClick.invoke(polygon)
19 | if (polygon.isInfoWindowOpen) {
20 | polygon.closeInfoWindow()
21 | } else {
22 | polygon.showInfoWindow()
23 | }
24 | true
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/Polyline.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import android.graphics.Paint
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ComposeNode
6 | import androidx.compose.runtime.currentComposer
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.toArgb
9 | import androidx.compose.ui.platform.ComposeView
10 | import androidx.compose.ui.platform.LocalContext
11 | import org.osmdroid.util.GeoPoint
12 | import org.osmdroid.views.overlay.Polyline
13 |
14 | enum class PolylineCap {
15 | BUTT, ROUND, SQUARE
16 | }
17 |
18 | @Composable
19 | @OsmAndroidComposable
20 | fun Polyline(
21 | geoPoints: List,
22 | color: Color = Color.Black,
23 | width: Float = 12f,
24 | cap: PolylineCap = PolylineCap.SQUARE,
25 | visible: Boolean = true,
26 | onClick: (Polyline) -> Unit = {},
27 | title: String? = null,
28 | snippet: String? = null,
29 | id: String? = null,
30 | onPolylineLoaded: (Paint) -> Unit = {},
31 | infoWindowContent: @Composable (InfoWindowData) -> Unit = {}
32 | ) {
33 |
34 | val context = LocalContext.current
35 | val applier =
36 | currentComposer.applier as? MapApplier ?: throw IllegalStateException("Invalid Applier")
37 |
38 | ComposeNode(
39 | factory = {
40 | val mapView = applier.mapView
41 | val polyline = Polyline(mapView)
42 | polyline.apply {
43 | setPoints(geoPoints)
44 | outlinePaint.color = color.toArgb()
45 | outlinePaint.strokeWidth = width
46 |
47 | outlinePaint.strokeCap = when (cap) {
48 | PolylineCap.BUTT -> Paint.Cap.BUTT
49 | PolylineCap.ROUND -> Paint.Cap.ROUND
50 | PolylineCap.SQUARE -> Paint.Cap.SQUARE
51 | }
52 |
53 | isVisible = visible
54 | id?.let { this.id = id }
55 |
56 | mapView.overlayManager.add(this)
57 | onPolylineLoaded.invoke(outlinePaint)
58 |
59 | infoWindow = null
60 | }
61 |
62 | val composeView = ComposeView(context)
63 | .apply {
64 | setContent {
65 | infoWindowContent.invoke(InfoWindowData(title.orEmpty(), snippet.orEmpty()))
66 | }
67 | }
68 |
69 | val infoWindow = OsmInfoWindow(composeView, mapView)
70 | infoWindow.view.setOnClickListener {
71 | if (infoWindow.isOpen) infoWindow.close()
72 | }
73 | polyline.infoWindow = infoWindow
74 |
75 | PolylineNode(mapView, polyline, onClick).also { it.setupListeners() }
76 | }, update = {
77 | set(geoPoints) { polyline.setPoints(it) }
78 | set(color) { polyline.outlinePaint.color = it.toArgb() }
79 |
80 | update(visible) { polyline.isVisible = visible }
81 | })
82 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/PolylineNode.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import org.osmdroid.views.overlay.Polyline
4 |
5 | internal class PolylineNode(
6 | private val mapView: OsmMapView,
7 | val polyline: Polyline,
8 | var onPolylineClick: (Polyline) -> Unit
9 | ) : OsmAndNode {
10 |
11 | override fun onRemoved() {
12 | super.onRemoved()
13 | mapView.overlayManager.remove(polyline)
14 | }
15 |
16 | fun setupListeners() {
17 | polyline.setOnClickListener { polyline, _, _ ->
18 | onPolylineClick.invoke(polyline)
19 | if (polyline.isInfoWindowOpen) {
20 | polyline.closeInfoWindow()
21 | } else {
22 | polyline.showInfoWindow()
23 | }
24 | true
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/extendedosm/MarkerWithLabel.java:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose.extendedosm;
2 |
3 |
4 | import android.graphics.Canvas;
5 | import android.graphics.Point;
6 |
7 |
8 | import org.osmdroid.views.MapView;
9 | import org.osmdroid.views.overlay.Marker;
10 | import android.graphics.Paint;
11 |
12 | import com.utsman.osmandcompose.model.LabelProperties;
13 |
14 | public class MarkerWithLabel extends Marker {
15 | Paint textPaint = null;
16 | String mLabel = null;
17 |
18 | float mTextOffsetY;
19 | public MarkerWithLabel(MapView mapView, String label, LabelProperties labelProperties) {
20 | super( mapView);
21 | mLabel = label;
22 | textPaint = new Paint();
23 | textPaint.setColor(labelProperties.getLabelColor());
24 | textPaint.setTextSize(labelProperties.getLabelTextSize());
25 | textPaint.setAntiAlias(labelProperties.getLabelAntiAlias());
26 | textPaint.setTextAlign(labelProperties.getLabelAlign());
27 | mTextOffsetY = labelProperties.getLabelTextOffset();
28 | }
29 | public void draw(final Canvas c, final MapView osmv, boolean shadow) {
30 | draw( c, osmv);
31 | }
32 | public void draw( final Canvas c, final MapView osmv) {
33 | super.draw( c, osmv, false);
34 | Point p = this.mPositionPixels; // already provisioned by Marker
35 | c.drawText( mLabel, p.x, p.y+mTextOffsetY, textPaint);
36 | }
37 | }
--------------------------------------------------------------------------------
/osm-compose/src/main/java/com/utsman/osmandcompose/model/LabelProperties.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose.model
2 |
3 |
4 | //class LabelProperties for Marker with label
5 | //With default parameters that can be customized
6 | data class LabelProperties(
7 | val labelColor : Int = android.graphics.Color.BLACK,
8 | val labelTextSize : Float = 40f,
9 | val labelAntiAlias : Boolean = true,
10 | val labelAlign : android.graphics.Paint.Align = android.graphics.Paint.Align.CENTER,
11 | val labelTextOffset : Float = 30f
12 | )
13 |
--------------------------------------------------------------------------------
/osm-compose/src/test/java/com/utsman/osmandcompose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.osmandcompose
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Osm Android Compose"
16 | include ':app'
17 | include ':osm-compose'
18 |
--------------------------------------------------------------------------------