Skip to content

Commit 8a88f5e

Browse files
committed
Implement circles
1 parent 9a618f3 commit 8a88f5e

12 files changed

Lines changed: 767 additions & 5 deletions

File tree

.github/workflows/_build-examples.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
- name: Build Example07DraggableMarkers
5656
run: ./gradlew :examples:Example07DraggableMarkers:assembleDebug
5757

58+
- name: Build Example08Circles
59+
run: ./gradlew :examples:Example08Circles:assembleDebug
60+
5861
- name: Upload example APKs
5962
uses: actions/upload-artifact@v4
6063
with:

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Add to your `build.gradle.kts`:
1414

1515
```kotlin
1616
dependencies {
17-
implementation("de.afarber:openmapview:0.3.0")
17+
implementation("de.afarber:openmapview:0.5.0")
1818
}
1919
```
2020

@@ -46,6 +46,7 @@ Explore the example applications to see OpenMapView in action:
4646
- [Example05Camera](examples/Example05Camera) - Camera animations with callbacks
4747
- [Example06Clicks](examples/Example06Clicks) - Map click and long-click listeners
4848
- [Example07DraggableMarkers](examples/Example07DraggableMarkers) - Draggable markers with drag event listeners
49+
- [Example08Circles](examples/Example08Circles) - Circles with various radii, styling, and z-index ordering
4950

