diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index bf7d3965..24221107 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -13,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage +import com.mapconductor.example.pages.polyline.PolylineMapPage import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar import com.mapconductor.example.ui.sidebar.SidebarItem @@ -38,6 +40,12 @@ fun DemoAppScreen() { icon = Icons.Default.CheckCircle, route = "circle", ), + SidebarItem( + id = "polyline", + title = "Polyline Demo", + icon = Icons.Default.PlayArrow, + route = "polyline", + ), // SidebarItem( // id = "examples", // title = "Map Examples", @@ -67,6 +75,11 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "polyline" -> { + PolylineMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "settings" -> { // Placeholder for settings page Box( diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 652617f1..a1cafa27 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -9,6 +9,7 @@ import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.googlemaps.GoogleMapViewState import com.mapconductor.googlemaps.GoogleMapsView import com.mapconductor.here.HereMapView @@ -28,6 +29,7 @@ fun MapViewContainer( onMarkerAnimateStart: OnMarkerEventHandler? = null, onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { when (state) { @@ -43,6 +45,7 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onPolylineClick = onPolylineClick, content = content, ) @@ -57,6 +60,8 @@ fun MapViewContainer( onMarkerDragEnd = onMarkerDragEnd, onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, + onCircleClick = onCircleClick, + onPolylineClick = onPolylineClick, content = content, ) @@ -72,6 +77,7 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onPolylineClick = onPolylineClick, content = content, ) @@ -87,6 +93,7 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onPolylineClick = onPolylineClick, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt index 3026e00b..17512761 100644 --- a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel class NavigationViewModel : ViewModel() { - private val _currentPage = mutableStateOf("map") + private val _currentPage = mutableStateOf("polyline") val currentPage: State = _currentPage private val _isSidebarExpanded = mutableStateOf(false) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index a27e705b..56cb301a 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt @@ -16,18 +16,18 @@ fun CircleMapComponent( mapViewState: MapViewState<*>?, viewModel: CirclePageViewModel, modifier: Modifier = Modifier, - onMapClickHandler: OnMapEventHandler = {}, - onMarkerClickHandler: OnMarkerEventHandler = {}, - onCircleClickHandler: OnCircleEventHandler = {}, + onMapClick: OnMapEventHandler = {}, + onMarkerClick: OnMarkerEventHandler = {}, + onCircleClick: OnCircleEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, ) { - mapViewState?.let { mapViewState -> + mapViewState?.let { it -> MapViewContainer( modifier = modifier, - state = mapViewState, - onMapClick = onMapClickHandler, - onMarkerClick = onMarkerClickHandler, - onCircleClick = onCircleClickHandler, + state = it, + onMapClick = onMapClick, + onMarkerClick = onMarkerClick, + onCircleClick = onCircleClick, onMarkerDrag = onMarkerDrag, ) { // Circle diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt index 4646f143..f541b893 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt @@ -2,16 +2,19 @@ package com.mapconductor.example.pages.circle import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.mapconductor.example.R import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold -import com.mapconductor.example.ui.MapViewStatePanel import com.mapconductor.example.ui.MessageCard @Composable @@ -29,9 +32,9 @@ fun CircleMapPage( CircleMapComponent( mapViewState = mapViewState.value, viewModel = viewModel, - onMapClickHandler = viewModel::onMapClick, - onMarkerClickHandler = viewModel::onMarkerClick, - onCircleClickHandler = viewModel::onCircleClick, + onMapClick = viewModel::onMapClick, + onMarkerClick = viewModel::onMarkerClick, + onCircleClick = viewModel::onCircleClick, onMarkerDrag = viewModel::onMarkerDrag, ) @@ -47,11 +50,9 @@ fun CircleMapPage( ), title = "Messages", ) { - MapViewStatePanel( - mapViewState.value - ?.mapCameraPosition - ?.collectAsState() - ?.value, + Text( + text = stringResource(R.string.circle_example_description), + modifier = Modifier.fillMaxSize(), ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index 215de86f..8ad89b81 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -1,174 +1,175 @@ -package com.mapconductor.example.pages.circle - -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.DefaultIcon -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.spherical.calculatePositionAtDistance -import com.mapconductor.core.spherical.haversineDistance -import com.mapconductor.example.toast.ToastMessage -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -interface CirclePageViewModel { - val initCameraPosition: MapCameraPosition - val mapViewState: StateFlow?> - val messages: StateFlow> - - val circleCenter: GeoPoint - val radiusMeters: Double - val centerMarker: MarkerState - val edgeMarker: MarkerState - val circleState: CircleState - - fun onMapViewChanged(state: MapViewState<*>) - - fun cameraReset(listener: MapViewState.MoveCameraCallback? = null) - - fun onMarkerClick(clicked: MarkerState) - - fun onMapClick(clicked: GeoPoint) - - fun onCircleClick(event: CircleClickEvent) - - fun onMarkerDrag(dragged: MarkerState) - - fun showToast(text: String) - - fun removeToast(toastMessage: ToastMessage) -} - -class CirclePageViewModelImpl : - ViewModel(), - CirclePageViewModel { - private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) - override val messages: StateFlow> = _messages.asStateFlow() - - override val initCameraPosition = - MapCameraPosition( - position = - GeoPoint.fromLatLong( - latitude = 21.382314, - longitude = -157.933097, - ), - zoom = 12.0, - bearing = 0.0, - tilt = 0.0, - paddings = null, - ) - - override val circleCenter = GeoPoint.fromLatLong(21.382314, -157.933097) - - override val centerMarker = - MarkerState( - id = "center_marker", - position = circleCenter, - icon = - DefaultIcon( - fillColor = Color.Red, - strokeColor = Color.White, - label = "C", - ), - draggable = false, - ) - - private val _edgeMarker: MutableState = - mutableStateOf( - MarkerState( - id = "edge_marker", - position = - calculatePositionAtDistance( - center = circleCenter, - distanceMeters = 1000.0, - bearingDegrees = 90.0, // East - ), - icon = - DefaultIcon( - fillColor = Color.Green, - strokeColor = Color.White, - label = "E", - ), - draggable = true, - ), - ) - override val edgeMarker: MarkerState - get() = _edgeMarker.value - - override val radiusMeters by derivedStateOf { - haversineDistance(circleCenter, _edgeMarker.value.position) - } - - private val _circleState: MutableState = - mutableStateOf( - CircleState( - id = "circle", - center = circleCenter, - radiusMeters = 1000.0, // Initial radius - strokeColor = Color.Blue, - strokeWidth = 2.dp, - fillColor = Color.Blue.copy(alpha = 0.2f), - ), - ) - override val circleState: CircleState - get() = _circleState.value - - private val _mapViewState = MutableStateFlow?>(null) - override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() - - override fun onMapViewChanged(state: MapViewState<*>) { - this._mapViewState.value = state - } - - override fun cameraReset(listener: MapViewState.MoveCameraCallback?) { - this.mapViewState.value?.moveCameraTo( - cameraPosition = initCameraPosition, - durationMs = 3000, - listener = listener, - ) - } - - override fun onMarkerClick(clicked: MarkerState) { - showToast("${clicked.icon?.let { (it as? DefaultIcon)?.label } ?: "Marker"} clicked") - } - - override fun onMapClick(clicked: GeoPoint) { - showToast("Map clicked at: ${clicked.toUrlValue()}") - } - - override fun onCircleClick(event: CircleClickEvent) { - event.state.fillColor = Color.Blue.copy(alpha = 0.5f) - showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") - } - - override fun onMarkerDrag(dragged: MarkerState) { - _edgeMarker.value.position = dragged.position - - // Update circle radius - _circleState.value.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) - -// showToast("Radius updated: ${radiusMeters.toInt()}m") - } - - override fun showToast(text: String) { - this._messages.value = this._messages.value + ToastMessage(text = text) - } - - override fun removeToast(toastMessage: ToastMessage) { - this._messages.value = this._messages.value.filter { it != toastMessage } - } - - override fun onCleared() { - super.onCleared() - } -} +package com.mapconductor.example.pages.circle + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import com.mapconductor.core.circle.CircleClickEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.spherical.calculatePositionAtDistance +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.example.toast.ToastMessage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface CirclePageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val circleCenter: GeoPoint + val radiusMeters: Double + val centerMarker: MarkerState + val edgeMarker: MarkerState + val circleState: CircleState + + fun onMapViewChanged(state: MapViewState<*>) + + fun onMarkerClick(clicked: MarkerState) + + fun onMapClick(clicked: GeoPoint) + + fun onCircleClick(event: CircleClickEvent) + + fun onMarkerDrag(dragged: MarkerState) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class CirclePageViewModelImpl : + ViewModel(), + CirclePageViewModel { + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + private val colors: List = + listOf( + Color.Blue.copy(0.2f), + Color.Red.copy(alpha = 0.2f), + Color.Green.copy(alpha = 0.2f), + Color.Cyan.copy(alpha = 0.2f), + Color.LightGray.copy(alpha = 0.2f), + Color.Magenta.copy(alpha = 0.2f), + ) + private var tapIdx = 0 + + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 12.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + + override val circleCenter = GeoPoint.fromLatLong(21.382314, -157.933097) + + override val centerMarker = + MarkerState( + id = "center_marker", + position = circleCenter, + icon = + DefaultIcon( + fillColor = Color.Red, + strokeColor = Color.White, + label = "C", + ), + draggable = false, + ) + + private val _edgeMarker: MutableState = + mutableStateOf( + MarkerState( + id = "edge_marker", + position = + calculatePositionAtDistance( + center = circleCenter, + distanceMeters = 1000.0, + bearingDegrees = 90.0, // East + ), + icon = + DefaultIcon( + fillColor = Color.Green, + strokeColor = Color.White, + label = "E", + ), + draggable = true, + ), + ) + override val edgeMarker: MarkerState + get() = _edgeMarker.value + + override val radiusMeters by derivedStateOf { + haversineDistance(circleCenter, _edgeMarker.value.position) + } + + private val _circleState: MutableState = + mutableStateOf( + CircleState( + id = "circle", + center = circleCenter, + radiusMeters = 1000.0, // Initial radius + strokeColor = Color.Blue.copy(alpha = 0.5f), + strokeWidth = 2.dp, + fillColor = this.colors[0], + ), + ) + override val circleState: CircleState + get() = _circleState.value + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + } + + override fun onMarkerClick(clicked: MarkerState) { + showToast("${clicked.icon?.let { (it as? DefaultIcon)?.label } ?: "Marker"} clicked") + } + + override fun onMapClick(clicked: GeoPoint) { + showToast("Map clicked at: ${clicked.toUrlValue()}") + } + + override fun onCircleClick(event: CircleClickEvent) { + this.tapIdx = (this.tapIdx + 1) % this.colors.size + event.state.fillColor = this.colors[this.tapIdx] + showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") + } + + override fun onMarkerDrag(dragged: MarkerState) { + _edgeMarker.value.position = dragged.position + + // Update circle radius + _circleState.value.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) + +// showToast("Radius updated: ${radiusMeters.toInt()}m") + } + + override fun showToast(text: String) { + this._messages.value = this._messages.value + ToastMessage(text = text) + } + + override fun removeToast(toastMessage: ToastMessage) { + this._messages.value = this._messages.value.filter { it != toastMessage } + } + + override fun onCleared() { + super.onCleared() + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt new file mode 100644 index 00000000..8d711c4f --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt @@ -0,0 +1,41 @@ +package com.mapconductor.example.pages.polyline + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.Polyline +import com.mapconductor.example.MapViewContainer + +@Composable +fun PolylineMapComponent( + mapViewState: MapViewState<*>?, + viewModel: PolylinePageViewModel, + modifier: Modifier = Modifier, + onMapClick: OnMapEventHandler = {}, + onMarkerClick: OnMarkerEventHandler = {}, + onPolylineClick: OnPolylineEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler = {}, +) { + mapViewState?.let { it -> + MapViewContainer( + modifier = modifier, + state = it, + onMapClick = onMapClick, + onMarkerClick = onMarkerClick, + onPolylineClick = onPolylineClick, + onMarkerDrag = onMarkerDrag, + ) { + // Polyline + Polyline(viewModel.polylineState) + + // Waypoint markers + viewModel.wayPointMarkers.forEach { marker -> + Marker(marker) + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt new file mode 100644 index 00000000..37a93a9e --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt @@ -0,0 +1,52 @@ +package com.mapconductor.example.pages.polyline + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import com.mapconductor.example.ui.DemoMapPageScaffold + +@Composable +fun PolylineMapPage( + viewModel: PolylinePageViewModel = PolylinePageViewModelImpl(), + onToggleSidebar: () -> Unit = {}, +) { + DemoMapPageScaffold( + initCameraPosition = viewModel.initCameraPosition, + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + PolylineMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + onMapClick = viewModel::onMapClick, + onMarkerClick = viewModel::onMarkerClick, + onPolylineClick = viewModel::onPolylineClick, + onMarkerDrag = viewModel::onMarkerDrag, + ) + +// MessageCard( +// modifier = +// Modifier +// .align(Alignment.BottomStart) +// .padding( +// bottom = paddingValues.calculateBottomPadding() + 16.dp, +// start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, +// end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, +// ), +// title = "Messages", +// ) { +// MapViewStatePanel( +// mapViewState.value +// ?.mapCameraPosition +// ?.collectAsState() +// ?.value, +// ) +// } +// +// ToastHost( +// messages = viewModel.messages.collectAsState().value, +// onDismiss = { viewModel.removeToast(it) }, +// ) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt new file mode 100644 index 00000000..b8278db2 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -0,0 +1,162 @@ +package com.mapconductor.example.pages.polyline + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.example.toast.ToastMessage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface PolylinePageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val wayPointMarkers: List + val polylineState: PolylineState + + fun onMapViewChanged(state: MapViewState<*>) + + fun onMarkerClick(clicked: MarkerState) + + fun onMapClick(clicked: GeoPoint) + + fun onPolylineClick(state: PolylineState) + + fun onMarkerDrag(dragged: MarkerState) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class PolylinePageViewModelImpl : + ViewModel(), + PolylinePageViewModel { + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 15.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + + private val polylinePoints = mutableStateListOf( + GeoPoint.fromLatLong(21.382314, -157.933097), // Honolulu center + GeoPoint.fromLatLong(21.385314, -157.930097), // Northeast + GeoPoint.fromLatLong(21.387314, -157.935097), // Northwest + GeoPoint.fromLatLong(21.380314, -157.937097), // Southwest + GeoPoint.fromLatLong(21.378314, -157.930097), // Southeast + GeoPoint.fromLatLong(21.382314, -157.933097), // Back to center + ) + + private val _wayPointMarkers: MutableState> = + mutableStateOf( + polylinePoints.mapIndexed { index, point -> + MarkerState( + id = "waypoint_$index", + position = point, + icon = + DefaultIcon( + fillColor = + if (index == 0 || + index == polylinePoints.size - 1 + ) { + Color.Green + } else { + Color.Blue + }, + strokeColor = Color.White, + label = + if (index == + 0 + ) { + "S" + } else if (index == polylinePoints.size - 1) { + "E" + } else { + "$index" + }, + ), + draggable = true, + ) + }, + ) + + override val wayPointMarkers: List + get() = _wayPointMarkers.value + + private val _polylineState: MutableState = + mutableStateOf( + PolylineState( + id = "example_polyline", + points = polylinePoints, + strokeColor = Color.Red, + strokeWidth = 4.dp, + geodesic = true, + ), + ) + + override val polylineState: PolylineState + get() = _polylineState.value + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + } + + override fun onMarkerClick(clicked: MarkerState) { + val markerLabel = (clicked.icon as? DefaultIcon)?.label ?: "Marker" + showToast("Waypoint $markerLabel clicked") + } + + override fun onMapClick(clicked: GeoPoint) { + showToast("Map clicked at: ${clicked.toUrlValue()}") + } + + override fun onPolylineClick(state: PolylineState) { + _polylineState.value.strokeColor = Color.Magenta + showToast("Polyline clicked - ${state.points.size} points") + } + + override fun onMarkerDrag(dragged: MarkerState) { + val markerIndex = _wayPointMarkers.value.indexOfFirst { it.id == dragged.id } + if (markerIndex < 0) return + + // 1. pointsを更新 + polylinePoints[markerIndex].latitude = dragged.position.latitude + polylinePoints[markerIndex].longitude = dragged.position.longitude + } + + override fun showToast(text: String) { + this._messages.value = this._messages.value + ToastMessage(text = text) + } + + override fun removeToast(toastMessage: ToastMessage) { + this._messages.value = this._messages.value.filter { it != toastMessage } + } + + override fun onCleared() { + super.onCleared() + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt index 5dd13613..63ea54c5 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt @@ -28,9 +28,9 @@ fun StoreMapComponent( modifier: Modifier = Modifier, markers: List = emptyList(), onDirectionButtonClick: OnMarkerEventHandler = {}, - onMapClickHandler: OnMapEventHandler = {}, - onMarkerClickHandler: OnMarkerEventHandler = {}, - onMarkerDragHandler: OnMarkerEventHandler = {}, + onMapClick: OnMapEventHandler = {}, + onMarkerClick: OnMarkerEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler = {}, ) { val darkTheme: Boolean = isSystemInDarkTheme() val bubbleColor by remember { @@ -74,9 +74,9 @@ fun StoreMapComponent( MapViewContainer( modifier = modifier, state = currentMapViewState, - onMapClick = onMapClickHandler, - onMarkerClick = onMarkerClickHandler, - onMarkerDrag = onMarkerDragHandler, + onMapClick = onMapClick, + onMarkerClick = onMarkerClick, + onMarkerDrag = onMarkerDrag, onMarkerAnimateStart = { isMarkerAnimating = true }, onMarkerAnimateEnd = { isMarkerAnimating = false }, ) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt index 8b29d45d..28c5deaa 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt @@ -28,8 +28,8 @@ fun StoreMapPage( val intent = viewModel.onDirectionButtonClick(state) context.startActivity(intent) }, - onMapClickHandler = viewModel::onMapClick, - onMarkerClickHandler = viewModel::onMarkerClick, + onMapClick = viewModel::onMapClick, + onMarkerClick = viewModel::onMarkerClick, selectedMarker = selectedMarker.value, ) } diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index e9b44ad6..6d98d2f5 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ MapConductor - \ No newline at end of file + Hold the marker labeled \'E\' for one sec, then drag it. The circle will expand and shrink followed by the marker. Also changes the circle color by tap on it. + diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/Utils.kt b/mapconductor-core/src/main/java/com/mapconductor/core/Utils.kt index de927064..4a5196dc 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/Utils.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/Utils.kt @@ -1,23 +1,35 @@ -package com.mapconductor.core - -import com.mapconductor.core.features.IGeoPoint -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.roundToInt - -fun calculateZIndex(geoPointBase: IGeoPoint): Int { - // 南→北で奥行きを出す - // 同じ緯度内では西が上(前)に来る - return (-geoPointBase.latitude * 1_000_000 - geoPointBase.longitude).roundToInt() -} - -fun meterToPixel( - meter: Double, - latitude: Double, - zoom: Double, - tileSize: Double = 256.0, // Google Mapsはデフォルト256pxだが、Mapbox v10+はデフォルト512px -): Double { - val earthCircumference = 2 * Math.PI * 6378137 - val metersPerPixel = cos(Math.toRadians(latitude)) * earthCircumference / (tileSize * 2.0.pow(zoom)) - return meter / metersPerPixel -} +package com.mapconductor.core + +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.roundToInt +import android.util.Log + +fun calculateZIndex(geoPointBase: IGeoPoint): Int { + // 南→北で奥行きを出す + // 同じ緯度内では西が上(前)に来る + return (-geoPointBase.latitude * 1_000_000 - geoPointBase.longitude).roundToInt() +} + +fun meterToPixel( + meter: Double, + latitude: Double, + zoom: Double, + tileSize: Double = 256.0, // Google Mapsはデフォルト256pxだが、Mapbox v10+はデフォルト512px +): Double { + val earthCircumference = 2 * Math.PI * 6378137 + val metersPerPixel = cos(Math.toRadians(latitude)) * earthCircumference / (tileSize * 2.0.pow(zoom)) + return meter / metersPerPixel +} + +fun printPoints( + tag: String, + points: List, +) { + Log.d(tag, "-----------") + points.forEach { point -> + Log.d(tag, GeoPoint.from(point).toUrlValue()) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 55cb16b1..712be76c 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -17,6 +17,7 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer +import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineState @@ -127,6 +128,7 @@ abstract class BaseMapViewController { val polygon: ActualPolygon val state: PolygonState - val stateHashCode: Int + val fingerPrint: PolygonFingerPrint } data class PolygonEntityImpl( override val polygon: ActualPolygon, override val state: PolygonState, ) : PolygonEntity { - override val stateHashCode: Int = state.hashCode() + override val fingerPrint: PolygonFingerPrint = state.fingerPrint() } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt index 14a33c9d..d241594f 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt @@ -1,92 +1,107 @@ -package com.mapconductor.core.polyline - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.mapconductor.core.StateFlowDelegate -import com.mapconductor.core.features.IGeoPoint -import android.os.Parcelable -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged - -class PolylineState( - points: List, - id: String? = null, - strokeColor: Color = Color.Black, - strokeWidth: Dp = 2.dp, - geodesic: Boolean = false, - extra: Parcelable? = null, -) { - val id = - ( - id ?: polylineId( - listOf( - listHashCode(points), - strokeColor.hashCode(), - strokeWidth.hashCode(), - geodesic.hashCode(), - extra?.hashCode() ?: 0, - ), - ) - ).toString() - var strokeColor by mutableStateOf(strokeColor) - var strokeWidth by mutableStateOf(strokeWidth) - var geodesic by mutableStateOf(geodesic) - var points by StateFlowDelegate>(points) - - var extra by mutableStateOf(extra) - - private fun polylineId(hashCodes: List): Int = - hashCodes.reduce { result, hashCode -> - 31 * result + hashCode - } - - override fun equals(other: Any?): Boolean { - val otherState = (other as? PolylineState) ?: return false - return hashCode() == otherState.hashCode() - } - - override fun hashCode(): Int { - var result = extra?.hashCode() ?: 0 - result = 31 * result + this@PolylineState.strokeColor.hashCode() - result = 31 * result + this@PolylineState.strokeWidth.hashCode() - result = 31 * result + geodesic.hashCode() - result = 31 * result + points.hashCode() - return result - } - - private fun listHashCode(list: List): Int { - var result = 0 - list.forEach { - result = 31 * result + it.hashCode() - } - return result - } - - fun fingerPrint(): PolylineFingerPrint = - PolylineFingerPrint( - id = this.id.hashCode(), - strokeColor = this@PolylineState.strokeColor.hashCode(), - strokeWidth = this@PolylineState.strokeWidth.hashCode(), - geodesic = geodesic.toString().hashCode(), - points = listHashCode(points), - extra = extra?.hashCode() ?: 0, - ) - - fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() -} - -data class PolylineFingerPrint( - val id: Int, - val strokeColor: Int, - val strokeWidth: Int, - val geodesic: Int, - val points: Int, - val extra: Int, -) - -typealias OnPolylineEventHandler = (PolylineState) -> Unit +package com.mapconductor.core.polyline + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.mapconductor.core.features.IGeoPoint +import android.os.Parcelable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +class PolylineState( + points: List, + id: String? = null, + strokeColor: Color = Color.Black, + strokeWidth: Dp = 2.dp, + geodesic: Boolean = false, + extra: Parcelable? = null, +) { + val id = + ( + id ?: polylineId( + listOf( + listHashCode(points), + strokeColor.hashCode(), + strokeWidth.hashCode(), + geodesic.hashCode(), + extra?.hashCode() ?: 0, + ), + ) + ).toString() + var strokeColor by mutableStateOf(strokeColor) + var strokeWidth by mutableStateOf(strokeWidth) + var geodesic by mutableStateOf(geodesic) + var points by mutableStateOf>(points) + var extra by mutableStateOf(extra) + + private fun polylineId(hashCodes: List): Int = + hashCodes.reduce { result, hashCode -> + 31 * result + hashCode + } + + override fun equals(other: Any?): Boolean { + val otherState = (other as? PolylineState) ?: return false + return hashCode() == otherState.hashCode() + } + + override fun hashCode(): Int { + var result = extra?.hashCode() ?: 0 + result = 31 * result + this@PolylineState.strokeColor.hashCode() + result = 31 * result + this@PolylineState.strokeWidth.hashCode() + result = 31 * result + geodesic.hashCode() + result = 31 * result + listHashCode(points) + return result + } + + fun copy( + points: List = this.points, + id: String? = this.id, + strokeColor: Color = this.strokeColor, + strokeWidth: Dp = this.strokeWidth, + geodesic: Boolean = this.geodesic, + extra: Parcelable? = this.extra, + ): PolylineState = + PolylineState( + points = points, + id = id, + strokeColor = strokeColor, + strokeWidth = strokeWidth, + geodesic = geodesic, + extra = extra, + ) + + private fun listHashCode(list: List): Int { + var result = 0 + list.forEach { + result = 31 * result + it.hashCode() + } + return result + } + + fun fingerPrint(): PolylineFingerPrint = + PolylineFingerPrint( + id = this.id.hashCode(), + strokeColor = this@PolylineState.strokeColor.hashCode(), + strokeWidth = this@PolylineState.strokeWidth.hashCode(), + geodesic = geodesic.toString().hashCode(), + points = listHashCode(points), + extra = extra?.hashCode() ?: 0, + ) + + fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() +} + +data class PolylineFingerPrint( + val id: Int, + val strokeColor: Int, + val strokeWidth: Int, + val geodesic: Int, + val points: Int, + val extra: Int, +) + +typealias OnPolylineEventHandler = (PolylineState) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt index b1860d50..4308bdb0 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt @@ -12,7 +12,7 @@ import android.os.Parcelable @Composable fun MapViewScope.Polyline(state: PolylineState) { - val rememberState = remember { state } + val rememberState = remember(state.id) { state } SideEffect { polylineFlow.value = polylineFlow.value + rememberState } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineEntity.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineEntity.kt index 912358e5..40abd109 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineEntity.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineEntity.kt @@ -3,12 +3,12 @@ package com.mapconductor.core.polyline interface PolylineEntity { val polyline: ActualPolyline val state: PolylineState - val stateHashCode: Int + val fingerPrint: PolylineFingerPrint } data class PolylineEntityImpl( override val polyline: ActualPolyline, override val state: PolylineState, ) : PolylineEntity { - override val stateHashCode: Int = state.hashCode() + override val fingerPrint: PolylineFingerPrint = state.fingerPrint() } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt index 949a3ca8..abf854cf 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt @@ -94,8 +94,35 @@ class PolylineOverlayManagerImpl( } } - override suspend fun updatePolyline(polyline: PolylineState) { - TODO("Not implemented yet") + override suspend fun updatePolyline(state: PolylineState) { + semaphore.withPermit { + polylineEntities[state.id]?.let { prevEntity -> + + val updates = + listOf( + object : PolylineRenderer.UpdateParams { + override val entity: PolylineEntity = + PolylineEntityImpl( + state = state, + polyline = prevEntity.polyline, + ) + override val prevEntity: PolylineEntity = prevEntity + }, + ) + + val actualPolylines: List = onChange(updates) + actualPolylines.forEachIndexed { index, actualPolyline -> + actualPolyline?.let { + val entity = + PolylineEntityImpl( + state = state, + polyline = actualPolyline, + ) + polylineEntities[state.id] = entity + } + } + } + } } override suspend fun clearOverlays() { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt index 1c8f94b7..d588459c 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt @@ -20,11 +20,11 @@ interface PolylineRenderer { fun init(polylineOverlayManager: PolylineOverlayManager) - suspend fun addLines(newLines: List): List + suspend fun addPolylines(newLines: List): List - suspend fun removeLines(removeEntities: List>) + suspend fun removePolylines(removeEntities: List>) - suspend fun changeLine(changes: List>): List + suspend fun changePolylines(changes: List>): List } abstract class AbstractPolylineRenderer : PolylineRenderer { diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 4c33a367..8f2d4af1 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -11,6 +11,7 @@ import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler @Composable fun ArcGISMapView( @@ -24,6 +25,7 @@ fun ArcGISMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -66,6 +68,7 @@ fun ArcGISMapView( controller.markerDragListener = onMarkerDrag controller.markerDragEndListener = onMarkerDragEnd controller.circleClickListener = onCircleClick + controller.polylineClickListener = onPolylineClick controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index cdeac514..448442ac 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -129,9 +129,9 @@ class ArcGISMapViewController( override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( - onAdd = polylineRenderer::addLines, - onChange = polylineRenderer::changeLine, - onRemove = polylineRenderer::removeLines, + onAdd = polylineRenderer::addPolylines, + onChange = polylineRenderer::changePolylines, + onRemove = polylineRenderer::removePolylines, ) override val polylineRenderer: PolylineRenderer = diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt index 9b2185f3..ee3d427d 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt @@ -42,7 +42,7 @@ class ArcGISPolylineRenderer( override val holder: ArcGISMapViewHolder, override val coroutine: CoroutineScope, ) : AbstractPolylineRenderer() { - override suspend fun addLines(newLines: List): List { + override suspend fun addPolylines(newLines: List): List { return withContext(coroutine.coroutineContext) { return@withContext newLines.map { state -> @@ -67,18 +67,18 @@ class ArcGISPolylineRenderer( } } - override suspend fun removeLines(removeEntities: List>) { + override suspend fun removePolylines(removeEntities: List>) { val polylines = removeEntities.map { it.polyline } coroutine.launch { polylineLayer.graphics.removeAll(polylines) } } - override suspend fun changeLine(changes: List>): List { + override suspend fun changePolylines(changes: List>): List { return withContext(coroutine.coroutineContext) { return@withContext changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() + val finger = params.entity.fingerPrint + val prevFinger = params.prevEntity.fingerPrint if (finger.points != prevFinger.points) { params.entity.polyline.geometry = createGeometry(params.entity.state) } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 78807155..e371ddf6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -16,6 +16,7 @@ import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler import android.view.ViewGroup @Composable @@ -30,6 +31,7 @@ fun GoogleMapsView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -81,9 +83,10 @@ fun GoogleMapsView( controller.markerDragStartListener = onMarkerDragStart controller.markerDragListener = onMarkerDrag controller.markerDragEndListener = onMarkerDragEnd + controller.circleClickListener = onCircleClick + controller.polylineClickListener = onPolylineClick controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - controller.circleClickListener = onCircleClick holderRef.value = controller.holder controllerRef.value = controller diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index c77d8fcb..083b16ca 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -106,9 +106,9 @@ class GoogleMapViewController( override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( - onAdd = polylineRenderer::addLines, - onChange = polylineRenderer::changeLine, - onRemove = polylineRenderer::removeLines, + onAdd = polylineRenderer::addPolylines, + onChange = polylineRenderer::changePolylines, + onRemove = polylineRenderer::removePolylines, ) override val polygonRenderer: PolygonRenderer = diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt index e2dfa452..657fa7f6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt @@ -49,7 +49,7 @@ class GoogleMapCircleRenderer( .strokeColor(state.strokeColor.toArgb()) .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) .fillColor(state.fillColor.toArgb()) - .clickable(true) + .clickable(false) holder.map.addCircle(options).also { it.tag = state.id } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt index 1ddff0e3..43e58313 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt @@ -1,83 +1,84 @@ -package com.mapconductor.googlemaps.polyline - -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Polyline -import com.google.android.gms.maps.model.PolylineOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polyline.AbstractPolylineRenderer -import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineOverlayManagerImpl -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultGoogleMapPolylineRenderer : PolylineRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolylineOverlayManager = - PolylineOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class GoogleMapPolylineRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addLines(newLines: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newLines.map { state -> - val points = state.points.map { GeoPoint.from(it).toLatLng() } - val options = - PolylineOptions() - .addAll(points) - .color(state.strokeColor.toArgb()) - .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .clickable(true) - holder.map.addPolyline(options).also { - it.tag = state.id - } - } - } - } - - override suspend fun removeLines(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { params -> params.polyline.remove() } - } - } - - override suspend fun changeLine(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val polyline = params.entity.polyline - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() - if (finger.points != prevFinger.points) { - val points = - params.entity.state.points - .map { GeoPoint.from(it).toLatLng() } - polyline.points = points - } - polyline.width = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - polyline.color = - params.entity.state.strokeColor - .toArgb() - return@map polyline - } - } - } -} +package com.mapconductor.googlemaps.polyline + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.Polyline +import com.google.android.gms.maps.model.PolylineOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polyline.AbstractPolylineRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineOverlayManager +import com.mapconductor.core.polyline.PolylineOverlayManagerImpl +import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams +import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class DefaultGoogleMapPolylineRenderer : PolylineRendererFactory { + override fun create( + onAdd: suspend (List) -> List, + onChange: suspend (List>) -> List, + onRemove: suspend (List>) -> Unit, + onPostProcess: (suspend () -> Unit)?, + ): PolylineOverlayManager = + PolylineOverlayManagerImpl( + onRemove = onRemove, + onAdd = onAdd, + onChange = onChange, + onPostProcess = onPostProcess, + ) +} + +class GoogleMapPolylineRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope, +) : AbstractPolylineRenderer() { + override suspend fun addPolylines(newLines: List): List { + return withContext(coroutine.coroutineContext) { + return@withContext newLines.map { state -> + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolylineOptions() + .addAll(points) + .color(state.strokeColor.toArgb()) + .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .clickable(false) + holder.map.addPolyline(options).also { + it.tag = state.id + } + } + } + } + + override suspend fun removePolylines(removeEntities: List>) { + coroutine.launch { + removeEntities.forEach { params -> params.polyline.remove() } + } + } + + override suspend fun changePolylines(changes: List>): List { + return withContext(coroutine.coroutineContext) { + return@withContext changes.map { params -> + val polyline = params.entity.polyline + val finger = params.entity.fingerPrint + val prevFinger = params.prevEntity.fingerPrint +// if (finger.points != prevFinger.points) { + val points = + params.entity.state.points + .map { GeoPoint.from(it).toLatLng() } + polyline.points = points +// } +// printPoints("change", params.entity.state.points) + polyline.width = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() + polyline.color = + params.entity.state.strokeColor + .toArgb() + return@map polyline + } + } + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 1fba4577..7f95a575 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -9,10 +9,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner +import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler import android.util.Log import android.view.ViewGroup import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -30,6 +32,8 @@ fun HereMapView( onMarkerDragEnd: OnMarkerEventHandler? = {}, onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, + onCircleClick: OnCircleEventHandler? = {}, + onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable HereMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -71,6 +75,8 @@ fun HereMapView( controller.markerDragStartListener = onMarkerDragStart controller.markerDragListener = onMarkerDrag controller.markerDragEndListener = onMarkerDragEnd + controller.circleClickListener = onCircleClick + controller.polylineClickListener = onPolylineClick controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index e7472eee..da0c8892 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -1,338 +1,347 @@ -package com.mapconductor.here - -import com.here.sdk.animation.AnimationState -import com.here.sdk.core.GeoOrientation -import com.here.sdk.core.Point2D -import com.here.sdk.gestures.GestureState -import com.here.sdk.gestures.LongPressListener -import com.here.sdk.gestures.TapListener -import com.here.sdk.mapview.MapCamera -import com.here.sdk.mapview.MapCameraAnimationFactory -import com.here.sdk.mapview.MapCameraListener -import com.here.sdk.mapview.MapCameraUpdateFactory -import com.here.sdk.mapview.MapMarker -import com.here.sdk.mapview.MapMeasure -import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.MapScene -import com.here.sdk.mapview.MapView -import com.here.time.Duration -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewHolder -import com.mapconductor.core.map.MapViewState.MoveCameraCallback -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator -import com.mapconductor.here.circle.DefaultHereMapCircleRenderer -import com.mapconductor.here.circle.HereMapCircleRenderer -import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer -import com.mapconductor.here.marker.HereMapMarkerRenderer -import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer -import com.mapconductor.here.polygon.HereMapPolygonRenderer -import com.mapconductor.here.polyline.DefaultHereMapPolylineRenderer -import com.mapconductor.here.polyline.HereMapPolylineRenderer -import com.mapconductor.settings.Settings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -interface IHereMapViewController : MapViewController { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - durationMs: Long, - listener: MoveCameraCallback? = null, - ) -} - -class HereMapViewController( - override val holder: MapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultHereMapPolylineRenderer(), - private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), - private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), -) : BaseMapViewController< - MapCamera.State, - HereMapActualMarker, - HereMapActualCircle, - HereMapActualPolyline, - HereMapActualPolygon, - >(), - IHereMapViewController, - MapCameraListener, - TapListener, - LongPressListener { - companion object { - private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 - } - - private var selectedMarker: MarkerEntity? = null - - override val markerRenderer: MarkerRenderer = - HereMapMarkerRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - - override val polylineRenderer: PolylineRenderer = - HereMapPolylineRenderer( - holder = holder, - coroutine = coroutine, - ) - - override val polygonRenderer = - HereMapPolygonRenderer( - holder = holder, - coroutine = coroutine, - ) - - override val circleRenderer = - HereMapCircleRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addLines, - onChange = polylineRenderer::changeLine, - onRemove = polylineRenderer::removeLines, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - - override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() - polylineOverlayManager.clearOverlays() - polygonOverlayManager.clearOverlays() - circleOverlayManager.clearOverlays() - } - - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) - - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) - - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) - - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) - - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - - init { - setupListeners() - markerRenderer.init(markerOverlayManager) - polygonRenderer.init(polygonOverlayManager) - circleRenderer.init(circleOverlayManager) - } - - override fun setupListeners() { - holder.mapView.camera.removeListener(this) - holder.mapView.camera.addListener(this) - holder.mapView.gestures.tapListener = this - holder.mapView.gestures.longPressListener = this - } - - override fun moveCamera( - dstPosition: MapCameraPosition, - listener: MoveCameraCallback?, - ) { - val camera = this.holder.mapView.camera - val adjustCameraUpdate = - MapCameraUpdateFactory.lookAt( - GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), - GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), - MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), - ) - - camera.applyUpdate(adjustCameraUpdate) - listener?.onComplete(true) - } - - override fun animateCamera( - dstPosition: MapCameraPosition, - durationMs: Long, - listener: MoveCameraCallback?, - ) { - val camera = this.holder.mapView.camera - -// bowFactor > 0: 最初にズームアウト → 到達時にズームイン -// bowFactor < 0: 最初にズームイン → 到達時にズームアウト(ややレア) -// bowFactor = 0: 常に同じズーム(直線的) - val bowFactor = 1.0 - val animation = - MapCameraAnimationFactory.flyTo( - GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), - GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), - MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), - bowFactor, - Duration.ofMillis(durationMs), - ) - coroutine.launch { - camera.startAnimation(animation) { animState -> - when (animState) { - // Do nothing here - AnimationState.STARTED -> Unit - AnimationState.COMPLETED -> listener?.onComplete(true) - AnimationState.CANCELLED -> listener?.onComplete(false) - } - } - } - } - - override fun onMapCameraUpdated(cameraState: MapCamera.State) { - val correctCameraState = - MapCamera.State( - cameraState.targetCoordinates, - GeoOrientation(cameraState.orientationAtTarget.bearing, cameraState.orientationAtTarget.tilt), - 0.0, - cameraState.zoomLevel - ZOOM_ADJUST_VALUE, - ) - - cameraMoveListener?.invoke(correctCameraState) - } - - override fun onTap(point: Point2D) { - val position = this.getGeoPointFromPoint(point) ?: return - val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE - val tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity() - - val entity = - markerRenderer.findNearestMarker( - position = position, - tolerance = tolerance, - zoom = zoom, - ) - if (entity != null) { - markerClickListener?.invoke(entity.state) - return - } - - // TODO: Implement click handling for other overlays - - // If no overlay is processed, process the tap as onMapClick - mapClickListener?.let { it(position) } - } - - override fun onLongPress( - gesture: GestureState, - point: Point2D, - ) { - val position = this.getGeoPointFromPoint(point) ?: return - - when (gesture.value) { - GestureState.BEGIN.value -> { - val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE - val tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity() - - val entity = - markerRenderer.findNearestMarker( - position = position, - tolerance = tolerance, - zoom = zoom, - ) ?: return - - entity.state.position = position - selectedMarker = entity - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(entity.state, true) - - markerDragStartListener?.invoke(entity.state) - } - - GestureState.UPDATE.value -> { - selectedMarker?.also { selected -> - holder.mapView.viewToGeoCoordinates(point)?.also { coordinates -> - selected.marker.coordinates = coordinates - selected.state.position = coordinates.toGeoPoint() - } - markerDragListener?.invoke(selected.state) - } - } - - GestureState.END.value, GestureState.CANCEL.value -> { - selectedMarker?.also { selected -> - markerOverlayManager.markerManager.updateEntity(selected) - - // Restore the recomposition for the position property - markerRenderer.setDraggingState(selected.state, false) - - markerDragEndListener?.invoke(selected.state) - selectedMarker = null - } - } - } - } - - private fun getGeoPointFromPoint(point: Point2D): GeoPoint? = - holder.mapView - .viewToGeoCoordinates(point) - ?.toGeoPoint() -} +package com.mapconductor.here + +import com.here.sdk.animation.AnimationState +import com.here.sdk.core.GeoOrientation +import com.here.sdk.core.Point2D +import com.here.sdk.gestures.GestureState +import com.here.sdk.gestures.LongPressListener +import com.here.sdk.gestures.TapListener +import com.here.sdk.mapview.MapCamera +import com.here.sdk.mapview.MapCameraAnimationFactory +import com.here.sdk.mapview.MapCameraListener +import com.here.sdk.mapview.MapCameraUpdateFactory +import com.here.sdk.mapview.MapMarker +import com.here.sdk.mapview.MapMeasure +import com.here.sdk.mapview.MapPolygon +import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.MapScene +import com.here.sdk.mapview.MapView +import com.here.time.Duration +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.CircleClickEvent +import com.mapconductor.core.circle.CircleOverlayManager +import com.mapconductor.core.circle.CircleRendererFactory +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.core.map.MapViewState.MoveCameraCallback +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerOverlayManager +import com.mapconductor.core.marker.MarkerRenderer +import com.mapconductor.core.marker.MarkerRendererFactory +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.polygon.PolygonOverlayManager +import com.mapconductor.core.polygon.PolygonRendererFactory +import com.mapconductor.core.polyline.PolylineOverlayManager +import com.mapconductor.core.polyline.PolylineRenderer +import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.here.circle.DefaultHereMapCircleRenderer +import com.mapconductor.here.circle.HereMapCircleRenderer +import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer +import com.mapconductor.here.marker.HereMapMarkerRenderer +import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer +import com.mapconductor.here.polygon.HereMapPolygonRenderer +import com.mapconductor.here.polyline.DefaultHereMapPolylineRenderer +import com.mapconductor.here.polyline.HereMapPolylineRenderer +import com.mapconductor.settings.Settings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +interface IHereMapViewController : MapViewController { + fun moveCamera( + dstPosition: MapCameraPosition, + listener: MoveCameraCallback? = null, + ) + + fun animateCamera( + dstPosition: MapCameraPosition, + durationMs: Long, + listener: MoveCameraCallback? = null, + ) +} + +class HereMapViewController( + override val holder: MapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), + override val hexGeocell: HexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ), + private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), + private val polylineRendererFactory: PolylineRendererFactory = + DefaultHereMapPolylineRenderer(), + private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), + private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), +) : BaseMapViewController< + MapCamera.State, + HereMapActualMarker, + HereMapActualCircle, + HereMapActualPolyline, + HereMapActualPolygon, + >(), + IHereMapViewController, + MapCameraListener, + TapListener, + LongPressListener { + companion object { + private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 + } + + private var selectedMarker: MarkerEntity? = null + + override val markerRenderer: MarkerRenderer = + HereMapMarkerRenderer( + holder = holder, + coroutine = coroutine, + ) + + override fun createMarkerOverlayManager(): MarkerOverlayManager = + markerRendererFactory.create( + hexGeocell = hexGeocell, + onIconAdd = markerRenderer::addIcons, + onIconRemove = markerRenderer::removeIcons, + onIconChange = markerRenderer::changeIcons, + onAnimate = markerRenderer::animate, + ) + + override val polylineRenderer: PolylineRenderer = + HereMapPolylineRenderer( + holder = holder, + coroutine = coroutine, + ) + + override val polygonRenderer = + HereMapPolygonRenderer( + holder = holder, + coroutine = coroutine, + ) + + override val circleRenderer = + HereMapCircleRenderer( + holder = holder, + coroutine = coroutine, + ) + + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { + } + + override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { + } + + override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { + } + + override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { + } + + override fun createPolylineOverlayManager(): PolylineOverlayManager = + polylineRendererFactory.create( + onAdd = polylineRenderer::addPolylines, + onChange = polylineRenderer::changePolylines, + onRemove = polylineRenderer::removePolylines, + ) + + override fun createPolygonOverlayManager(): PolygonOverlayManager = + polygonRendererFactory.create( + onAdd = polygonRenderer::addPolygons, + onChange = polygonRenderer::changePolygon, + onRemove = polygonRenderer::removePolygons, + ) + + override fun createCircleOverlayManager(): CircleOverlayManager = + circleRendererFactory.create( + onAdd = circleRenderer::addCircles, + onChange = circleRenderer::changeCircle, + onRemove = circleRenderer::removeCircles, + ) + + override suspend fun clearOverlays() { + markerOverlayManager.clearOverlays() + polylineOverlayManager.clearOverlays() + polygonOverlayManager.clearOverlays() + circleOverlayManager.clearOverlays() + } + + override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) + + override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) + + override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) + + override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) + + override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) + + override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + + init { + setupListeners() + markerRenderer.init(markerOverlayManager) + polygonRenderer.init(polygonOverlayManager) + circleRenderer.init(circleOverlayManager) + } + + override fun setupListeners() { + holder.mapView.camera.removeListener(this) + holder.mapView.camera.addListener(this) + holder.mapView.gestures.tapListener = this + holder.mapView.gestures.longPressListener = this + } + + override fun moveCamera( + dstPosition: MapCameraPosition, + listener: MoveCameraCallback?, + ) { + val camera = this.holder.mapView.camera + val adjustCameraUpdate = + MapCameraUpdateFactory.lookAt( + GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), + GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), + MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), + ) + + camera.applyUpdate(adjustCameraUpdate) + listener?.onComplete(true) + } + + override fun animateCamera( + dstPosition: MapCameraPosition, + durationMs: Long, + listener: MoveCameraCallback?, + ) { + val camera = this.holder.mapView.camera + +// bowFactor > 0: 最初にズームアウト → 到達時にズームイン +// bowFactor < 0: 最初にズームイン → 到達時にズームアウト(ややレア) +// bowFactor = 0: 常に同じズーム(直線的) + val bowFactor = 1.0 + val animation = + MapCameraAnimationFactory.flyTo( + GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), + GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), + MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), + bowFactor, + Duration.ofMillis(durationMs), + ) + coroutine.launch { + camera.startAnimation(animation) { animState -> + when (animState) { + // Do nothing here + AnimationState.STARTED -> Unit + AnimationState.COMPLETED -> listener?.onComplete(true) + AnimationState.CANCELLED -> listener?.onComplete(false) + } + } + } + } + + override fun onMapCameraUpdated(cameraState: MapCamera.State) { + val correctCameraState = + MapCamera.State( + cameraState.targetCoordinates, + GeoOrientation(cameraState.orientationAtTarget.bearing, cameraState.orientationAtTarget.tilt), + 0.0, + cameraState.zoomLevel - ZOOM_ADJUST_VALUE, + ) + + cameraMoveListener?.invoke(correctCameraState) + } + + override fun onTap(point: Point2D) { + val touchPosition = this.getGeoPointFromPoint(point) ?: return + val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + + val entity = + markerRenderer.findNearestMarker( + position = touchPosition, + tolerance = tolerance, + zoom = zoom, + ) + if (entity != null) { + markerClickListener?.invoke(entity.state) + return + } + + circleOverlayManager.find(touchPosition)?.let { entity -> + val event = + CircleClickEvent( + state = entity.state, + position = touchPosition, + ) + circleClickListener?.invoke(event) + return + } + + // If no overlay is processed, process the tap as onMapClick + mapClickListener?.let { it(touchPosition) } + } + + override fun onLongPress( + gesture: GestureState, + point: Point2D, + ) { + val position = this.getGeoPointFromPoint(point) ?: return + + when (gesture.value) { + GestureState.BEGIN.value -> { + val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + + val entity = + markerRenderer.findNearestMarker( + position = position, + tolerance = tolerance, + zoom = zoom, + ) ?: return + + entity.state.position = position + selectedMarker = entity + + // Suppress the recomposition for the position property + markerRenderer.setDraggingState(entity.state, true) + + markerDragStartListener?.invoke(entity.state) + } + + GestureState.UPDATE.value -> { + selectedMarker?.also { selected -> + holder.mapView.viewToGeoCoordinates(point)?.also { coordinates -> + selected.marker.coordinates = coordinates + selected.state.position = coordinates.toGeoPoint() + } + markerDragListener?.invoke(selected.state) + } + } + + GestureState.END.value, GestureState.CANCEL.value -> { + selectedMarker?.also { selected -> + markerOverlayManager.markerManager.updateEntity(selected) + + // Restore the recomposition for the position property + markerRenderer.setDraggingState(selected.state, false) + + markerDragEndListener?.invoke(selected.state) + selectedMarker = null + } + } + } + } + + private fun getGeoPointFromPoint(point: Point2D): GeoPoint? = + holder.mapView + .viewToGeoCoordinates(point) + ?.toGeoPoint() +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt index a79c1b1d..9aba0664 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt @@ -40,7 +40,7 @@ class HereMapPolylineRenderer( override val holder: HereMapViewHolder, override val coroutine: CoroutineScope, ) : AbstractPolylineRenderer() { - override suspend fun addLines(newLines: List): List { + override suspend fun addPolylines(newLines: List): List { val polylines = newLines.map { state -> val geoPolyline = createGeoPolyline(state) @@ -53,17 +53,17 @@ class HereMapPolylineRenderer( return polylines } - override suspend fun removeLines(removeEntities: List>) { + override suspend fun removePolylines(removeEntities: List>) { val polylines = removeEntities.map { it.polyline } coroutine.launch { holder.map.removeMapPolylines(polylines) } } - override suspend fun changeLine(changes: List>): List { + override suspend fun changePolylines(changes: List>): List { return changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() + val finger = params.entity.fingerPrint + val prevFinger = params.prevEntity.fingerPrint if (finger.points != prevFinger.points) { val geoPolyline = createGeoPolyline(params.entity.state) params.entity.polyline.geometry = geoPolyline diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index f4d60b41..3d54058a 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -10,6 +10,7 @@ import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polyline.OnPolylineEventHandler import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -26,6 +27,7 @@ fun MapboxMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -73,6 +75,7 @@ fun MapboxMapView( controller.markerDragListener = onMarkerDrag controller.markerDragEndListener = onMarkerDragEnd controller.circleClickListener = onCircleClick + controller.polylineClickListener = onPolylineClick controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index ac4156e1..43c06763 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -153,9 +153,9 @@ internal class MapboxMapViewController( override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( - onAdd = polylineRenderer::addLines, - onChange = polylineRenderer::changeLine, - onRemove = polylineRenderer::removeLines, + onAdd = polylineRenderer::addPolylines, + onChange = polylineRenderer::changePolylines, + onRemove = polylineRenderer::removePolylines, onPostProcess = polylineRenderer::redraw, ) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt index 1aa6d847..ecd3de54 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt @@ -40,7 +40,7 @@ class MapboxPolylineRenderer( override val coroutine: CoroutineScope, private val layer: MapboxPolylineLayer, ) : AbstractPolylineRenderer() { - override suspend fun addLines(newLines: List): List { + override suspend fun addPolylines(newLines: List): List { val polylines = newLines.map { state -> val points = state.points.map { GeoPoint.from(it).toPoint() } @@ -56,12 +56,14 @@ class MapboxPolylineRenderer( return polylines } - override suspend fun removeLines(removeEntities: List>) { + override suspend fun removePolylines(removeEntities: List>) { val featureIds = removeEntities.map { "polyline-${it.state.id}" } layer.source.removeGeoJSONSourceFeatures(featureIds) } - override suspend fun changeLine(changes: List>): List { + override suspend fun changePolylines( + changes: List>, + ): List { val features = changes.map { params -> val state = params.entity.state