├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── utsman │ │ └── osmapp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── utsman │ │ │ └── osmapp │ │ │ ├── Coordinates.kt │ │ │ ├── Logger.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MarkerPage.kt │ │ │ ├── PolygonPage.kt │ │ │ ├── PolylineEncoderDecoder.java │ │ │ ├── PolylinePage.kt │ │ │ ├── SimplePage.kt │ │ │ ├── navigation │ │ │ ├── Navigation.kt │ │ │ ├── NavigationRoute.kt │ │ │ └── Route.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── round_eject_24.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── utsman │ └── osmapp │ └── ExampleUnitTest.kt ├── build.gradle ├── docs ├── getting-started.md ├── images │ ├── info-window.png │ ├── marker-custom.png │ ├── marker-default.png │ ├── polygon.png │ ├── polyline-custom-1.png │ ├── polyline-paint.png │ ├── polyline.png │ └── simple-maps.png ├── index.md ├── marker.md ├── polyline-polygon.md └── usage.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── osm-compose ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── utsman │ │ └── osmandcompose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── utsman │ │ └── osmandcompose │ │ ├── CameraState.kt │ │ ├── MapApplier.kt │ │ ├── MapListeners.kt │ │ ├── MapProperties.kt │ │ ├── MapPropertiesNode.kt │ │ ├── MapViewUpdater.kt │ │ ├── Marker.kt │ │ ├── MarkerLabeled.kt │ │ ├── MarkerNode.kt │ │ ├── MarkerState.kt │ │ ├── OpenStreetMap.kt │ │ ├── OsmAndNode.kt │ │ ├── OsmAndroidComposable.kt │ │ ├── OsmInfoWindow.kt │ │ ├── OverlayManagerState.kt │ │ ├── Polygon.kt │ │ ├── PolygonNode.kt │ │ ├── Polyline.kt │ │ ├── PolylineNode.kt │ │ ├── extendedosm │ │ └── MarkerWithLabel.java │ │ └── model │ │ └── LabelProperties.kt │ └── test │ └── java │ └── com │ └── utsman │ └── osmandcompose │ └── ExampleUnitTest.kt └── settings.gradle /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.x 17 | - uses: actions/cache@v2 18 | with: 19 | key: ${{ github.ref }} 20 | path: .cache 21 | - run: pip install mkdocs-material 22 | - run: pip install mkdocs-glightbox 23 | - run: mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Osm Android Compose -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenStreetMap Android Compose 2 | 3 | For install and usage, please check [documentation](https://utsmannn.github.io/osm-android-compose/) 4 | 5 | # Welcome 6 | 7 | The origin OpenStreetMaps Android visit [https://osmdroid.github.io/osmdroid/](https://osmdroid.github.io/osmdroid/) or [github wiki](https://github.com/osmdroid/osmdroid/wiki) 8 | 9 | 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. 10 | 11 | ## Contributing 12 | 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. 13 | 14 | ## License 15 | ``` 16 | Copyright 2023 Muhammad Utsman 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.utsman.osmapp' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.utsman.osmapp" 12 | minSdk 24 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.2.0' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.7.0' 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 53 | implementation 'androidx.activity:activity-compose:1.3.1' 54 | implementation "androidx.compose.ui:ui:$compose_ui_version" 55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" 56 | implementation 'androidx.compose.material:material:1.2.0' 57 | testImplementation 'junit:junit:4.13.2' 58 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 60 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" 61 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" 62 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" 63 | 64 | implementation "androidx.navigation:navigation-compose:2.5.3" 65 | 66 | implementation project(":osm-compose") 67 | implementation 'org.osmdroid:osmdroid-android:6.1.16' 68 | } -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/utsman/osmapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.osmapp 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.osmapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/utsman/osmapp/Coordinates.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.osmapp 2 | 3 | import org.osmdroid.util.GeoPoint 4 | 5 | object Coordinates { 6 | 7 | private val depokJakartaRoutesPolyline = "BGvjumMyq93rGkCO8L4DwMjSwMjSkIrO4D7GoLoG0KkI4IkI0F0FkNwMwM4NkNsOgKgK8GwHsEsE4IkIgPsO8LsJwC8BsJ0F0F4DkN8G8LoG8QsJ0FkDgFwC4IsEsEwCgKgFoG4DwH4DoGkD0FwCwWoLsE8B4D8BwH4DwHsE0FkDsEwC4csO4SsJ0KsEgK4DgFwCoG4DgFwC0FwC8GkDgFwC4I4D0PkI0U8LoL0F0K8GgU4NsEkDwbkX0UsTwHwHkSsOsOoL8L0KwH8G0KkIwM4IwM8G0K0FgKsE8L0FgU8G0PoGsO0FouBwRgoBoQsO0FsiBkN0F8BsJ4D8gD8kB0P0F0FwC0K4D8LsEsnBgP4IkDkIkD4hB4N0UkIgKkDkN4DoLkDgKwCgZoGgKwCoG8B8G8BwWgFgK8B8GoBoLwCoGoBgFoBoV4DoL8BwHoBwHoBkwBsJsO4D0PsE0PgF0P0F4IwCwgBsJkI8BsJ8B8QkD0ZoG4NkD8L4DwlB4NsYwHkIkDge4I0Z8GsiBsJoLwC8GoB8GUsJAsOA0yBjD4wBvCssB3DwMnB4mB_EkXjDgKTgKnBwHTgFTwMnB8VvC0jBrEwlB3DkNnBoGT8GTkSvC4InB0KvCkI7BgF7BsTzFsJvCsOrE4N3DoG7BgKjDgUnGgPrEwR_EwRrE0UrE0FnB0FAgKT0KTkIUkI8BoGwCokB0UwM8G4DwC0FwCwH8BoGA0FT8G7B8GrEgFrE4D3DgFnG4DjIwC_JvC3SnB3IT_EnBjN7B3NnB3IvC3SvCnLvCvHjDnG3DzFrE_EnGzF_EjDjInBnGTjIoBzF8B3DwCjDwCzFoG7B4D7BsETsEA8G8BwH4DoG8GwHwHsEoLgFgKkDgjBoGoGoB46B0KgPwCsOwCs2B4I8L8BkIoBgKAg3BU0PoBsOwC0PkDkNkDsd4I0Z8GoL8BsOwCoLoBkXwCofwCsOUsOA4IA8GoBkI8B4IkDgKkDgP4DoL8BgKUw0BwH0esEoQkDgZwCgZwC4IU0KoBwRAoawCsdwC4ckDoBAoiCoL8sC0UgjB4I4mB0FgewH0yBwMokBsJwCUk9C8V8uBoL4NwCkcoG8Q8B8GAoGAoVoBoLT4N7BoBAgP7B8LvCwRrE0P_EoVvH8VrJ0PnGgjBrO41BzZ0tBnV8BTge3NoQ7G8GnBwHnBgFA8GAwRAkNA8pBnBkhBAwMoB4I8BgKoBsJ4D4IgFkIgFkN4I0FkD8pBkckS8L8GgF8QsJgFsJ0FsJwC0F4DgFsEsE0FsEwb4SkN4IkDwC0FwC0F8B4I8BgF8B4D8B8QgK0K8GsEoBsEAwMwHsO8GgyB8Q4DoB4NkD4IwC0Z0F8LoB4N8BwMoB0KoB4XsEo0DoVw_D4XsOkDgmCwMwM8B4hB0F4N8BsOwCsOwCoQUwHAkS7Bg3BrJ0P7B4NToLUkNoBoLwCoGwC4DoB0PkIwlBgUsd4NkXwHsYsE4hB8GsgCoL8pBoGgoBoG4X4DoGoB4DU0FoBwHoBsEUkI8BoGUgFoB8L8B0ZgFwMwCk1B8G8LoBgU8BkNoBk6B4DgPoBoGUouBkD4IUoL8Bof4D03BkIsdoGgPkDofwHoL4DwM4D8Q0F4_BgZwboL0U4Io9Gg1C0tBwR0jB4N4NgFwCTsEAwRgF8uBoLsJwCoQ4Dw0B8L8G8BsJwC4DUkSwC8V4Ds2B0KsOkDw0BgK0U4DkwBkIsJoB0mCkNgmC0PoVsEgFoBsd8GgFoBsJ8BgKwC4IwCgFoBkDUsTgFsEUsnB8G0Z0F0e8GgU4D8VsE8pBwHkI8BgP8BgKoB0UwCkDAsJA8fUwRUgFA4IUsOUsJAsJAsiBUkXT4NnB8arEwR3DoQrEkS7GoV3IgK_EgK_E8QrJ8L7GsJnG4SvMwWvRwb_TkI_EgKzFgFvCgKzFsY3NwRjIgKzF4D7BsEvC0KzFwH3DwMnG8arOwHvCkN3DwR7BgFTsJTgjB7BsOnBwRnBgPTgUnBkhBnBg6C4D4rB0F4SkDkIoBwM8BgFU8L8B8GoB8GoB8GAgKTgP7BsJnBkInBsT3D4NjDoBA4hB7GoGnB0KvC4I7B8LvC8GnB4I7BoQ3Doa3IsiB_JwMjD0KnB8QnBgFTkNA8LA8LU0Z8Bkc8B8LU0U8BkNoB8LU8QUsJUgPUwgB8B0KUwHUoaU8GAsOUoGU0UA8LAkDAkDA4IAsEAsOAwMA4IAoGA8LUoVUoQU4NUoL8B0FU4I8BkwBkIoGoBkNwCk1BkN8LoBkSoBoGT8GAoG8B8GkDwMwH8GsE0KkI4XsToQoL8pBkc0oBwW4NwHsTkI0KgFwHwCkNwC8GUkIAkI7B0F7BkI3DoL7GkI7G0KvHwRnLsEjD4IrEwMnGwRvH8QzF8GvCgPrE8GnBwHTgKnBoLTsOAgFA8LAwMAwHA0eAkNA0jBA0UUkDAsT4DsOkD8G8BgP0FgKgF8G4D4IsE0K8GkI4IsEsEkIoGkDwCgKkIgK0K4I0KkDkDgK8L0PsT8LwMsT0U4IsJsE4DoG4DoG4DkI4DwHkDkI8B8LoB4cgFoLwC4IUsJUoLoBsJoBkc4D8GUsEU0FUsEU8fUsOTgKU8QTkITgZnB4NA0U7BwHA0PTwgB8BkNoBoQkD8GwCwWoB0FAoGnBoG7BoGvC8G3D0FjD0evRsEvCsJ_E4I_EwqBzZ8VjNgFjDgKnG4I_E4I3DsJ3D0enLkIjDgUvH0K3D8LrE4DnBoGvCwHjDsE7B8GjDsOnGsE7BwCnB8VzKsJrEsJ_E8Q3I4IrEsY7L8GjDsOnGsiBvRwH_EwH_EoGrEsEvC8Q7GsJ3DsEnB0FTsJwCsEUgFAkI7B4DTsETgFUsEoBs1E8zB4rBkSgzDgtB8QoGkDoBkNvC0F7BgFjDgKjIwMriBkNrsBgtB_mEkSjwB8G7VgF_O8G_T0FrTwWr7B0FrOoG4DkD8BgFkD0FoBsEoBsEAkNwC0FAkDT4NjDsd3IgoBjNwRzFwHvC4I7B4c7Gk6BnLwvBnLoa7GwH7B8anGsd7GoLvC89BrJ8arE4uC3NwWjD0F_YwH7f4DzP8GrnBwHrnB0K36B4DvWoBzFwCAgFA0FU0jBAkNA8B3DwCvCsEvCgFTsEU4D8BkDkD0F8B8GoBwHoBoVAwvBAk_BUoQAonCUgyBUoLA4D7GwC_EniCzmC7BU7BTnBnBT7BU7BoBnB8BT8BU" 8 | 9 | val depok = GeoPoint(-6.3970066, 106.8224316) 10 | val jakarta = GeoPoint(-6.1907982, 106.8315909) 11 | val tangerang = GeoPoint(-6.2686957,106.7084831) 12 | val bekasi = GeoPoint(-6.3074877,106.9746681) 13 | 14 | val depokJakartaRoutes: List 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 | *
  1. Reducing the decimal digits of each value. 15 | *
  2. Encoding only the offset from the previous point. 16 | *
  3. Using variable length for each coordinate delta. 17 | *
  4. Using 64 URL-safe characters to display the result. 18 | *

19 | * 20 | * The advantage of this encoding are the following: 21 | *

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 | ![](https://img.shields.io/maven-central/v/tech.utsmankece/osm-android-compose?style=for-the-badge) 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 | ![](/images/marker-default.png){ 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 | ![](/images/marker-custom.png){ 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 | ![](/images/info-window.png){ 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 | ![](/images/polyline.png){ 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 | ![](/images/polyline-custom-1.png){ 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 | ![](/images/polygon.png){ 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 | ![](/images/simple-maps.png){ 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 | --------------------------------------------------------------------------------