5051
![Example05Camera Demo](examples/Example05Camera/screenshot.gif)
5152

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
plugins {
2+
id("com.android.application")
3+
id("org.jetbrains.kotlin.android")
4+
id("org.jetbrains.kotlin.plugin.compose")
5+
id("com.diffplug.spotless")
6+
}
7+
8+
android {
9+
namespace = "de.afarber.openmapview.example08circles"
10+
compileSdk = 35
11+
12+
defaultConfig {
13+
applicationId = "de.afarber.openmapview.example08circles"
14+
minSdk = 23
15+
targetSdk = 35
16+
versionCode = 1
17+
versionName = "1.0"
18+
}
19+
20+
buildTypes {
21+
release {
22+
isMinifyEnabled = false
23+
}
24+
}
25+
26+
compileOptions {
27+
sourceCompatibility = JavaVersion.VERSION_17
28+
targetCompatibility = JavaVersion.VERSION_17
29+
}
30+
31+
kotlinOptions {
32+
jvmTarget = "17"
33+
}
34+
35+
buildFeatures {
36+
compose = true
37+
}
38+
39+
composeOptions {
40+
kotlinCompilerExtensionVersion = "1.5.3"
41+
}
42+
43+
sourceSets {
44+
getByName("main").java.srcDirs("src/main/kotlin")
45+
}
46+
}
47+
48+
dependencies {
49+
implementation(project(":openmapview"))
50+
implementation("androidx.core:core-ktx:1.15.0")
51+
implementation("androidx.activity:activity-compose:1.9.3")
52+
implementation("androidx.compose.ui:ui:1.7.5")
53+
implementation("androidx.compose.material3:material3:1.3.1")
54+
implementation("androidx.compose.ui:ui-viewbinding:1.7.5")
55+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
56+
}
57+
58+
spotless {
59+
kotlin {
60+
target("src/**/*.kt")
61+
ktlint("1.3.1").editorConfigOverride(
62+
mapOf(
63+
"ktlint_function_naming_ignore_when_annotated_with" to "Composable",
64+
),
65+
)
66+
trimTrailingWhitespace()
67+
endWithNewline()
68+
indentWithSpaces(4)
69+
licenseHeaderFile(rootProject.file("spotless.license.kt"), "(package|import)")
70+
}
71+
kotlinGradle {
72+
target("*.kts")
73+
ktlint("1.3.1")
74+
}
75+
}
10.3 MB
Loading
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<uses-permission android:name="android.permission.INTERNET" />
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@android:drawable/ic_dialog_map"
8+
android:label="Example08Circles"
9+
android:supportsRtl="true"
10+
android:theme="@android:style/Theme.Material.Light.NoActionBar">
11+
<activity
12+
android:name=".MainActivity"
13+
android:exported="true">
14+
<intent-filter>
15+
<action android:name="android.intent.action.MAIN" />
16+
<category android:name="android.intent.category.LAUNCHER" />
17+
</intent-filter>
18+
</activity>
19+
</application>
20+
</manifest>
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview.example08circles
9+
10+
import android.content.Intent
11+
import android.graphics.Color
12+
import android.net.Uri
13+
import android.os.Bundle
14+
import android.widget.Toast
15+
import androidx.activity.ComponentActivity
16+
import androidx.activity.compose.setContent
17+
import androidx.compose.foundation.layout.Box
18+
import androidx.compose.foundation.layout.Column
19+
import androidx.compose.foundation.layout.fillMaxSize
20+
import androidx.compose.foundation.layout.padding
21+
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.filled.Add
23+
import androidx.compose.material.icons.filled.Clear
24+
import androidx.compose.material3.FloatingActionButton
25+
import androidx.compose.material3.Icon
26+
import androidx.compose.material3.MaterialTheme
27+
import androidx.compose.material3.Surface
28+
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.LaunchedEffect
30+
import androidx.compose.runtime.getValue
31+
import androidx.compose.runtime.mutableStateOf
32+
import androidx.compose.runtime.remember
33+
import androidx.compose.runtime.setValue
34+
import androidx.compose.ui.Alignment
35+
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.platform.LocalContext
37+
import androidx.compose.ui.unit.dp
38+
import androidx.compose.ui.viewinterop.AndroidView
39+
import de.afarber.openmapview.CircleOptions
40+
import de.afarber.openmapview.LatLng
41+
import de.afarber.openmapview.OpenMapView
42+
import kotlin.random.Random
43+
44+
class MainActivity : ComponentActivity() {
45+
override fun onCreate(savedInstanceState: Bundle?) {
46+
super.onCreate(savedInstanceState)
47+
setContent {
48+
MaterialTheme {
49+
Surface(
50+
modifier = Modifier.fillMaxSize(),
51+
color = MaterialTheme.colorScheme.background,
52+
) {
53+
MapViewScreen()
54+
}
55+
}
56+
}
57+
}
58+
}
59+
60+
@Composable
61+
fun MapViewScreen() {
62+
val context = LocalContext.current
63+
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
64+
var mapView by remember { mutableStateOf<OpenMapView?>(null) }
65+
var circleCount by remember { mutableStateOf(0) }
66+
67+
val bochumCenter = LatLng(51.4661, 7.2491)
68+
69+
// Show initial instruction toast
70+
LaunchedEffect(Unit) {
71+
Toast.makeText(
72+
context,
73+
"Click circles to see their properties.\nUse FABs to add random circles or clear all",
74+
Toast.LENGTH_LONG,
75+
).show()
76+
}
77+
78+
fun addDemoCircles() {
79+
mapView?.let { map ->
80+
// Circle 1: Small red circle with high z-index
81+
map.addCircle(
82+
CircleOptions()
83+
.center(bochumCenter)
84+
.radius(500f)
85+
.strokeColor(Color.RED)
86+
.strokeWidth(5f)
87+
.fillColor(Color.argb(64, 255, 0, 0))
88+
.clickable(true)
89+
.zIndex(2f)
90+
.tag("Small Red Circle - 500m"),
91+
)
92+
93+
// Circle 2: Medium blue circle with mid z-index
94+
val offset1 = LatLng(bochumCenter.latitude + 0.01, bochumCenter.longitude + 0.01)
95+
map.addCircle(
96+
CircleOptions()
97+
.center(offset1)
98+
.radius(1000f)
99+
.strokeColor(Color.BLUE)
100+
.strokeWidth(8f)
101+
.fillColor(Color.argb(64, 0, 0, 255))
102+
.clickable(true)
103+
.zIndex(1f)
104+
.tag("Medium Blue Circle - 1000m"),
105+
)
106+
107+
// Circle 3: Large green circle with low z-index
108+
val offset2 = LatLng(bochumCenter.latitude - 0.01, bochumCenter.longitude - 0.01)
109+
map.addCircle(
110+
CircleOptions()
111+
.center(offset2)
112+
.radius(1500f)
113+
.strokeColor(Color.GREEN)
114+
.strokeWidth(10f)
115+
.fillColor(Color.argb(64, 0, 255, 0))
116+
.clickable(true)
117+
.zIndex(0f)
118+
.tag("Large Green Circle - 1500m"),
119+
)
120+
121+
circleCount += 3
122+
Toast.makeText(context, "Added 3 demonstration circles", Toast.LENGTH_SHORT).show()
123+
}
124+
}
125+
126+
fun addRandomCircle() {
127+
mapView?.let { map ->
128+
val randomLat = bochumCenter.latitude + (Random.nextDouble() - 0.5) * 0.03
129+
val randomLng = bochumCenter.longitude + (Random.nextDouble() - 0.5) * 0.06
130+
val randomRadius = Random.nextInt(300, 1500).toFloat()
131+
val randomColor = Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
132+
133+
map.addCircle(
134+
CircleOptions()
135+
.center(LatLng(randomLat, randomLng))
136+
.radius(randomRadius)
137+
.strokeColor(randomColor)
138+
.strokeWidth(Random.nextInt(3, 12).toFloat())
139+
.fillColor(Color.argb(64, Color.red(randomColor), Color.green(randomColor), Color.blue(randomColor)))
140+
.clickable(true)
141+
.zIndex(Random.nextFloat() * 5)
142+
.tag("Random Circle ${++circleCount} - ${randomRadius.toInt()}m"),
143+
)
144+
145+
Toast.makeText(context, "Added random circle", Toast.LENGTH_SHORT).show()
146+
}
147+
}
148+
149+
Box(modifier = Modifier.fillMaxSize()) {
150+
AndroidView(
151+
factory = { ctx ->
152+
OpenMapView(ctx).apply {
153+
mapView = this
154+
lifecycleOwner.lifecycle.addObserver(this)
155+
156+
setCenter(bochumCenter)
157+
setZoom(12.0)
158+
159+
// Add initial demonstration circles
160+
addDemoCircles()
161+
162+
// Set circle click listener
163+
setOnCircleClickListener { circle ->
164+
val tagStr = circle.tag?.toString() ?: "Unknown Circle"
165+
val coordStr = "%.4f, %.4f".format(circle.center.latitude, circle.center.longitude)
166+
Toast.makeText(
167+
context,
168+
"$tagStr\nCenter: $coordStr\nZ-Index: ${circle.zIndex}",
169+
Toast.LENGTH_SHORT,
170+
).show()
171+
}
172+
173+
// Set attribution click listener
174+
setOnAttributionClickListener {
175+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.openstreetmap.org/copyright"))
176+
context.startActivity(intent)
177+
}
178+
}
179+
},
180+
modifier = Modifier.fillMaxSize(),
181+
)
182+
183+
// Floating Action Buttons
184+
Column(
185+
modifier =
186+
Modifier
187+
.align(Alignment.BottomEnd)
188+
.padding(16.dp),
189+
horizontalAlignment = Alignment.End,
190+
) {
191+
// Add random circle FAB
192+
FloatingActionButton(
193+
onClick = { addRandomCircle() },
194+
modifier = Modifier.padding(bottom = 16.dp),
195+
) {
196+
Icon(
197+
imageVector = Icons.Default.Add,
198+
contentDescription = "Add random circle",
199+
)
200+
}
201+
202+
// Clear all circles FAB
203+
FloatingActionButton(
204+
onClick = {
205+
mapView?.clear()
206+
circleCount = 0
207+
Toast.makeText(
208+
context,
209+
"All circles cleared",
210+
Toast.LENGTH_SHORT,
211+
).show()
212+
},
213+
) {
214+
Icon(
215+
imageVector = Icons.Default.Clear,
216+
contentDescription = "Clear all circles",
217+
)
218+
}
219+
}
220+
}
221+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
import android.graphics.Color
11+
12+
/**
13+
* Represents a circle on the map with a center point and radius in meters.
14+
*
15+
* @property center Geographic coordinate of the circle center
16+
* @property radius Radius of the circle in meters
17+
* @property strokeColor Color of the circle outline (default: black)
18+
* @property strokeWidth Width of the outline in pixels (default: 10f)
19+
* @property fillColor Fill color for the circle interior (default: semi-transparent gray)
20+
* @property visible Whether the circle is visible. Default is true
21+
* @property clickable Whether the circle is clickable. Default is false
22+
* @property zIndex Draw order. Circles with higher zIndex are drawn on top. Default is 0.0
23+
* @property tag Optional user data associated with the circle
24+
*/
25+
data class Circle(
26+
val center: LatLng,
27+
val radius: Float,
28+
val strokeColor: Int = Color.BLACK,
29+
val strokeWidth: Float = 10f,
30+
val fillColor: Int = Color.argb(128, 128, 128, 128),
31+
val visible: Boolean = true,
32+
val clickable: Boolean = false,
33+
val zIndex: Float = 0f,
34+
val tag: Any? = null,
35+
) {
36+
/**
37+
* Unique identifier for this circle instance.
38+
* Used internally for management and callbacks.
39+
*/
40+
internal val id: String = "circle_${System.nanoTime()}_${System.identityHashCode(this)}"
41+
42+
init {
43+
require(radius > 0) { "Circle radius must be greater than 0" }
44+
}
45+
46+
override fun equals(other: Any?): Boolean {
47+
if (this === other) return true
48+
if (other !is Circle) return false
49+
return id == other.id
50+
}
51+
52+
override fun hashCode(): Int = id.hashCode()
53+
}

0 commit comments

Comments
 (0)