Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"Bash(xcodebuild -workspace MapConductorSampleApp.xcworkspace -scheme MapConductorSampleApp -destination 'generic/platform=iOS Simulator' build)",
"Bash(git -C /Users/masashi/android-sdk/android-geojson-layer log --oneline bfa194c07a..e75c70bae)",
"Bash(git -C /Users/masashi/android-sdk/android-geojson-layer log --oneline e75c70bae..bfa194c07a)",
"Bash(git *)"
"Bash(git *)",
"WebFetch(domain:repo1.maven.org)",
"WebFetch(domain:central.sonatype.com)"
],
"deny": [],
"ask": []
Expand Down
2 changes: 2 additions & 0 deletions README.es-419.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ Este ejemplo usa MapLibre Maps, pero los objetos del mapa están escritos usando

## Cambiar de proveedor de mapas

![](docs/src/assets/top-page/unified-map-view.png)

Una de las ideas principales detrás de MapConductor es que tus superposiciones de mapa no deberían tener que reescribirse cuando cambias de proveedor de mapas.

Por ejemplo:
Expand Down
2 changes: 2 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ fun SimpleMapScreen(modifier: Modifier) {

## マッププロバイダーの切り替え

![](docs/src/assets/top-page/unified-map-view.png)

MapConductorの主な考え方の1つは、マッププロバイダーを変更してもマップオーバーレイを書き直す必要がないということです。

例:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ This example uses MapLibre Maps, but the map objects are written using MapConduc

## Switching Map Providers

![](docs/src/assets/top-page/unified-map-view.png)

One of the main ideas behind MapConductor is that your map overlays should not have to be rewritten when you change map providers.

For example:
Expand Down
2 changes: 1 addition & 1 deletion android-for-googlemaps
2 changes: 1 addition & 1 deletion android-geojson-layer
2 changes: 1 addition & 1 deletion android-heatmap
2 changes: 1 addition & 1 deletion android-icons
Binary file added docs/src/assets/top-page/unified-map-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/src/components/index/Features.astro
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const {
<CardGrid>
<Card title={titleForUnifiedAPI}>
{descriptionForUnifiedAPI}
<Image src={MapviewPreview} alt="Marker" width="200" class="preview" />
<Image src={MapviewPreview} alt="Mapview" width="200" class="preview" />
</Card>
<Card title={titleForMarker}>
{descriptionForMarker}
Expand Down
Binary file added example-app/src/main/assets/cluster_red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
package com.mapconductor.example.pages.circle

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.mapconductor.core.circle.Circle
import com.mapconductor.core.circle.CircleState
import com.mapconductor.core.map.MapViewStateInterface
import com.mapconductor.core.map.OnCameraMoveHandler
import com.mapconductor.core.marker.Marker
import com.mapconductor.core.marker.MarkerState
import com.mapconductor.core.polyline.Polyline
import com.mapconductor.core.polyline.PolylineState
import com.mapconductor.example.MapViewContainer

@Composable
Expand All @@ -15,21 +33,67 @@ fun CircleMapComponent(
circleState: CircleState,
centerMarker: MarkerState,
edgeMarker: MarkerState,
labelPosition: IntOffset?,
modifier: Modifier = Modifier,
onMapCameraMove: OnCameraMoveHandler = { },
) {
mapViewState?.let { it ->
var labelSize by remember { mutableStateOf(IntSize.Zero) }

mapViewState?.let { state ->
MapViewContainer(
modifier = modifier,
state = it,
state = state,
onCameraMove = onMapCameraMove,
) {
// Circle
Circle(circleState)

// Radius line polygon from center (C) to edge (E)
// Stable id prevents multiple polygons accumulating during rapid drag
Polyline(
PolylineState(
points = listOf(centerMarker.position, edgeMarker.position),
id = "circle-radius-line",
strokeColor = Color.White,
strokeWidth = 3.dp,
),
)

// Center marker (not draggable)
Marker(centerMarker)

// Edge marker (draggable)
Marker(edgeMarker)

Box(modifier = Modifier.fillMaxSize()) {
labelPosition?.let { pos ->
Box(
modifier = Modifier
.onGloballyPositioned { labelSize = it.size }
.offset {
IntOffset(
pos.x - labelSize.width / 2,
pos.y - labelSize.height / 2
)
},
) {
val labelText = "${circleState.radiusMeters.toInt()} m"
// White outline
BasicText(
text = labelText,
style = TextStyle(
color = Color.White,
drawStyle = Stroke(width = 6f),
),
)
// Black fill
BasicText(
text = labelText,
style = TextStyle(color = Color.Red),
)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ fun CircleMapPage(onToggleSidebar: () -> Unit = {}) {
onMapViewStateChanged = viewModel::onMapViewChanged,
) { paddingValues ->
val mapViewState = viewModel.mapViewState.collectAsState()
val labelPosition = viewModel.labelPosition.collectAsState()

CircleMapComponent(
mapViewState = mapViewState.value,
circleState = viewModel.circleState,
centerMarker = viewModel.centerMarker,
edgeMarker = viewModel.edgeMarker,
labelPosition = labelPosition.value,
onMapCameraMove = viewModel::onMapCameraMove,
)

MessageCard(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,39 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.mapping.view.Camera
import com.arcgismaps.mapping.view.Offset
import com.mapconductor.core.circle.CircleEvent
import com.mapconductor.core.circle.CircleState
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.map.MapCameraPosition
import com.mapconductor.core.map.MapViewStateInterface
import com.mapconductor.core.marker.DefaultMarkerIcon
import com.mapconductor.core.marker.MarkerState
import com.mapconductor.core.spherical.Spherical
import com.mapconductor.core.spherical.WGS84Geodesic.computeDistanceBetween
import com.mapconductor.core.spherical.calculatePositionAtDistance
import com.mapconductor.example.toast.ToastMessage
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

interface CirclePageViewModelInterface {
val initCameraPosition: MapCameraPosition
val mapViewState: StateFlow<MapViewStateInterface<*>?>
val messages: StateFlow<List<ToastMessage>>

val labelPosition: StateFlow<IntOffset?>

val circleCenter: GeoPoint
val radiusMeters: Double
val centerMarker: MarkerState
Expand All @@ -37,9 +49,7 @@ interface CirclePageViewModelInterface {

fun onMapViewChanged(state: MapViewStateInterface<*>)

fun onMarkerClick(clicked: MarkerState)

fun onMapClick(clicked: GeoPoint)
fun onMapCameraMove(cameraPosition: MapCameraPosition)

fun onCircleClick(event: CircleEvent)

Expand All @@ -50,11 +60,17 @@ interface CirclePageViewModelInterface {
fun removeToast(toastMessage: ToastMessage)
}

class CirclePageViewModel :
class CirclePageViewModel(
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
) :
ViewModel(),
CirclePageViewModelInterface {
private val _messages: MutableStateFlow<List<ToastMessage>> = MutableStateFlow(emptyList())
override val messages: StateFlow<List<ToastMessage>> = _messages.asStateFlow()

private val _labelPosition: MutableStateFlow<IntOffset?> = MutableStateFlow(null)
override val labelPosition: StateFlow<IntOffset?> = _labelPosition.asStateFlow()

private val colors: List<Color> =
listOf(
Color.Blue,
Expand Down Expand Up @@ -96,33 +112,46 @@ class CirclePageViewModel :
draggable = false,
)

private val _edgeMarker: MutableState<MarkerState> =
mutableStateOf(
MarkerState(
id = "edge_marker",
position =
calculatePositionAtDistance(
center = circleCenter,
distanceMeters = 1000.0,
bearingDegrees = 90.0, // East
),
icon =
DefaultMarkerIcon(
fillColor = Color.Green,
strokeColor = Color.White,
label = "E",
),
draggable = true,
onDragStart = this::onMarkerMove,
onDrag = this::onMarkerMove,
onDragEnd = this::onMarkerMove,
override val edgeMarker: MarkerState = MarkerState(
id = "edge_marker",
position =
calculatePositionAtDistance(
center = circleCenter,
distanceMeters = 1000.0,
bearingDegrees = 90.0, // East
),
)
override val edgeMarker: MarkerState
get() = _edgeMarker.value
icon =
DefaultMarkerIcon(
fillColor = Color.Green,
strokeColor = Color.White,
label = "E",
),
draggable = true,
onDragStart = this::onMarkerMove,
onDrag = this::onMarkerMove,
onDragEnd = this::onMarkerMove,
)

override val radiusMeters by derivedStateOf {
computeDistanceBetween(circleCenter, _edgeMarker.value.position)
computeDistanceBetween(circleCenter, edgeMarker.position)
}

private fun calculateLabelPosition() {
mainScope.launch {
mapViewState.value?.getMapViewHolder()?.let { holder ->
val midPoint = Spherical.linearInterpolate(
from = centerMarker.position,
to = edgeMarker.position,
fraction = 0.5,
)
holder.toScreenOffset(midPoint)?.let {
_labelPosition.value = IntOffset(
x = it.x.roundToInt(),
y = it.y.roundToInt(),
)
}
}
}
}

override val circleState: CircleState
Expand All @@ -147,12 +176,8 @@ class CirclePageViewModel :
this._mapViewState.value = state
}

override fun onMarkerClick(clicked: MarkerState) {
showToast("${clicked.icon?.let { (it as? DefaultMarkerIcon)?.label } ?: "Marker"} clicked")
}

override fun onMapClick(clicked: GeoPoint) {
showToast("Map clicked at: ${clicked.toUrlValue()}")
override fun onMapCameraMove(cameraPosition: MapCameraPosition) {
this.calculateLabelPosition()
}

override fun onCircleClick(event: CircleEvent) {
Expand All @@ -161,7 +186,8 @@ class CirclePageViewModel :
}

override fun onMarkerMove(dragged: MarkerState) {
_edgeMarker.value.position = dragged.position
edgeMarker.position = dragged.position
this.calculateLabelPosition()
}

override fun showToast(text: String) {
Expand Down
Loading
Loading