diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f50d2426..3f3779e0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,9 @@ { "permissions": { "allow": [ - "Bash(./gradlew:*)" + "Bash(./gradlew:*)", + "Bash(grep:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/README.md b/README.md index e0f4433a..86043f8e 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,94 @@ -# セットアップ +# MapConductor Android SDK -1. リポジトリをクローン -``` +A unified mapping library that provides a common API for multiple map providers including Google Maps, Mapbox, HERE, and ArcGIS. Write once, deploy across all major mapping platforms. + +## Features + +- **🗺️ Multi-Provider Support**: Seamlessly switch between Google Maps, Mapbox, HERE, and ArcGIS with a single API +- **🎯 Unified Interface**: Common abstractions for markers, circles, polylines, polygons, and ground overlays +- **⚡ High Performance**: Spatial indexing with hexagonal cells for efficient marker clustering +- **🔄 Reactive State**: Built on Kotlin StateFlow for reactive UI updates +- **🎨 Jetpack Compose**: Modern Android UI toolkit integration + +## Architecture + +### Module Structure + +- **`mapconductor-core`**: Core abstractions and shared functionality +- **`mapconductor-for-googlemaps`**: Google Maps implementation +- **`mapconductor-for-mapbox`**: Mapbox implementation +- **`mapconductor-for-here`**: HERE Maps implementation +- **`mapconductor-for-arcgis`**: ArcGIS implementation +- **`mapconductor-icons`**: Reusable marker icon components +- **`example-app`**: Comprehensive demo application + +### Key Components + +- **MapViewController**: Abstract controller interface for all map providers +- **MapViewBase**: Generic Compose-based map view component +- **Overlay Managers**: Separate managers for markers, circles, polylines, and polygons +- **Projection Utilities**: WebMercator and WGS84 coordinate transformations +- **HexGeocell**: Spatial indexing system for performance optimization + +## Quick Start + +### 1. Setup + +Clone the repository: +```bash git clone https://github.com/MapConductor/android-sdk.git ``` -2. https://github.com/MapConductor/map-sdk-credentials/ から`secrets.properties` をプロジェクトルートに追加保存する +Add `secrets.properties` to the project root from https://github.com/MapConductor/map-sdk-credentials/ + +### 2. Basic Usage + +```kotlin +@Composable +fun MyMapScreen() { + GoogleMapView( + modifier = Modifier.fillMaxSize(), + onMapReady = { controller -> + // Add markers, circles, polylines, etc. + } + ) { mapState, controller -> + // Your map content here + } +} +``` + +### 3. Switch Map Providers -3. Android Studioでビルド +Simply change the map view component: +```kotlin +// Google Maps +GoogleMapView { /* ... */ } -# コーディングスタイル +// Mapbox +MapboxMapView { /* ... */ } -KtLintに従います。ローカルで実行する場合は、下記コマンドを実行します(可能ならば自動修正されます)。 +// HERE Maps +HereMapView { /* ... */ } +// ArcGIS +ArcGISMapView { /* ... */ } ``` + +## Development + +### Building +```bash +./gradlew build +``` + +### Code Style +This project follows KtLint conventions: +```bash ./gradlew allLintChecks ``` -# 基本実装状況 (Android) + +## Feature Implementation Status | | Google Maps | Mapbox | Here | ArcGIS | |-----------------|-------------|----------|----------|----------------| @@ -26,6 +97,6 @@ KtLintに従います。ローカルで実行する場合は、下記コマン | Circle | ☑ | ☑ | ☐ | ☐ (wip) | | Polyline | ☑ | ☑ | ☑ | ☑ | | Polygon | ☐ | ☐ | ☐ | ☐ | -| GroundOverlay | ☐ | ☐ | ☐ | ☐ | +| GroundImage | ☐ | N/A | N/A | N/A | | RasterTileLayer | ☐ | ☐ | ☐ | ☐ | | VectorTileLayer | ☐ | ☐ | ☐ | ☐ | diff --git a/build.gradle.kts b/build.gradle.kts index ab1d3a7d..8dd3a7f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.library) apply false - id("org.jlleitschuh.gradle.ktlint") version "13.0.0-rc.1" + alias(libs.plugins.jlleitschuh.ktlint) apply false } buildscript { @@ -17,22 +17,25 @@ buildscript { } } -val modules: List = rootDir.resolve("projects.properties").readLines() - .firstOrNull { it.startsWith("modules=") } - ?.removePrefix("modules=") - ?.split(",") - ?.map { it.trim() } - ?.filter { it.isNotEmpty() } - ?: emptyList() +val modules: List = + rootDir + .resolve("projects.properties") + .readLines() + .firstOrNull { it.startsWith("modules=") } + ?.removePrefix("modules=") + ?.split(",") + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + ?: emptyList() tasks.register("allLintChecks") { group = "verification" description = "Run ktlintFormat and lint for all modules" - val lintTasks = modules.map { module -> - listOf(":$module:ktlintFormat", ":$module:lint") - } + val lintTasks = + modules.map { module -> + listOf(":$module:ktlintFormat", ":$module:lint") + } dependsOn(lintTasks) } - diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index bc1fe2b5..bb1ea10c 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -81,6 +81,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.appcompat) + implementation(platform(libs.firebase.bom)) // Google Maps SDK implementation(libs.play.services.maps) @@ -104,6 +105,7 @@ dependencies { implementation(project(":mapconductor-for-mapbox")) implementation(project(":mapconductor-for-arcgis")) implementation(libs.androidx.vectordrawable) + implementation(libs.androidx.room.runtime.android) testImplementation(libs.junit) testImplementation(libs.androidx.core) 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 e1efae26..3e0d9c36 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.animation.AnimationMapPage @@ -26,6 +28,8 @@ import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage import com.mapconductor.example.pages.mapDesign.MapDesignMapPage +import com.mapconductor.example.pages.marker.MarkerBasicPage +import com.mapconductor.example.pages.polygon.PolygonMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar @@ -33,8 +37,21 @@ import com.mapconductor.example.ui.sidebar.SidebarItem import com.mapconductor.example.ui.theme.AppTheme @Composable -fun DemoAppScreen() { - val navigationViewModel: NavigationViewModel = viewModel() +fun DemoAppScreen(initPage: String = "map") { + val navigationViewModel: NavigationViewModel = + viewModel( + factory = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(NavigationViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return NavigationViewModel(initPage) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + }, + ) + val currentPage by navigationViewModel.currentPage val isSidebarExpanded by navigationViewModel.isSidebarExpanded val context = LocalContext.current @@ -65,6 +82,12 @@ fun DemoAppScreen() { icon = Icons.Default.Home, route = "map", ), + SidebarItem( + id = "marker-basic", + title = "Marker", + icon = Icons.Default.Home, + route = "marker-basic", + ), SidebarItem( id = "flyTo", title = "Move camera", @@ -89,6 +112,12 @@ fun DemoAppScreen() { icon = Icons.Default.PlayArrow, route = "polyline", ), + SidebarItem( + id = "polygon", + title = "polygon ", + icon = Icons.Default.PlayArrow, + route = "polygon", + ), SidebarItem( id = "animation", title = "Animation ", @@ -125,6 +154,11 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "marker-basic" -> { + MarkerBasicPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "circle" -> { CircleMapPage( onToggleSidebar = navigationViewModel::toggleSidebar, @@ -135,14 +169,14 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } - "flyTo" -> { - FlyToMapPage( - icons = flyToMapPageIcons, + "polygon" -> { + PolygonMapPage( onToggleSidebar = navigationViewModel::toggleSidebar, ) } - "animation" -> { - AnimationMapPage( + "flyTo" -> { + FlyToMapPage( + icons = flyToMapPageIcons, onToggleSidebar = navigationViewModel::toggleSidebar, ) } @@ -152,6 +186,11 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "animation" -> { + AnimationMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "mapDesign" -> { MapDesignMapPage( onToggleSidebar = navigationViewModel::toggleSidebar, diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index b59feaab..42a5f92b 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -11,7 +11,9 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { - DemoAppScreen() + DemoAppScreen( + initPage = "map", + ) } } } 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 36b09250..43bbaa9c 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -3,20 +3,21 @@ package com.mapconductor.example import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.mapconductor.arcgis.ArcGISMapView -import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.core.MapViewScope import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.googlemaps.GoogleMapViewState +import com.mapconductor.googlemaps.GoogleMapViewStateImpl import com.mapconductor.googlemaps.GoogleMapsView import com.mapconductor.here.HereMapView -import com.mapconductor.here.HereMapViewState +import com.mapconductor.here.HereViewStateImpl import com.mapconductor.mapbox.MapboxMapView -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.mapbox.MapboxViewStateImpl @Composable fun MapViewContainer( @@ -31,11 +32,12 @@ fun MapViewContainer( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, onGroundImageClick: OnGroundImageEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { when (state) { - is GoogleMapViewState -> + is GoogleMapViewStateImpl -> GoogleMapsView( modifier = modifier, state = state, @@ -48,11 +50,12 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, onGroundImageClick = onGroundImageClick, content = content, ) - is HereMapViewState -> + is HereViewStateImpl -> HereMapView( modifier = modifier, state = state, @@ -63,13 +66,13 @@ fun MapViewContainer( onMarkerDragEnd = onMarkerDragEnd, onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, - onGroundImageClick = onGroundImageClick, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, content = content, ) - is MapboxMapViewState -> + is MapboxViewStateImpl -> MapboxMapView( modifier = modifier, state = state, @@ -82,11 +85,11 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, - onGroundImageClick = onGroundImageClick, + onPolygonClick = onPolygonClick, content = content, ) - is ArcGISMapViewState -> + is ArcGISMapViewStateImpl -> ArcGISMapView( modifier = modifier, state = state, @@ -98,8 +101,8 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, - onGroundImageClick = onGroundImageClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, 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 0ed71062..36047dcb 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 @@ -4,8 +4,10 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -class NavigationViewModel : ViewModel() { - private val _currentPage = mutableStateOf("groundImage") +class NavigationViewModel( + initPage: String, +) : ViewModel() { + private val _currentPage = mutableStateOf(initPage) val currentPage: State = _currentPage private val _isSidebarExpanded = mutableStateOf(false) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt index d404c60e..f6d2c0a6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @@ -24,7 +25,7 @@ fun AnimationMapPage( onToggleSidebar: () -> Unit = {}, ) { DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> diff --git a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt index 33dd422b..d2c695a3 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt @@ -102,6 +102,6 @@ class AnimationPageViewModelImpl : Log.i("AnimationPageViewModelImpl", "onMarkerClick: ${clicked.id}") val spot = exampleSpots.firstOrNull { it.id == clicked.id } - clicked.animation = spot?.animation + clicked.setAnimation(spot?.animation) } } 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 7d983ae6..81552777 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 @@ -1,47 +1,43 @@ package com.mapconductor.example.pages.circle import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.circle.Circle +import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer @Composable fun CircleMapComponent( mapViewState: MapViewState<*>?, - viewModel: CirclePageViewModel, + circleState: CircleState, + centerMarker: MarkerState, + edgeMarker: MarkerState, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler = {}, - onMarkerClick: OnMarkerEventHandler = {}, onCircleClick: OnCircleEventHandler = {}, - onMarkerDrag: OnMarkerEventHandler = {}, + onMarkerMove: OnMarkerEventHandler = {}, ) { mapViewState?.let { it -> MapViewContainer( modifier = modifier, state = it, - onMapClick = onMapClick, - onMarkerClick = onMarkerClick, onCircleClick = onCircleClick, - onMarkerDrag = onMarkerDrag, + onMarkerDragStart = onMarkerMove, + onMarkerDrag = onMarkerMove, + onMarkerDragEnd = onMarkerMove, ) { // Circle - Circle(viewModel.circleState) + Circle(circleState) // Center marker (not draggable) - key(viewModel.centerMarker.id) { - Marker(viewModel.centerMarker) - } + Marker(centerMarker) // Edge marker (draggable) - key(viewModel.edgeMarker.id) { - Marker(viewModel.edgeMarker) - } + Marker(edgeMarker) } } } 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 f541b893..619113bd 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 @@ -1,29 +1,29 @@ package com.mapconductor.example.pages.circle +import androidx.compose.foundation.layout.Column 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.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember 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.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @Composable -fun CircleMapPage( - viewModel: CirclePageViewModel = CirclePageViewModelImpl(), - onToggleSidebar: () -> Unit = {}, -) { +fun CircleMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { CirclePageViewModelImpl() } DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> @@ -31,15 +31,16 @@ fun CircleMapPage( CircleMapComponent( mapViewState = mapViewState.value, - viewModel = viewModel, - onMapClick = viewModel::onMapClick, - onMarkerClick = viewModel::onMarkerClick, + circleState = viewModel.circleState, + centerMarker = viewModel.centerMarker, + edgeMarker = viewModel.edgeMarker, onCircleClick = viewModel::onCircleClick, - onMarkerDrag = viewModel::onMarkerDrag, + onMarkerMove = viewModel::onMarkerMove, ) - // Message Card MessageCard( + title = "Circle Example", + maxHeight = 250.dp, modifier = Modifier .align(Alignment.BottomStart) @@ -48,12 +49,36 @@ fun CircleMapPage( start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), - title = "Messages", ) { - Text( - text = stringResource(R.string.circle_example_description), - modifier = Modifier.fillMaxSize(), - ) + // Fill Opacity Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") + Slider( + value = viewModel.fillOpacity, + onValueChange = { viewModel.fillOpacity = it }, + valueRange = 0f..1f, + colors = + SliderDefaults.colors(), + ) + } + + // Stroke Width Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") + Slider( + value = viewModel.strokeWidth, + onValueChange = { viewModel.strokeWidth = it }, + valueRange = 0f..10f, + colors = + SliderDefaults.colors(), + ) + } } ToastHost( 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 8ad89b81..3f7b7144 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 @@ -4,10 +4,11 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf 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.dp import androidx.lifecycle.ViewModel -import com.mapconductor.core.circle.CircleClickEvent +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.MapCameraPosition @@ -31,6 +32,8 @@ interface CirclePageViewModel { val centerMarker: MarkerState val edgeMarker: MarkerState val circleState: CircleState + var fillOpacity: Float + var strokeWidth: Float fun onMapViewChanged(state: MapViewState<*>) @@ -38,9 +41,9 @@ interface CirclePageViewModel { fun onMapClick(clicked: GeoPoint) - fun onCircleClick(event: CircleClickEvent) + fun onCircleClick(event: CircleEvent) - fun onMarkerDrag(dragged: MarkerState) + fun onMarkerMove(dragged: MarkerState) fun showToast(text: String) @@ -54,14 +57,16 @@ class CirclePageViewModelImpl : 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), + Color.Blue, + Color.Red, + Color.Green, + Color.Cyan, + Color.LightGray, + Color.Magenta, ) private var tapIdx = 0 + override var fillOpacity by mutableStateOf(0.3f) + override var strokeWidth by mutableStateOf(3.0f) override val initCameraPosition = MapCameraPosition( @@ -117,19 +122,16 @@ class CirclePageViewModelImpl : haversineDistance(circleCenter, _edgeMarker.value.position) } - private val _circleState: MutableState = - mutableStateOf( + override val circleState: CircleState + get() = CircleState( id = "circle", center = circleCenter, - radiusMeters = 1000.0, // Initial radius + radiusMeters = radiusMeters, // Initial radius strokeColor = Color.Blue.copy(alpha = 0.5f), - strokeWidth = 2.dp, - fillColor = this.colors[0], - ), - ) - override val circleState: CircleState - get() = _circleState.value + strokeWidth = strokeWidth.dp, + fillColor = this.colors[0].copy(alpha = fillOpacity), + ) private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() @@ -146,17 +148,17 @@ class CirclePageViewModelImpl : showToast("Map clicked at: ${clicked.toUrlValue()}") } - override fun onCircleClick(event: CircleClickEvent) { + override fun onCircleClick(event: CircleEvent) { this.tapIdx = (this.tapIdx + 1) % this.colors.size - event.state.fillColor = this.colors[this.tapIdx] + event.state.fillColor = this.colors[this.tapIdx].copy(alpha = fillOpacity) showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") } - override fun onMarkerDrag(dragged: MarkerState) { + override fun onMarkerMove(dragged: MarkerState) { _edgeMarker.value.position = dragged.position // Update circle radius - _circleState.value.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) + circleState.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) // showToast("Radius updated: ${radiusMeters.toInt()}m") } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index a6fce2c2..9a3a7224 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -9,16 +9,15 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.GroundImageCapableMapViewItems import com.mapconductor.example.ui.MessageCard @Composable @@ -26,22 +25,10 @@ fun GroundImageMapPage( groundImageResources: GroundImageResources, onToggleSidebar: () -> Unit = {}, ) { - val viewModel: GroundImageMapPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(GroundImageMapPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return GroundImageMapPageViewModelImpl(groundImageResources) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) + val viewModel = remember { GroundImageMapPageViewModelImpl(groundImageResources) } DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = GroundImageCapableMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> @@ -64,7 +51,7 @@ fun GroundImageMapPage( start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), - title = "Messages", + title = "GroundImage Example", ) { Column { Text("opacity: ${"%.2f".format(viewModel.opacity)}", color = Color.Black) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImagePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt similarity index 100% rename from example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImagePageViewModel.kt rename to example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt index e8fe290b..28321be1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt @@ -2,7 +2,6 @@ package com.mapconductor.example.pages.map.flyto import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState @@ -26,16 +25,12 @@ fun FlyToMapComponent( ) { // Render polylines connecting all markers polylines.forEach { polyline -> - key(polyline.id) { - Polyline(polyline) - } + Polyline(polyline) } // Render markers for fly to destinations markers.forEach { marker -> - key(marker.id) { - Marker(marker) - } + Marker(marker) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt index 80c0303a..5566c9ca 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt @@ -18,13 +18,12 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @@ -33,22 +32,10 @@ fun FlyToMapPage( icons: FlyToMapIcons, onToggleSidebar: () -> Unit = {}, ) { - val viewModel: FlyToPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(FlyToPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return FlyToPageViewModelImpl(icons) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) + val viewModel: FlyToPageViewModel = remember { FlyToPageViewModelImpl(icons) } DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt index 46746739..e4408951 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt @@ -1,6 +1,8 @@ package com.mapconductor.example.pages.mapDesign import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState import com.mapconductor.example.MapViewContainer @@ -11,6 +13,7 @@ fun MapDesignMapComponent( modifier: Modifier = Modifier, ) { mapViewState?.let { state -> + MapViewContainer( modifier = modifier, state = state, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt index 472456b5..763ec664 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -22,133 +21,121 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel -import com.mapconductor.arcgis.ArcGISDesign +import com.mapconductor.arcgis.ArcGISDesignType import com.mapconductor.arcgis.ArcGISMapViewState -import com.mapconductor.core.map.MapDesignType import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard -import com.mapconductor.googlemaps.GoogleMapDesign +import com.mapconductor.googlemaps.GoogleMapDesignType import com.mapconductor.googlemaps.GoogleMapViewState -import com.mapconductor.here.HereMapDesign -import com.mapconductor.here.HereMapViewState -import com.mapconductor.mapbox.MapboxMapDesign -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.here.HereMapDesignType +import com.mapconductor.here.HereViewState +import com.mapconductor.mapbox.MapboxDesignType +import com.mapconductor.mapbox.MapboxViewState @OptIn(ExperimentalMaterial3Api::class) @Composable fun MapDesignMapPage(onToggleSidebar: () -> Unit = {}) { - val viewModel: MapDesignPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(MapDesignPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return MapDesignPageViewModelImpl() as T - } - throw IllegalArgumentException("Unknown VieModel class") - } - }, - ) + val viewModel = remember { MapDesignPageViewModelImpl() } - val optionsState = viewModel.options.collectAsState() + val mapDesignOptions = viewModel.mapDesignOptions.collectAsState() DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> val mapViewState = viewModel.mapViewState.collectAsState() - - fun callChangeMapDesignType( - state: MapViewState<*>, - designType: MapDesignType<*>, - ) { - when (state) { - is GoogleMapViewState -> (designType as? GoogleMapDesign)?.let { state.changeMapDesignType(it) } - is HereMapViewState -> (designType as? HereMapDesign)?.let { state.changeMapDesignType(it) } - is MapboxMapViewState -> (designType as? MapboxMapDesign)?.let { state.changeMapDesignType(it) } - is ArcGISMapViewState -> (designType as? ArcGISDesign)?.let { state.changeMapDesignType(it) } - } - } + val mapDesignOptions = viewModel.mapDesignOptions.collectAsState() MapDesignMapComponent( mapViewState = mapViewState.value, ) - // Message Card - 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 = "Select Map Design...", - ) { - var expanded by remember { mutableStateOf(false) } - val items = optionsState.value - - // SDKキー(Google/Here/Mapbox/ArcGISで切替) - val sdkKey = - when (mapViewState.value) { - is GoogleMapViewState -> "google" - is HereMapViewState -> "here" - is MapboxMapViewState -> "mapbox" - is ArcGISMapViewState -> "arcgis" - else -> "none" + mapViewState.value?.let { state -> + // Message Card + 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 = "Select Map Design...", + ) { + key(state) { + MapDesignTypeSelector( + state = state, + mapDesignOptions = mapDesignOptions.value, + ) } - - var selectedLabel by rememberSaveable(sdkKey) { - // SDKごとに独立して保存 - mutableStateOf(items.firstOrNull()?.label ?: "") } + } + } +} - // ★ options(候補リスト)が変わったら選択と展開状態をリセット - LaunchedEffect(items) { - selectedLabel = items.firstOrNull()?.label ?: "" - expanded = false - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MapDesignTypeSelector( + state: MapViewState<*>, + mapDesignOptions: List, +) { + var expanded by remember { mutableStateOf(false) } - val enabled = mapViewState.value != null && items.isNotEmpty() + var selectedLabel by rememberSaveable(state.id) { + mutableStateOf(mapDesignOptions.firstOrNull()?.label ?: "") + } + + val onClick: (MapDesignOption) -> Unit = fun (mapDesignOption: MapDesignOption) { + selectedLabel = mapDesignOption.label + expanded = false + when (state) { + is GoogleMapViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as GoogleMapDesignType + } + is HereViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as HereMapDesignType + } + is ArcGISMapViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as ArcGISDesignType + } + is MapboxViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as MapboxDesignType + } + else -> throw IllegalArgumentException("Not implemented yet") + } + } - key(sdkKey) { - // ★ SDK切替でサブツリーごと作り直し - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { if (enabled) expanded = !expanded }, - ) { - TextField( - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled), - value = selectedLabel, - onValueChange = {}, - readOnly = true, - enabled = enabled, - label = { Text("Map design") }, + key(state.id) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + TextField( + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = selectedLabel, + onValueChange = {}, + readOnly = true, + label = { Text("Map design") }, + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + mapDesignOptions.forEach { item -> + DropdownMenuItem( + text = { Text(item.label) }, + onClick = { + onClick(item) + }, ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - items.forEach { item -> - DropdownMenuItem( - text = { Text(item.label) }, - onClick = { - selectedLabel = item.label - expanded = false - mapViewState.value?.let { state -> - callChangeMapDesignType(state, item.design) - } - }, - ) - } - } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt index 495a8b02..06739d45 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt @@ -1,7 +1,5 @@ package com.mapconductor.example.pages.mapDesign -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.mapconductor.arcgis.ArcGISDesign import com.mapconductor.arcgis.ArcGISMapViewState @@ -9,18 +7,17 @@ import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapDesignType import com.mapconductor.core.map.MapViewState -import com.mapconductor.example.toast.ToastMessage import com.mapconductor.googlemaps.GoogleMapDesign import com.mapconductor.googlemaps.GoogleMapViewState import com.mapconductor.here.HereMapDesign -import com.mapconductor.here.HereMapViewState +import com.mapconductor.here.HereViewState import com.mapconductor.mapbox.MapboxMapDesign -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.mapbox.MapboxViewState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -data class MapDesignOptions( +data class MapDesignOption( val label: String, val design: MapDesignType<*>, ) @@ -28,17 +25,10 @@ data class MapDesignOptions( interface MapDesignPageViewModel { val initCameraPosition: MapCameraPosition val mapViewState: StateFlow?> - val messages: StateFlow> - val options: StateFlow> + val mapDesignOptions: StateFlow> fun onMapViewChanged(state: MapViewState<*>) - - fun onMapClick(clicked: GeoPoint) - - fun showToast(text: String) - - fun removeToast(toastMessage: ToastMessage) } class MapDesignPageViewModelImpl : @@ -57,155 +47,140 @@ class MapDesignPageViewModelImpl : paddings = null, ) - private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) - override val messages: StateFlow> = _messages.asStateFlow() - private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() - private val _options: MutableStateFlow> = MutableStateFlow(emptyList()) - override val options: StateFlow> = _options.asStateFlow() + private val _mapDesignOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + override val mapDesignOptions: StateFlow> = _mapDesignOptions.asStateFlow() override fun onMapViewChanged(state: MapViewState<*>) { this._mapViewState.value = state when (state) { is GoogleMapViewState -> { - _options.value = googleMapDesigns + _mapDesignOptions.value = googleMapDesigns } - is HereMapViewState -> { - _options.value = hereMapDesigns + is HereViewState -> { + _mapDesignOptions.value = hereMapDesigns } - is MapboxMapViewState -> { - _options.value = mapboxMapDesigns + is MapboxViewState -> { + _mapDesignOptions.value = mapboxMapDesigns } is ArcGISMapViewState -> { - _options.value = arcGISMapDesigns + _mapDesignOptions.value = arcGISMapDesigns } } } - override fun onMapClick(clicked: GeoPoint) { - showToast("Map clicked at: ${clicked.toUrlValue()}") - } - - 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 } - } - private val googleMapDesigns = listOf( - MapDesignOptions(label = "Normal", design = GoogleMapDesign.Normal), - MapDesignOptions(label = "Satellite", design = GoogleMapDesign.Satellite), - MapDesignOptions(label = "Hybrid", design = GoogleMapDesign.Hybrid), - MapDesignOptions(label = "Terrain", design = GoogleMapDesign.Terrain), - MapDesignOptions(label = "None", design = GoogleMapDesign.None), + MapDesignOption(label = "Normal", design = GoogleMapDesign.Normal), + MapDesignOption(label = "Satellite", design = GoogleMapDesign.Satellite), + MapDesignOption(label = "Hybrid", design = GoogleMapDesign.Hybrid), + MapDesignOption(label = "Terrain", design = GoogleMapDesign.Terrain), + MapDesignOption(label = "None", design = GoogleMapDesign.None), ) private val hereMapDesigns = listOf( - MapDesignOptions(label = "NormalDay", design = HereMapDesign.NormalDay), - MapDesignOptions(label = "NormalNigh", design = HereMapDesign.NormalNigh), - MapDesignOptions(label = "Satellite", design = HereMapDesign.Satellite), - MapDesignOptions(label = "HybridDay", design = HereMapDesign.HybridDay), - MapDesignOptions(label = "HybridNight", design = HereMapDesign.HybridNight), - MapDesignOptions(label = "LiteDay", design = HereMapDesign.LiteDay), - MapDesignOptions(label = "LiteNight", design = HereMapDesign.LiteNight), - MapDesignOptions(label = "LiteHybridDay", design = HereMapDesign.LiteHybridDay), - MapDesignOptions(label = "LiteHybridNight", design = HereMapDesign.LiteHybridNight), - MapDesignOptions(label = "LogisticsDay", design = HereMapDesign.LogisticsDay), - MapDesignOptions(label = "LogisticsNight", design = HereMapDesign.LogisticsNight), - MapDesignOptions(label = "LogisticsHybridDay", design = HereMapDesign.LogisticsHybridDay), - MapDesignOptions(label = "RoadNetworkDay", design = HereMapDesign.RoadNetworkDay), - MapDesignOptions(label = "RoadNetworkNight", design = HereMapDesign.RoadNetworkNight), + MapDesignOption(label = "NormalDay", design = HereMapDesign.NormalDay), + MapDesignOption(label = "NormalNigh", design = HereMapDesign.NormalNigh), + MapDesignOption(label = "Satellite", design = HereMapDesign.Satellite), + MapDesignOption(label = "HybridDay", design = HereMapDesign.HybridDay), + MapDesignOption(label = "HybridNight", design = HereMapDesign.HybridNight), + MapDesignOption(label = "LiteDay", design = HereMapDesign.LiteDay), + MapDesignOption(label = "LiteNight", design = HereMapDesign.LiteNight), + MapDesignOption(label = "LiteHybridDay", design = HereMapDesign.LiteHybridDay), + MapDesignOption(label = "LiteHybridNight", design = HereMapDesign.LiteHybridNight), + MapDesignOption(label = "LogisticsDay", design = HereMapDesign.LogisticsDay), + MapDesignOption(label = "LogisticsNight", design = HereMapDesign.LogisticsNight), + MapDesignOption(label = "LogisticsHybridDay", design = HereMapDesign.LogisticsHybridDay), + MapDesignOption(label = "RoadNetworkDay", design = HereMapDesign.RoadNetworkDay), + MapDesignOption(label = "RoadNetworkNight", design = HereMapDesign.RoadNetworkNight), ) private val mapboxMapDesigns = listOf( - MapDesignOptions(label = "Standard", design = MapboxMapDesign.Standard), - MapDesignOptions(label = "StandardSatellite", design = MapboxMapDesign.StandardSatellite), - MapDesignOptions(label = "Streets", design = MapboxMapDesign.Streets), - MapDesignOptions(label = "Outdoors", design = MapboxMapDesign.Outdoors), - MapDesignOptions(label = "Light", design = MapboxMapDesign.Light), - MapDesignOptions(label = "Dark", design = MapboxMapDesign.Dark), - MapDesignOptions(label = "Satellite", design = MapboxMapDesign.Satellite), - MapDesignOptions(label = "SatelliteStreets", design = MapboxMapDesign.SatelliteStreets), - MapDesignOptions(label = "NavigationDay", design = MapboxMapDesign.NavigationDay), - MapDesignOptions(label = "NavigationNight", design = MapboxMapDesign.NavigationNight), + MapDesignOption(label = "Standard", design = MapboxMapDesign.Standard), + MapDesignOption(label = "StandardSatellite", design = MapboxMapDesign.StandardSatellite), + MapDesignOption(label = "Streets", design = MapboxMapDesign.Streets), + MapDesignOption(label = "Outdoors", design = MapboxMapDesign.Outdoors), + MapDesignOption(label = "Light", design = MapboxMapDesign.Light), + MapDesignOption(label = "Dark", design = MapboxMapDesign.Dark), + MapDesignOption(label = "Satellite", design = MapboxMapDesign.Satellite), + MapDesignOption(label = "SatelliteStreets", design = MapboxMapDesign.SatelliteStreets), + MapDesignOption(label = "NavigationDay", design = MapboxMapDesign.NavigationDay), + MapDesignOption(label = "NavigationNight", design = MapboxMapDesign.NavigationNight), ) private val arcGISMapDesigns = listOf( - MapDesignOptions(label = "Streets", design = ArcGISDesign.Companion.Streets), - MapDesignOptions(label = "Imagery", design = ArcGISDesign.Companion.Imagery), - MapDesignOptions(label = "ImageryStandard", design = ArcGISDesign.Companion.ImageryStandard), - MapDesignOptions(label = "ImageryLabels", design = ArcGISDesign.Companion.ImageryLabels), - MapDesignOptions(label = "LightGray", design = ArcGISDesign.Companion.LightGray), - MapDesignOptions(label = "LightGrayBase", design = ArcGISDesign.Companion.LightGrayBase), - MapDesignOptions(label = "LightGrayLabels", design = ArcGISDesign.Companion.LightGrayLabels), - MapDesignOptions(label = "DarkGray", design = ArcGISDesign.Companion.DarkGray), - MapDesignOptions(label = "DarkGrayBase", design = ArcGISDesign.Companion.DarkGrayBase), - MapDesignOptions(label = "DarkGrayLabels", design = ArcGISDesign.Companion.DarkGrayLabels), - MapDesignOptions(label = "Navigation", design = ArcGISDesign.Companion.Navigation), - MapDesignOptions(label = "NavigationNight", design = ArcGISDesign.Companion.NavigationNight), - MapDesignOptions(label = "StreetsNight", design = ArcGISDesign.Companion.StreetsNight), - MapDesignOptions(label = "StreetsRelief", design = ArcGISDesign.Companion.StreetsRelief), - MapDesignOptions(label = "Topographic", design = ArcGISDesign.Companion.Topographic), - MapDesignOptions(label = "Oceans", design = ArcGISDesign.Companion.Oceans), - MapDesignOptions(label = "OceansBase", design = ArcGISDesign.Companion.OceansBase), - MapDesignOptions(label = "OceansLabels", design = ArcGISDesign.Companion.OceansLabels), - MapDesignOptions(label = "Terrain", design = ArcGISDesign.Companion.Terrain), - MapDesignOptions(label = "TerrainBase", design = ArcGISDesign.Companion.TerrainBase), - MapDesignOptions(label = "TerrainDetail", design = ArcGISDesign.Companion.TerrainDetail), - MapDesignOptions(label = "Community", design = ArcGISDesign.Companion.Community), - MapDesignOptions(label = "ChartedTerritory", design = ArcGISDesign.Companion.ChartedTerritory), - MapDesignOptions(label = "ColoredPencil", design = ArcGISDesign.Companion.ColoredPencil), - MapDesignOptions(label = "Nova", design = ArcGISDesign.Companion.Nova), - MapDesignOptions(label = "ModernAntique", design = ArcGISDesign.Companion.ModernAntique), - MapDesignOptions(label = "Midcentury", design = ArcGISDesign.Companion.Midcentury), - MapDesignOptions(label = "Newspaper", design = ArcGISDesign.Companion.Newspaper), - MapDesignOptions(label = "HillshadeLight", design = ArcGISDesign.Companion.HillshadeLight), - MapDesignOptions(label = "HillshadeDark", design = ArcGISDesign.Companion.HillshadeDark), - MapDesignOptions(label = "StreetsReliefBase", design = ArcGISDesign.Companion.StreetsReliefBase), - MapDesignOptions(label = "TopographicBase", design = ArcGISDesign.Companion.TopographicBase), - MapDesignOptions(label = "ChartedTerritoryBase", design = ArcGISDesign.Companion.ChartedTerritoryBase), - MapDesignOptions(label = "ModernAntiqueBase", design = ArcGISDesign.Companion.ModernAntiqueBase), - MapDesignOptions(label = "HumanGeography", design = ArcGISDesign.Companion.HumanGeography), - MapDesignOptions(label = "HumanGeographyBase", design = ArcGISDesign.Companion.HumanGeographyBase), - MapDesignOptions(label = "HumanGeographyDetail", design = ArcGISDesign.Companion.HumanGeographyDetail), - MapDesignOptions(label = "HumanGeographyLabels", design = ArcGISDesign.Companion.HumanGeographyLabels), - MapDesignOptions(label = "HumanGeographyDark", design = ArcGISDesign.Companion.HumanGeographyDark), - MapDesignOptions(label = "HumanGeographyDarkBase", design = ArcGISDesign.Companion.HumanGeographyDarkBase), - MapDesignOptions( + MapDesignOption(label = "Streets", design = ArcGISDesign.Companion.Streets), + MapDesignOption(label = "Imagery", design = ArcGISDesign.Companion.Imagery), + MapDesignOption(label = "ImageryStandard", design = ArcGISDesign.Companion.ImageryStandard), + MapDesignOption(label = "ImageryLabels", design = ArcGISDesign.Companion.ImageryLabels), + MapDesignOption(label = "LightGray", design = ArcGISDesign.Companion.LightGray), + MapDesignOption(label = "LightGrayBase", design = ArcGISDesign.Companion.LightGrayBase), + MapDesignOption(label = "LightGrayLabels", design = ArcGISDesign.Companion.LightGrayLabels), + MapDesignOption(label = "DarkGray", design = ArcGISDesign.Companion.DarkGray), + MapDesignOption(label = "DarkGrayBase", design = ArcGISDesign.Companion.DarkGrayBase), + MapDesignOption(label = "DarkGrayLabels", design = ArcGISDesign.Companion.DarkGrayLabels), + MapDesignOption(label = "Navigation", design = ArcGISDesign.Companion.Navigation), + MapDesignOption(label = "NavigationNight", design = ArcGISDesign.Companion.NavigationNight), + MapDesignOption(label = "StreetsNight", design = ArcGISDesign.Companion.StreetsNight), + MapDesignOption(label = "StreetsRelief", design = ArcGISDesign.Companion.StreetsRelief), + MapDesignOption(label = "Topographic", design = ArcGISDesign.Companion.Topographic), + MapDesignOption(label = "Oceans", design = ArcGISDesign.Companion.Oceans), + MapDesignOption(label = "OceansBase", design = ArcGISDesign.Companion.OceansBase), + MapDesignOption(label = "OceansLabels", design = ArcGISDesign.Companion.OceansLabels), + MapDesignOption(label = "Terrain", design = ArcGISDesign.Companion.Terrain), + MapDesignOption(label = "TerrainBase", design = ArcGISDesign.Companion.TerrainBase), + MapDesignOption(label = "TerrainDetail", design = ArcGISDesign.Companion.TerrainDetail), + MapDesignOption(label = "Community", design = ArcGISDesign.Companion.Community), + MapDesignOption(label = "ChartedTerritory", design = ArcGISDesign.Companion.ChartedTerritory), + MapDesignOption(label = "ColoredPencil", design = ArcGISDesign.Companion.ColoredPencil), + MapDesignOption(label = "Nova", design = ArcGISDesign.Companion.Nova), + MapDesignOption(label = "ModernAntique", design = ArcGISDesign.Companion.ModernAntique), + MapDesignOption(label = "Midcentury", design = ArcGISDesign.Companion.Midcentury), + MapDesignOption(label = "Newspaper", design = ArcGISDesign.Companion.Newspaper), + MapDesignOption(label = "HillshadeLight", design = ArcGISDesign.Companion.HillshadeLight), + MapDesignOption(label = "HillshadeDark", design = ArcGISDesign.Companion.HillshadeDark), + MapDesignOption(label = "StreetsReliefBase", design = ArcGISDesign.Companion.StreetsReliefBase), + MapDesignOption(label = "TopographicBase", design = ArcGISDesign.Companion.TopographicBase), + MapDesignOption(label = "ChartedTerritoryBase", design = ArcGISDesign.Companion.ChartedTerritoryBase), + MapDesignOption(label = "ModernAntiqueBase", design = ArcGISDesign.Companion.ModernAntiqueBase), + MapDesignOption(label = "HumanGeography", design = ArcGISDesign.Companion.HumanGeography), + MapDesignOption(label = "HumanGeographyBase", design = ArcGISDesign.Companion.HumanGeographyBase), + MapDesignOption(label = "HumanGeographyDetail", design = ArcGISDesign.Companion.HumanGeographyDetail), + MapDesignOption(label = "HumanGeographyLabels", design = ArcGISDesign.Companion.HumanGeographyLabels), + MapDesignOption(label = "HumanGeographyDark", design = ArcGISDesign.Companion.HumanGeographyDark), + MapDesignOption(label = "HumanGeographyDarkBase", design = ArcGISDesign.Companion.HumanGeographyDarkBase), + MapDesignOption( label = "HumanGeographyDarkDetail", design = ArcGISDesign.Companion.HumanGeographyDarkDetail, ), - MapDesignOptions( + MapDesignOption( label = "HumanGeographyDarkLabels", design = ArcGISDesign.Companion.HumanGeographyDarkLabels, ), - MapDesignOptions(label = "Outdoor", design = ArcGISDesign.Companion.Outdoor), - MapDesignOptions(label = "OsmStandard", design = ArcGISDesign.Companion.OsmStandard), - MapDesignOptions(label = "OsmStandardRelief", design = ArcGISDesign.Companion.OsmStandardRelief), - MapDesignOptions(label = "OsmStandardReliefBase", design = ArcGISDesign.Companion.OsmStandardReliefBase), - MapDesignOptions(label = "OsmStreets", design = ArcGISDesign.Companion.OsmStreets), - MapDesignOptions(label = "OsmStreetsRelief", design = ArcGISDesign.Companion.OsmStreetsRelief), - MapDesignOptions(label = "OsmLightGray", design = ArcGISDesign.Companion.OsmLightGray), - MapDesignOptions(label = "OsmLightGrayBase", design = ArcGISDesign.Companion.OsmLightGrayBase), - MapDesignOptions(label = "OsmLightGrayLabels", design = ArcGISDesign.Companion.OsmLightGrayLabels), - MapDesignOptions(label = "OsmDarkGray", design = ArcGISDesign.Companion.OsmDarkGray), - MapDesignOptions(label = "OsmDarkGrayBase", design = ArcGISDesign.Companion.OsmDarkGrayBase), - MapDesignOptions(label = "OsmDarkGrayLabels", design = ArcGISDesign.Companion.OsmDarkGrayLabels), - MapDesignOptions(label = "OsmStreetsReliefBase", design = ArcGISDesign.Companion.OsmStreetsReliefBase), - MapDesignOptions(label = "OsmBlueprint", design = ArcGISDesign.Companion.OsmBlueprint), - MapDesignOptions(label = "OsmHybrid", design = ArcGISDesign.Companion.OsmHybrid), - MapDesignOptions(label = "OsmHybridDetail", design = ArcGISDesign.Companion.OsmHybridDetail), - MapDesignOptions(label = "OsmNavigation", design = ArcGISDesign.Companion.OsmNavigation), - MapDesignOptions(label = "OsmNavigationDark", design = ArcGISDesign.Companion.OsmNavigationDark), + MapDesignOption(label = "Outdoor", design = ArcGISDesign.Companion.Outdoor), + MapDesignOption(label = "OsmStandard", design = ArcGISDesign.Companion.OsmStandard), + MapDesignOption(label = "OsmStandardRelief", design = ArcGISDesign.Companion.OsmStandardRelief), + MapDesignOption(label = "OsmStandardReliefBase", design = ArcGISDesign.Companion.OsmStandardReliefBase), + MapDesignOption(label = "OsmStreets", design = ArcGISDesign.Companion.OsmStreets), + MapDesignOption(label = "OsmStreetsRelief", design = ArcGISDesign.Companion.OsmStreetsRelief), + MapDesignOption(label = "OsmLightGray", design = ArcGISDesign.Companion.OsmLightGray), + MapDesignOption(label = "OsmLightGrayBase", design = ArcGISDesign.Companion.OsmLightGrayBase), + MapDesignOption(label = "OsmLightGrayLabels", design = ArcGISDesign.Companion.OsmLightGrayLabels), + MapDesignOption(label = "OsmDarkGray", design = ArcGISDesign.Companion.OsmDarkGray), + MapDesignOption(label = "OsmDarkGrayBase", design = ArcGISDesign.Companion.OsmDarkGrayBase), + MapDesignOption(label = "OsmDarkGrayLabels", design = ArcGISDesign.Companion.OsmDarkGrayLabels), + MapDesignOption(label = "OsmStreetsReliefBase", design = ArcGISDesign.Companion.OsmStreetsReliefBase), + MapDesignOption(label = "OsmBlueprint", design = ArcGISDesign.Companion.OsmBlueprint), + MapDesignOption(label = "OsmHybrid", design = ArcGISDesign.Companion.OsmHybrid), + MapDesignOption(label = "OsmHybridDetail", design = ArcGISDesign.Companion.OsmHybridDetail), + MapDesignOption(label = "OsmNavigation", design = ArcGISDesign.Companion.OsmNavigation), + MapDesignOption(label = "OsmNavigationDark", design = ArcGISDesign.Companion.OsmNavigationDark), ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt new file mode 100644 index 00000000..68568071 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt @@ -0,0 +1,292 @@ +package com.mapconductor.example.pages.marker + +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.Text +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.info.InfoBubble +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.DrawableDefaultIcon +import com.mapconductor.core.marker.ImageIcon +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.R +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable + +@Composable +fun MarkerBasicMapComponent( + mapViewState: MapViewState<*>, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + var selected by remember { mutableStateOf(null) } + val darkTheme: Boolean = isSystemInDarkTheme() + val bubbleColor = if (darkTheme) Color.Black else Color.White + + MapViewContainer( + state = mapViewState, + modifier = modifier, + onMarkerClick = { selected = it }, + onMapClick = { selected = null }, + ) { + Marker( + position = GeoPoint.fromLatLong(0.018, 0.004), + icon = + DefaultIcon( + scale = 0.7f, + label = "0.7", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 0.7f, + label = "0.7", + debug = true, + ), + """.trimIndent(), + ) + + Marker( + position = GeoPoint.fromLatLong(0.018, 0.006), + icon = + DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + """.trimIndent(), + ) + Marker( + position = GeoPoint.fromLatLong(0.018, 0.009), + icon = + DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + """.trimIndent(), + ) + + Marker( + position = GeoPoint.fromLatLong(0.018, 0.013), + icon = + DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + """.trimIndent(), + ) + + Marker( + position = GeoPoint.fromLatLong(0.014, 0.004), + extra = "DefaultIcon()", + ) + Marker( + position = GeoPoint.fromLatLong(0.014, 0.008), + icon = + DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ), + extra = + """ + DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ) + """.trimIndent(), + ) + Marker( + position = GeoPoint.fromLatLong(0.014, 0.012), + icon = + DefaultIcon( + fillColor = + Color( + red = 0x2E, + green = 0xF5, + blue = 0x27, + ), + strokeColor = + Color( + red = 0xFC, + green = 0x22, + blue = 0x5C, + ), + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ), + extra = + """ + DefaultIcon( + fillColor = Color( + red = 0x2E, + green = 0xF5, + blue = 0x27, + ), + strokeColor = Color( + red = 0xFC, + green = 0x22, + blue = 0x5C, + ), + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ) + """.trimIndent(), + ) + + AppCompatResources.getDrawable(context, R.drawable.human)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.004), + icon = + DrawableDefaultIcon( + backgroundDrawable = icon, + ), + extra = + """ + DrawableDefaultIcon( + backgroundDrawable = icon, + ) + """.trimIndent(), + ) + } + + AppCompatResources.getDrawable(context, R.drawable.ic_launcher_foreground)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.006), + icon = + DrawableDefaultIcon( + backgroundDrawable = icon, + strokeColor = Color.Black, + scale = 1.5f, + ), + extra = + """ + DrawableDefaultIcon( + backgroundDrawable = icon, + strokeColor = Color.Black, + scale = 1.5f, + ) + """.trimIndent(), + ) + } + + AppCompatResources.getDrawable(context, R.drawable.wmo_00_clear)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.009), + icon = + ImageIcon( + drawable = icon, + debug = true, + anchor = Offset(0.5f, 1.0f), + ), + extra = + """ + ImageIcon( + drawable = icon, + debug = true, + anchor = Offset(0.5f, 1.0f), + ) + """.trimIndent(), + ) + } + + createMarkerWithLabelIcon(context, "Label")?.let { + Marker( + position = GeoPoint.fromLatLong(0.01, 0.012), + icon = + ImageIcon( + drawable = it, + anchor = Offset(0.5f, 1.0f), + ), + extra = + """ + ImageIcon( + drawable = createMarkerWithLabelIcon(label), + anchor = Offset(0.5f, 1.0f), + ) + """.trimIndent(), + ) + } + (selected?.extra as? String)?.let { snippet -> + InfoBubble( + marker = selected!!, + bubbleColor = bubbleColor, + ) { + Text( + text = snippet, + textAlign = TextAlign.Left, + ) + } + } + } +} + +fun createMarkerWithLabelIcon( + context: Context, + label: String, +): BitmapDrawable? { + val drawable = AppCompatResources.getDrawable(context, R.drawable.marker_with_label) ?: return null + + val iconBitmap = drawable.toBitmap() + Canvas(iconBitmap).apply { + val textPaint = + Paint().apply { + color = Color.White.toArgb() + this.textSize = ResourceProvider.dpToPx(40f).toFloat() + textAlign = Paint.Align.CENTER + isAntiAlias = true + isSubpixelText = true + } + + drawText(label, ResourceProvider.dpToPx(77f).toFloat(), ResourceProvider.dpToPx(120f).toFloat(), textPaint) + } + + return iconBitmap.toDrawable(context.resources) +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt new file mode 100644 index 00000000..174a6abd --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt @@ -0,0 +1,42 @@ +package com.mapconductor.example.pages.marker + +import androidx.compose.foundation.layout.fillMaxSize +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 com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold + +@Composable +fun MarkerBasicPage(onToggleSidebar: () -> Unit = {}) { + val initCameraPosition = + remember { + MapCameraPosition( + position = GeoPoint(0.014, 0.008), + zoom = 15.0, + ) + } + + var mapViewState by remember { mutableStateOf?>(null) } + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = { newMapViewState -> + mapViewState = newMapViewState + }, + ) { paddingValues -> + mapViewState?.let { + MarkerBasicMapComponent( + mapViewState = it, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt index bae34c77..a71f1574 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt @@ -3,27 +3,34 @@ package com.mapconductor.example.pages.polygon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.Polygon +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.example.MapViewContainer @Composable fun PolygonMapComponent( mapViewState: MapViewState<*>?, + polygonVertexMarkers: List, + polygonState: PolygonState, modifier: Modifier = Modifier, - onMarkerDrag: OnMarkerEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler, ) { -// mapViewState?.let { it -> -// MapViewContainer( -// modifier = modifier, -// state = it, -// onMarkerDrag = onMarkerDrag, -// ) { -// // Polyline -// Polyline(viewModel.polylineState) -// -// // Waypoint markers -// viewModel.wayPointMarkers.forEach { marker -> -// Marker(marker) -// } -// } -// } + mapViewState?.let { it -> + MapViewContainer( + modifier = modifier, + state = it, + onMarkerDrag = onMarkerDrag, + ) { + // Polygon + Polygon(polygonState) + + // Vertex markers (draggable) + polygonVertexMarkers.forEach { marker -> + Marker(marker) + } + } + } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt new file mode 100644 index 00000000..4d6adc99 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -0,0 +1,87 @@ +package com.mapconductor.example.pages.polygon + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard +import com.mapconductor.example.ui.PolygonCapableMapViewItems + +@Composable +fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { PolygonMapPageViewModelImpl() } + + DemoMapPageScaffold( + initSelect = 1, + menuItems = PolygonCapableMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + // Map Component + PolygonMapComponent( + mapViewState = mapViewState.value, + polygonState = viewModel.polygonState, + polygonVertexMarkers = viewModel.polygonVertexMarkers, + modifier = + Modifier.padding( + bottom = paddingValues.calculateBottomPadding(), + ), + onMarkerDrag = viewModel::onMarkerDrag, + ) + MessageCard( + title = "Polygon Example", + maxHeight = 250.dp, + 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, + ), + ) { + // Fill Opacity Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") + Slider( + value = viewModel.fillOpacity, + onValueChange = { viewModel.fillOpacity = it }, + valueRange = 0f..1f, + colors = + SliderDefaults.colors(), + ) + } + + // Stroke Width Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") + Slider( + value = viewModel.strokeWidth, + onValueChange = { viewModel.strokeWidth = it }, + valueRange = 0f..10f, + colors = + SliderDefaults.colors(), + ) + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt new file mode 100644 index 00000000..ab084aff --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -0,0 +1,104 @@ +package com.mapconductor.example.pages.polygon + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +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.polygon.PolygonState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface PolygonMapPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + + val polygonVertexMarkers: List + var fillOpacity: Float + var strokeWidth: Float + val polygonState: PolygonState + + fun onMapViewChanged(state: MapViewState<*>) + + fun onMarkerDrag(dragged: MarkerState) +} + +class PolygonMapPageViewModelImpl : + ViewModel(), + PolygonMapPageViewModel { + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + // Polygon vertices + private val polygonVertices = + mutableStateListOf( + GeoPoint(41.79883, 140.75675), + GeoPoint(41.799240000000005, 140.75875000000002), + GeoPoint(41.797650000000004, 140.75905), + GeoPoint(41.79637, 140.76018000000002), + GeoPoint(41.79567, 140.75845), + GeoPoint(41.794470000000004, 140.75714000000002), + GeoPoint(41.795010000000005, 140.75611), + GeoPoint(41.79477000000001, 140.75484), + GeoPoint(41.79576, 140.75475), + GeoPoint(41.796150000000004, 140.75364000000002), + GeoPoint(41.79744, 140.75454000000002), + GeoPoint(41.79909000000001, 140.75465), + ) + + override val initCameraPosition = + MapCameraPosition( + position = GeoPoint(41.796855, 140.756910), + zoom = 16.0, + ) + + override var fillOpacity by mutableStateOf(0.3f) + override var strokeWidth by mutableStateOf(3.0f) + + override val polygonVertexMarkers: List = + polygonVertices.mapIndexed { index, point -> + MarkerState( + position = point, + icon = + DefaultIcon( + scale = 0.7f, + fillColor = Color.Yellow, + strokeColor = Color.Black, + ), + id = "vertex_$index", + draggable = true, + extra = index, + ) + } + + override val polygonState: PolygonState + get() = + PolygonState( + points = polygonVertices, + id = "example_polygon", + strokeColor = Color.Red, + strokeWidth = strokeWidth.dp, + fillColor = Color.Blue.copy(alpha = fillOpacity), + geodesic = false, + ) + + override fun onMapViewChanged(state: MapViewState<*>) { + _mapViewState.value = state + } + + override fun onMarkerDrag(dragged: MarkerState) { + (dragged.extra as? Int)?.let { index -> + if (index >= 0 && index < polygonVertices.size) { + polygonVertices[index] = GeoPoint.from(dragged.position) + } + } + } +} 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 index 538ae3f5..d70b0d78 100644 --- 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 @@ -6,7 +6,6 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.Marker import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.Polyline import com.mapconductor.core.polyline.PolylineState import com.mapconductor.example.MapViewContainer @@ -17,15 +16,15 @@ fun PolylineMapComponent( polylineState: PolylineState, wayPointMarkers: List, modifier: Modifier = Modifier, - onPolylineClick: OnPolylineEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, ) { mapViewState?.let { it -> MapViewContainer( modifier = modifier, state = it, - onPolylineClick = onPolylineClick, + onMarkerDragStart = onMarkerDrag, onMarkerDrag = onMarkerDrag, + onMarkerDragEnd = onMarkerDrag, ) { // Polyline Polyline(polylineState) 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 index de95aaa9..d5ee3340 100644 --- 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 @@ -2,15 +2,16 @@ package com.mapconductor.example.pages.polyline import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable -fun PolylineMapPage( - viewModel: PolylinePageViewModel = PolylinePageViewModelImpl(), - onToggleSidebar: () -> Unit = {}, -) { +fun PolylineMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { PolylinePageViewModelImpl() } DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + initSelect = 1, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> @@ -20,32 +21,7 @@ fun PolylineMapPage( polylineState = viewModel.polylineState, wayPointMarkers = viewModel.wayPointMarkers, mapViewState = mapViewState.value, - 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 index 652209b9..04722a0f 100644 --- 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 @@ -1,8 +1,6 @@ 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 @@ -25,8 +23,6 @@ interface PolylinePageViewModel { fun onMapViewChanged(state: MapViewState<*>) - fun onPolylineClick(state: PolylineState) - fun onMarkerDrag(dragged: MarkerState) } @@ -56,51 +52,43 @@ class PolylinePageViewModelImpl : GeoPoint.fromLatLong(21.382314, -157.933097), // Back to center ) - private val _wayPointMarkers: MutableState> = - mutableStateOf( - polylinePoints.mapIndexed { index, point -> - val markerColor = - when { - index == 0 -> Color.Green - index == polylinePoints.size - 1 -> Color.Green - else -> Color.Yellow - } - val label = - when { - index == 0 -> "S" - index == polylinePoints.size - 1 -> "E" - else -> "$index" - } - MarkerState( - id = "waypoint_$index", - position = point, - icon = - DefaultIcon( - fillColor = markerColor, - strokeColor = Color.Black, - label = label, - ), - draggable = true, - ) - }, - ) - - override val wayPointMarkers: List - get() = _wayPointMarkers.value + override val wayPointMarkers: List = + polylinePoints.mapIndexed { index, point -> + val markerColor = + when { + index == 0 -> Color.Green + index == polylinePoints.size - 1 -> Color.Green + else -> Color.Yellow + } + val label = + when { + index == 0 -> "S" + index == polylinePoints.size - 1 -> "E" + else -> "$index" + } + MarkerState( + id = "waypoint_$index", + position = point, + icon = + DefaultIcon( + fillColor = markerColor, + strokeColor = Color.Black, + label = label, + ), + draggable = true, + extra = index, + ) + } - private val _polylineState: MutableState = - mutableStateOf( + override val polylineState: PolylineState + get() = 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() @@ -109,17 +97,12 @@ class PolylinePageViewModelImpl : this._mapViewState.value = state } - override fun onPolylineClick(state: PolylineState) { - _polylineState.value.strokeColor = Color.Magenta - } - 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 + (dragged.extra as? Int)?.let { index -> + if (index >= 0 && index < polylinePoints.size) { + polylinePoints[index] = GeoPoint.from(dragged.position) + } + } } override fun onCleared() { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt index 95067474..10e84479 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt @@ -6,13 +6,22 @@ import androidx.compose.ui.unit.sp import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState -import android.os.Bundle +import java.io.Serializable /** * This example uses publicly available business addresses (e.g., Starbucks) and geocodes them * using the U.S. Census Bureau Geocoding API. * No personally identifiable information (PII) is used or inferred. */ +data class StoreInfo( + val name: String, + val address: String, + val instore: Boolean, + val driveThrough: Boolean, + val onlyReserved: Boolean, + val store: String, +) : Serializable + val StarbucksHI_list = listOf( MarkerState( @@ -22,14 +31,14 @@ val StarbucksHI_list = longitude = -158.062544988096, ), extra = - Bundle().apply { - putString("name", "Pupukea (North Shore)") - putString("address", "59-720 Kamehameha Highway, Haleiwa, HI 96712") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, + StoreInfo( + name = "Pupukea (North Shore)", + address = "59-720 Kamehameha Highway, Haleiwa, HI 96712", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), icon = DefaultIcon( label = "店", @@ -47,14 +56,14 @@ val StarbucksHI_list = longitude = -157.922371535818, ), extra = - Bundle().apply { - putString("name", "Honolulu Airport (HNL) – Main") - putString("address", "300 Rogers Blvd, Honolulu, HI 96820") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, + StoreInfo( + name = "Honolulu Airport (HNL) – Main", + address = "300 Rogers Blvd, Honolulu, HI 96820", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( position = @@ -63,14 +72,14 @@ val StarbucksHI_list = longitude = -157.930536387573, ), extra = - Bundle().apply { - putString("name", "Aiea Shopping Center") - putString("address", "99-115 Aiea Heights Drive #125, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Aiea Shopping Center", + address = "99-115 Aiea Heights Drive #125, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -79,21 +88,14 @@ val StarbucksHI_list = longitude = -157.944839558127, ), extra = - Bundle().apply { - putString("name", "Pearlridge Center") - putString("address", "98-125 Kaonohi Street, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, -// icon = -// MarkerIcon.Companion.Triangle( -// outsideColor = 0xFF008000.toInt(), -// strokeWidth = 2f, -// triangleHeight = 24f, -// triangleWidth = 24f, -// ), + StoreInfo( + name = "Pearlridge Center", + address = "98-125 Kaonohi Street, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -102,14 +104,14 @@ val StarbucksHI_list = longitude = -157.928412704343, ), extra = - Bundle().apply { - putString("name", "Stadium Marketplace") - putString("address", "4561 Salt Lake Boulevard, Aiea, HI 96818") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Stadium Marketplace", + address = "4561 Salt Lake Boulevard, Aiea, HI 96818", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -118,14 +120,14 @@ val StarbucksHI_list = longitude = -157.941897795274, ), extra = - Bundle().apply { - putString("name", "Pearlridge Mall") - putString("address", "98-1005 Moanalua Road, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "Pearlridge Mall", + address = "98-1005 Moanalua Road, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -134,14 +136,14 @@ val StarbucksHI_list = longitude = -155.067322812851, ), extra = - Bundle().apply { - putString("name", "Waiakea Center (Hilo)") - putString("address", "315-325 Makaala Street, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Waiakea Center (Hilo)", + address = "315-325 Makaala Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -150,14 +152,14 @@ val StarbucksHI_list = longitude = -155.06690203818, ), extra = - Bundle().apply { - putString("name", "Prince Kuhio Plaza (Hilo)") - putString("address", "111 East Puainako Street, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Prince Kuhio Plaza (Hilo)", + address = "111 East Puainako Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -166,14 +168,14 @@ val StarbucksHI_list = longitude = -155.082770375139, ), extra = - Bundle().apply { - putString("name", "Downtown Hilo (Kilauea Ave)") - putString("address", "438 Kilauea Ave, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Downtown Hilo (Kilauea Ave)", + address = "438 Kilauea Ave, Hilo, HI 96720", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -182,14 +184,14 @@ val StarbucksHI_list = longitude = -157.91581, ), extra = - Bundle().apply { - putString("name", "Airport Trade Center") - putString("address", "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Airport Trade Center", + address = "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -198,14 +200,14 @@ val StarbucksHI_list = longitude = -157.865194116049, ), extra = - Bundle().apply { - putString("name", "Aloha Tower") - putString("address", "1 Aloha Tower Drive, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "Aloha Tower", + address = "1 Aloha Tower Drive, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -214,14 +216,14 @@ val StarbucksHI_list = longitude = -157.8614898, ), extra = - Bundle().apply { - putString("name", "Bishop (Downtown)") - putString("address", "1000 Bishop Street #104, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "Bishop (Downtown)", + address = "1000 Bishop Street #104, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -230,14 +232,14 @@ val StarbucksHI_list = longitude = -157.860743724617, ), extra = - Bundle().apply { - putString("name", "Pickup – King & Alakea") - putString("address", "220 South King Street, Honolulu, HI 96813") - putBoolean("instore", false) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, + StoreInfo( + name = "Pickup – King & Alakea", + address = "220 South King Street, Honolulu, HI 96813", + instore = false, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", + ), ), MarkerState( position = @@ -246,14 +248,14 @@ val StarbucksHI_list = longitude = -157.83841421971, ), extra = - Bundle().apply { - putString("name", "Discovery Bay Center") - putString("address", "1778 Ala Moana Boulevard, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "Discovery Bay Center", + address = "1778 Ala Moana Boulevard, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -262,14 +264,14 @@ val StarbucksHI_list = longitude = -158.023228524098, ), extra = - Bundle().apply { - putString("name", "Ewa Beach – Laulani Village") - putString("address", "91-1401 Fort Weaver Road, Ewa Beach, HI 96706") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, + StoreInfo( + name = "Ewa Beach – Laulani Village", + address = "91-1401 Fort Weaver Road, Ewa Beach, HI 96706", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( position = @@ -278,14 +280,14 @@ val StarbucksHI_list = longitude = -157.828071689214, ), extra = - Bundle().apply { - putString("name", "DFS (Duty Free) Waikiki") - putString("address", "330 Royal Hawaiian Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "DFS (Duty Free) Waikiki", + address = "330 Royal Hawaiian Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -294,14 +296,14 @@ val StarbucksHI_list = longitude = -157.862582769768, ), extra = - Bundle().apply { - putString("name", "Financial Plaza (Downtown)") - putString("address", "130 Merchant Street #111, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, + StoreInfo( + name = "Financial Plaza (Downtown)", + address = "130 Merchant Street #111, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", + ), ), MarkerState( position = @@ -310,14 +312,14 @@ val StarbucksHI_list = longitude = -157.713041, ), extra = - Bundle().apply { - putString("name", "Hawaii Kai Town Center") - putString("address", "6700 Kalanianaole Highway, Honolulu, HI 96825") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Hawaii Kai Town Center", + address = "6700 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -326,14 +328,14 @@ val StarbucksHI_list = longitude = -157.849735879475, ), extra = - Bundle().apply { - putString("name", "Hokua (Ala Moana)") - putString("address", "1288 Ala Moana Blvd, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, + StoreInfo( + name = "Hokua (Ala Moana)", + address = "1288 Ala Moana Blvd, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( position = @@ -342,14 +344,14 @@ val StarbucksHI_list = longitude = -157.868748238078, ), extra = - Bundle().apply { - putString("name", "Kamehameha Shopping Center") - putString("address", "1620 North School Street, Honolulu, HI 96817") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Kamehameha Shopping Center", + address = "1620 North School Street, Honolulu, HI 96817", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -358,14 +360,14 @@ val StarbucksHI_list = longitude = -157.7875773, ), extra = - Bundle().apply { - putString("name", "Kahala Mall") - putString("address", "4211 Waialae Avenue, Honolulu, HI 96816") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Kahala Mall", + address = "4211 Waialae Avenue, Honolulu, HI 96816", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -374,14 +376,14 @@ val StarbucksHI_list = longitude = -157.813890137018, ), extra = - Bundle().apply { - putString("name", "Kapahulu Avenue") - putString("address", "625 Kapahulu Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + StoreInfo( + name = "Kapahulu Avenue", + address = "625 Kapahulu Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( position = @@ -390,14 +392,14 @@ val StarbucksHI_list = longitude = -157.704922547261, ), extra = - Bundle().apply { - putString("name", "Koko Marina Center") - putString("address", "7192 Kalanianaole Highway, Honolulu, HI 96825") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Koko Marina Center", + address = "7192 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -406,14 +408,14 @@ val StarbucksHI_list = longitude = -157.810260198584, ), extra = - Bundle().apply { - putString("name", "Manoa Valley") - putString("address", "2902 East Manoa Road, Honolulu, HI 96822") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, + StoreInfo( + name = "Manoa Valley", + address = "2902 East Manoa Road, Honolulu, HI 96822", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( position = @@ -422,493 +424,13 @@ val StarbucksHI_list = longitude = -157.843910788044, ), extra = - Bundle().apply { - putString("name", "Macy’s Ala Moana Center") - putString("address", "1450 Ala Moana Boulevard, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.341260775481, - longitude = -157.929507250967, - ), - extra = - Bundle().apply { - putString("name", "Moanalua Shopping Center") - putString("address", "930 Valkenburgh Street, Honolulu, HI 96818") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.279011840151, - longitude = -157.825557564916, - ), - extra = - Bundle().apply { - putString("name", "Ohana Waikiki West") - putString("address", "2330 Kuhio Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.2786604527, - longitude = -157.828371626919, - ), - extra = - Bundle().apply { - putString("name", "Waikiki Shopping Plaza") - putString("address", "2270 Kalakaua Avenue #1800, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.293774306726, - longitude = -157.85297798269, - ), - extra = - Bundle().apply { - putString("name", "Ward Entertainment Center") - putString("address", "310 Kamakee Street #6, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.406095, - longitude = -157.800761, - ), - extra = - Bundle().apply { - putString("name", "Windward City Shopping Center") - putString("address", "45-480 Kaneohe Bay Drive, Kaneohe, HI 96744") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.888659282451, - longitude = -156.477197459052, - ), - extra = - Bundle().apply { - putString("name", "Queen Kaahumanu Center") - putString("address", "275 West Kaahumanu Avenue #1200, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.881960703032, - longitude = -156.45511618549, - ), - extra = - Bundle().apply { - putString("name", "Maui Marketplace") - putString("address", "270 Dairy Road, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.393471214679, - longitude = -157.740438744365, - ), - extra = - Bundle().apply { - putString("name", "Kailua Village") - putString("address", "539 Kailua Road, Kailua, HI 96734") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.65018280057, - longitude = -155.987752998108, - ), - extra = - Bundle().apply { - putString("name", "Kona Coast Shopping Center") - putString("address", "74-5588 Palani Road, Kailua-Kona, HI 96740") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.020593379111, - longitude = -155.668585540658, - ), - extra = - Bundle().apply { - putString("name", "Parker Ranch Center") - putString("address", "67-1185 Mamalahoa Highway #D108, Kamuela, HI 96743") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.344027896932, - longitude = -158.11830127628, - ), - extra = - Bundle().apply { - putString("name", "Halekuai Center") - putString("address", "563 Farrington Highway #101, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.328579072139, - longitude = -158.086506230214, - ), - extra = - Bundle().apply { - putString("name", "Kapolei Parkway & Kamokila") - putString("address", "338 Kamokila Boulevard #108, Kapolei, HI 96797") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.734513943855, - longitude = -156.452970465534, - ), - extra = - Bundle().apply { - putString("name", "Kukui Mall") - putString("address", "1819 South Kihei Road, Kihei, HI 96738") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.750703062197, - longitude = -156.451408824978, - ), - extra = - Bundle().apply { - putString("name", "Piilani Village Shopping Center") - putString("address", "247 Piikea Avenue #106, Kihei, HI 96753") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.94001271876, - longitude = -155.856842731652, - ), - extra = - Bundle().apply { - putString("name", "Mauna Lani (Kohala Coast)") - putString("address", "68-1330 Mauna Lani Drive #H-101B, Kohala Coast, HI 96743") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.886244, - longitude = -156.684697, - ), - extra = - Bundle().apply { - putString("name", "Lahaina Cannery Mall") - putString("address", "1221 Honoapiilani Highway, Lahaina, HI 96761") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.877708110758, - longitude = -156.679031878844, - ), - extra = - Bundle().apply { - putString("name", "Lahaina (Front Street)") - putString("address", "845 Wainee Street, Lahaina, HI 96761") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.969553724378, - longitude = -159.388283368972, - ), - extra = - Bundle().apply { - putString("name", "Kukui Grove Center") - putString("address", "3-2600 Kaumualii Highway #A8, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.889156401906, - longitude = -156.449318101378, - ), - extra = - Bundle().apply { - putString("name", "Kahului Airport (OGG)") - putString("address", "1 Keolani Place, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.458431746129, - longitude = -158.015862355331, - ), - extra = - Bundle().apply { - putString("name", "Mililani Shopping Center") - putString("address", "95-221 Kipapa Drive, Mililani, HI 96789") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.838230357792, - longitude = -156.342698446307, - ), - extra = - Bundle().apply { - putString("name", "Pukalani Foodland Center") - putString("address", "55 Pukalani Street, Pukalani, HI 96768") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.378675, - longitude = -157.728499, - ), - extra = - Bundle().apply { - putString("name", "Enchanted Lake Center (Kailua)") - putString("address", "1020 Keolu Drive, Kailua, HI 96734") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.389003512015, - longitude = -158.033431400538, - ), - extra = - Bundle().apply { - putString("name", "Kunia Shopping Center") - putString("address", "94-673 Kupuohi Street, Waipahu, HI 96797") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.881002766882, - longitude = -159.457726341723, - ), - extra = - Bundle().apply { - putString("name", "Poipu Shopping Village") - putString("address", "2360 Kiahuna Plantation Drive #E70, Koloa, HI 96756") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.966834230955, - longitude = -159.381526209527, - ), - extra = - Bundle().apply { - putString("name", "Safeway Lihue") - putString("address", "4454 Nuhou Street, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.970935289068, - longitude = -159.375643172372, + StoreInfo( + name = "Macy’s Ala Moana Center", + address = "1450 Ala Moana Boulevard, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", ), - extra = - Bundle().apply { - putString("name", "Target Lihue (Kauai)") - putString("address", "4303 Nawiliwili Road, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 22.061786387888, - longitude = -159.320539848567, - ), - extra = - Bundle().apply { - putString("name", "Kauai Village SC (Kapaa)") - putString("address", "4-831 Kuhio Highway #208, Kapaa, HI 96746") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.32874860701, - longitude = -158.091318912219, - ), - extra = - Bundle().apply { - putString("name", "Target Kapolei") - putString("address", "4450 Kapolei Parkway, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.340541119127, - longitude = -158.124703887408, - ), - extra = - Bundle().apply { - putString("name", "Ko Olina Station") - putString("address", "92-1047 Olani Street, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.34213, - longitude = -157.95157, - ), - extra = - Bundle().apply { - putString("name", "Hickam AFB (Base Access)") - putString("address", "Bldg B-1250, Hickam AFB, Honolulu, HI 96853") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, ), ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt index 7bb05dd5..2a2c6db2 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt @@ -23,11 +23,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import android.os.Bundle @Composable fun StoreInfoView( - info: Bundle, + info: StoreInfo, onClick: () -> Unit = {}, ) { val darkTheme: Boolean = isSystemInDarkTheme() @@ -36,10 +35,10 @@ fun StoreInfoView( Column( modifier = Modifier.wrapContentSize(), ) { - val name = info.getString("name", "Starbucks") - val address = info.getString("address", "address") - val instore = info.getBoolean("instore", false) - val driveThrough = info.getBoolean("drive_through", false) + val name = info.name + val address = info.address + val instore = info.instore + val driveThrough = info.driveThrough Text(name, fontWeight = FontWeight.Bold, fontSize = 15.sp) Text(address, fontSize = 13.sp) 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 63ea54c5..5d47f108 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 @@ -19,7 +19,6 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer import com.mapconductor.example.R -import android.os.Bundle @Composable fun StoreMapComponent( @@ -61,8 +60,8 @@ fun StoreMapComponent( val markerList = remember { markers.map { state -> - (state.extra as Bundle).let { info -> - val storeIcon = info.getString("store") ?: "coffee_extra" + (state.extra as StoreInfo).let { info -> + val storeIcon = info.store state.copy( icon = icons[storeIcon], ) @@ -89,7 +88,7 @@ fun StoreMapComponent( marker = it, ) { StoreInfoView( - info = it.extra as Bundle, + info = it.extra as StoreInfo, onClick = { onDirectionButtonClick(it) }, 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 28c5deaa..f426256c 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 @@ -2,18 +2,18 @@ package com.mapconductor.example.pages.stores import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable -fun StoreMapPage( - viewModel: StoreMapPageViewModel = StoreMapPageViewModelImpl(), - onToggleSidebar: () -> Unit = {}, -) { +fun StoreMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { StoreMapPageViewModelImpl() } val context = LocalContext.current DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddings -> diff --git a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt index ce59dcbd..a20cac9a 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt @@ -27,41 +27,72 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.mapconductor.arcgis.ArcGISDesign +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.arcgis.rememberArcGISMapViewState import com.mapconductor.core.map.IMapCameraPosition import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateImpl import com.mapconductor.example.R import com.mapconductor.googlemaps.GoogleMapDesign +import com.mapconductor.googlemaps.GoogleMapViewStateImpl import com.mapconductor.googlemaps.rememberGoogleMapViewState import com.mapconductor.here.HereMapDesign +import com.mapconductor.here.HereViewStateImpl import com.mapconductor.here.rememberHereMapViewState import com.mapconductor.mapbox.MapboxMapDesign +import com.mapconductor.mapbox.MapboxViewStateImpl import com.mapconductor.mapbox.rememberMapboxMapViewState @Composable -fun DemoMapPageScaffold( - initCameraPosition: IMapCameraPosition, - initSelect: Int = 0, - onToggleSidebar: () -> Unit, - onMapViewStateChanged: (MapViewState<*>) -> Unit = {}, - content: @Composable BoxScope.(PaddingValues) -> Unit = {}, -) { - // ---------- Map States --------------- +fun GetGoogleMapViewItem(initCameraPosition: IMapCameraPosition): IconItem { val googleMapState = rememberGoogleMapViewState( mapDesign = GoogleMapDesign.Normal, cameraPosition = initCameraPosition, ) + return IconItem( + key = "googlemap", + label = "Google Map", + lightIconResId = R.drawable.google_maps_logo, + darkIconResId = R.drawable.google_maps_logo, + value = googleMapState, + ) +} + +@Composable +fun GetMapboxViewItem(initCameraPosition: IMapCameraPosition): IconItem { val mapboxMapState = rememberMapboxMapViewState( mapDesign = MapboxMapDesign.Standard, cameraPosition = initCameraPosition, ) + return IconItem( + key = "mapbox", + label = "Mapbox", + lightIconResId = R.drawable.mapbox_logo_black, + darkIconResId = R.drawable.mapbox_logo_white, + value = mapboxMapState, + ) +} + +@Composable +fun GetHereViewItem(initCameraPosition: IMapCameraPosition): IconItem { val hereMapState = rememberHereMapViewState( mapDesign = HereMapDesign.NormalDay, cameraPosition = initCameraPosition, ) + return IconItem( + key = "heremap", + label = "Here", + lightIconResId = R.drawable.here_logo_black, + darkIconResId = R.drawable.here_logo_white, + value = hereMapState, + ) +} + +@Composable +fun GetArcGISViewItem(initCameraPosition: IMapCameraPosition): IconItem { val elevationSources = listOf( "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", @@ -71,39 +102,49 @@ fun DemoMapPageScaffold( mapDesign = ArcGISDesign.Streets.withElevationSources(elevationSources), cameraPosition = initCameraPosition, ) + return IconItem( + key = "arcgis", + label = "ArcGIS", + lightIconResId = R.drawable.arcgis_logo_black, + darkIconResId = R.drawable.arcgis_logo_white, + value = arcGISMapState, + ) +} - val menuItems = - listOf( - IconItem( - key = "googlemap", - label = "Google Map", - lightIconResId = R.drawable.google_maps_logo, - darkIconResId = R.drawable.google_maps_logo, - value = googleMapState, - ), - IconItem( - key = "mapbox", - label = "Mapbox", - lightIconResId = R.drawable.mapbox_logo_black, - darkIconResId = R.drawable.mapbox_logo_white, - value = mapboxMapState, - ), - IconItem( - key = "heremap", - label = "Here", - lightIconResId = R.drawable.here_logo_black, - darkIconResId = R.drawable.here_logo_white, - value = hereMapState, - ), - IconItem( - key = "arcgis", - label = "ArcGIS", - lightIconResId = R.drawable.arcgis_logo_black, - darkIconResId = R.drawable.arcgis_logo_white, - value = arcGISMapState, - ), - ) +@Composable +fun DefaultMapViewItems(initCameraPosition: IMapCameraPosition): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + GetMapboxViewItem(initCameraPosition), + GetHereViewItem(initCameraPosition), + GetArcGISViewItem(initCameraPosition), + ) +@Composable +fun GroundImageCapableMapViewItems( + initCameraPosition: IMapCameraPosition, +): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + ) + +@Composable +fun PolygonCapableMapViewItems(initCameraPosition: IMapCameraPosition): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + GetMapboxViewItem(initCameraPosition), + GetHereViewItem(initCameraPosition), + GetArcGISViewItem(initCameraPosition), + ) + +@Composable +fun DemoMapPageScaffold( + menuItems: List>>, + initSelect: Int = 0, + onToggleSidebar: () -> Unit, + onMapViewStateChanged: (MapViewState<*>) -> Unit = {}, + content: @Composable (BoxScope.(PaddingValues) -> Unit) = {}, +) { var selectedIndex by rememberSaveable { mutableIntStateOf(initSelect) } LaunchedEffect(selectedIndex) { onMapViewStateChanged(menuItems.elementAt(selectedIndex).value) diff --git a/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt b/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt index 2320dc1d..188b6d70 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt @@ -1,21 +1,19 @@ package com.mapconductor.example.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.core.map.MapViewState -import com.mapconductor.googlemaps.GoogleMapViewState -import com.mapconductor.here.HereMapViewState -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.googlemaps.GoogleMapViewStateImpl +import com.mapconductor.here.HereViewStateImpl +import com.mapconductor.mapbox.MapboxViewStateImpl @Composable fun HeaderCard( - googleMapState: GoogleMapViewState, - mapboxMapState: MapboxMapViewState, - hereMapState: HereMapViewState, - arcGISMapState: ArcGISMapViewState, + googleMapState: GoogleMapViewStateImpl, + mapboxMapState: MapboxViewStateImpl, + hereMapState: HereViewStateImpl, + arcGISMapState: ArcGISMapViewStateImpl, modifier: Modifier = Modifier, onToggleSidebar: () -> Unit, onSdkSelectChange: (selected: MapViewState<*>) -> Unit, diff --git a/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt b/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt index 7dc6188a..9431e85e 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt @@ -190,12 +190,12 @@ private fun SidebarItemView( Row( verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = item.icon, - contentDescription = if (isExpanded) null else item.title, - modifier = Modifier.size(20.dp), - tint = contentColor, - ) +// Icon( +// imageVector = item.icon, +// contentDescription = if (isExpanded) null else item.title, +// modifier = Modifier.size(20.dp), +// tint = contentColor, +// ) AnimatedVisibility( visible = isExpanded, diff --git a/example-app/src/main/res/drawable/coffee_bean.png b/example-app/src/main/res/drawable/coffee_bean.png index 6789014c..459aac42 100644 Binary files a/example-app/src/main/res/drawable/coffee_bean.png and b/example-app/src/main/res/drawable/coffee_bean.png differ diff --git a/example-app/src/main/res/drawable/human.jpg b/example-app/src/main/res/drawable/human.jpg new file mode 100644 index 00000000..c2dd484c Binary files /dev/null and b/example-app/src/main/res/drawable/human.jpg differ diff --git a/example-app/src/main/res/drawable/marker_with_label.png b/example-app/src/main/res/drawable/marker_with_label.png new file mode 100644 index 00000000..4f899979 Binary files /dev/null and b/example-app/src/main/res/drawable/marker_with_label.png differ diff --git a/google-services.json b/google-services.json new file mode 100644 index 00000000..eca34aad --- /dev/null +++ b/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "35981014220", + "project_id": "mapconductor", + "storage_bucket": "mapconductor.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:35981014220:android:4265a83c3abbd61e8d7e69", + "android_client_info": { + "package_name": "com.mapconductor.example" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAplsr7yaYVUKq8Kiaw-C4tICkDdaSWO0U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c675bf42..ed17ea89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ compose = "1.8.1" composeBom = "2025.05.00" runner = "1.6.2" -mapboxAndroid = "11.13.1" +mapboxAndroid = "11.14.3" playServicesMaps = "19.2.0" secretsGradlePlugin = "2.0.1" @@ -30,6 +30,11 @@ secretsGradlePlugin = "2.0.1" arcgisMapsKotlin = "200.7.0" uiTooling = "1.8.1" vectordrawable = "1.2.0" +roomRuntimeAndroid = "2.7.2" +googleServices = "4.4.3" +firebaseBom = "34.2.0" +googleFirebaseAppdistribution = "5.1.1" +ktLint = "13.1.0" [libraries] # Android 基本 @@ -60,7 +65,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" } # Mapbox -mapbox-android = { module = "com.mapbox.maps:android", version.ref = "mapboxAndroid" } +mapbox-android = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapboxAndroid" } # Google Maps play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } @@ -77,9 +82,16 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } +androidx-room-runtime-android = { group = "androidx.room", name = "room-runtime-android", version.ref = "roomRuntimeAndroid" } + +# Firebase +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices"} +google-firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "googleFirebaseAppdistribution" } +jlleitschuh-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktLint"} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index dc351e37..6b905ac3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -2,63 +2,21 @@ package com.mapconductor.core import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.circle.CircleOverlay import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.groundimage.GroundImageOverlay import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubbleEntry import com.mapconductor.core.map.MapOverlay import com.mapconductor.core.map.MapOverlayRegistry +import com.mapconductor.core.marker.MarkerOverlay import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.polygon.PolygonOverlay import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineOverlay import com.mapconductor.core.polyline.PolylineState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -// data class OverlayProvider( -// val compositionLocal: ProvidableCompositionLocal>>, -// val stateFlow: MutableStateFlow>, -// ) - -// @Composable -// fun ProvideOverlayLocals( -// providers: List>, -// content: @Composable () -> Unit, -// ) { -// val wrapped = -// providers.foldRight(content) { provider, acc -> -// @Suppress("UNCHECKED_CAST") -// { -// val local = provider.compositionLocal as ProvidableCompositionLocal>> -// val flow = provider.stateFlow as MutableStateFlow> -// CompositionLocalProvider(local provides flow) { -// acc() -// } -// } -// } -// -// wrapped() -// } - -// @Composable -// fun CollectAndRenderOverlays( -// map: T?, -// registry: MapOverlayRegistry, -// controller: MapViewController, -// ) { -// registry.getAll().forEach { overlay -> -// @Suppress("UNCHECKED_CAST") -// val typedOverlay = overlay as MapOverlay -// -// val flowState = typedOverlay.flow.collectAsState() -// -// LaunchedEffect(map, flowState.value) { -// if (map == null) return@LaunchedEffect -// typedOverlay.render(flowState.value, controller) -// } -// } -// } open class MapViewScope { val markerFlow = MutableStateFlow>(emptyList()) @@ -73,86 +31,16 @@ open class MapViewScope { registry.register(MarkerOverlay(markerFlow)) registry.register(CircleOverlay(circleFlow)) registry.register(PolylineOverlay(polylineFlow)) - // registry.register(PolygonOverlay(polygonFlow)) // TODO: Implement addPolygons in MapViewController + registry.register(PolygonOverlay(polygonFlow)) registry.register(GroundImageOverlay(groundImageFlow)) return registry } } -class MarkerOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addMarkers(data) - } -} - -class CircleOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addCircles(data) - } -} - -val LocalMarkerCollector = - compositionLocalOf>> { - error("Marker must be under the ") - } - -class PolylineOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addPolylines(data) - } -} - -val LocalPolylineCollector = - compositionLocalOf>> { - error("Polyline must be under the ") - } - -val LocalCircleCollector = - compositionLocalOf>> { - error("Circle must be under the ") - } - -// TODO: Implement addPolygons in MapViewController first -// class PolygonOverlay( -// override val flow: StateFlow>, -// ) : MapOverlay { -// override suspend fun render( -// data: List, -// controller: MapViewController<*, *, *>, -// ) { -// controller.addPolygons(data) -// } -// } -// -// val LocalPolygonCollector = -// compositionLocalOf>> { -// error("Polygon must be under the ") -// } - -val LocalGroundImageCollector = - compositionLocalOf>> { - error("GroundImage must be under the ") - } - @Composable fun CollectAndRenderOverlays( registry: MapOverlayRegistry, - controller: MapViewControllerAlias, + controller: MapViewController, ) { registry.getAll().forEach { overlay -> @Suppress("UNCHECKED_CAST") diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt new file mode 100644 index 00000000..2d656f1c --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt @@ -0,0 +1,44 @@ +package com.mapconductor.core.circle + +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleOverlayRenderer +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractCircleOverlayRenderer : CircleOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun removeCircle(entity: CircleEntity) + + abstract suspend fun createCircle(state: CircleState): ActualCircle? + + abstract suspend fun updateCircleProperties( + circle: ActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): ActualCircle? + + override suspend fun onAdd(data: List): List = + data.map { params -> createCircle(params.state) } + + override suspend fun onChange(data: List>): List = + data.map { params -> + updateCircleProperties( + circle = params.prev.circle, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removeCircle(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt index e19c32cf..bd7d3cc4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt @@ -7,7 +7,6 @@ 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.GeoPoint import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.marker.MarkerState import android.os.Parcelable @@ -134,9 +133,9 @@ data class CircleFingerPrint( val extra: Int, ) -data class CircleClickEvent( +data class CircleEvent( val state: CircleState, - val position: GeoPoint, + val clicked: IGeoPoint, ) -typealias OnCircleEventHandler = (CircleClickEvent) -> Unit +typealias OnCircleEventHandler = (CircleEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt new file mode 100644 index 00000000..67cba2f3 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.circle + +interface CircleCapable { + suspend fun compositionCircles(data: List) + + suspend fun updateCircle(state: CircleState) + + fun setOnCircleClickListener(listener: OnCircleEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt new file mode 100644 index 00000000..a21ccb10 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt @@ -0,0 +1,141 @@ +package com.mapconductor.core.circle + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +abstract class CircleController( + val circleManager: CircleManager, + open val renderer: CircleOverlayRenderer, + override var clickListener: OnCircleEventHandler? = null, +) : OverlayController< + CircleState, + CircleEntity, + CircleEvent, + > { + override val zIndex: Int = 3 + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val modifiedEntities = mutableListOf>() + val previous = circleManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = circleManager.getEntity(state.id)!! + updated.add( + object : CircleOverlayRenderer.ChangeParams { + override val current: CircleEntity = + CircleEntityImpl( + state = state, + circle = prevEntity.circle, + ) + override val prev: CircleEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : CircleOverlayRenderer.AddParams { + override val state: CircleState = state + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + circleManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove circle + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new circles + if (added.isNotEmpty()) { + val actualCircles: List = renderer.onAdd(added) + actualCircles.forEachIndexed { index, circle -> + circle?.let { + val entity = + CircleEntityImpl( + circle = circle, + state = added[index].state, + ) + circleManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed circles + if (updated.isNotEmpty()) { + val actualCircles: List = renderer.onChange(updated) + actualCircles.forEachIndexed { index, circle -> + circle?.let { + val params = updated[index] + val entity = + CircleEntityImpl( + state = params.current.state, + circle = circle, + ) + circleManager.registerEntity(entity) + } + } + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: CircleState) { + semaphore.withPermit { + val prevEntity = circleManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val circle = prevEntity.circle + val entity = + CircleEntityImpl( + circle = circle, + state = state, + ) + val circleParams = + object : CircleOverlayRenderer.ChangeParams { + override val current: CircleEntity = entity + override val prev: CircleEntity = prevEntity + } + val circles = renderer.onChange(listOf(circleParams)) + + circles[0]?.let { + val entity = + CircleEntityImpl( + circle = it, + state = state, + ) + circleManager.registerEntity(entity) + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = circleManager.allEntities() + renderer.onRemove(entities) + circleManager.clear() + } + } + + override fun find(position: IGeoPoint): CircleEntity? = circleManager.find(position) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt index d306f50b..e164058a 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt @@ -5,17 +5,31 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.spherical.haversineDistance import java.util.concurrent.ConcurrentHashMap -class CircleManager { +interface CircleManager { + fun registerEntity(entity: CircleEntity) + + fun removeEntity(id: String): CircleEntity? + + fun getEntity(id: String): CircleEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): CircleEntity? +} + +class CircleManagerImpl : CircleManager { private val entities: ConcurrentHashMap> = ConcurrentHashMap() - fun getEntity(id: String): CircleEntity? = entities.get(id) + override fun getEntity(id: String): CircleEntity? = entities.get(id) - fun removeEntity(id: String): CircleEntity? { + override fun removeEntity(id: String): CircleEntity? { val removed = entities.remove(id) return removed } - fun registerEntity(entity: CircleEntity) { + override fun registerEntity(entity: CircleEntity) { entities[entity.state.id] = entity } @@ -23,13 +37,13 @@ class CircleManager { entities[entity.state.id] = entity } - fun allEntities(): List> = entities.values.toList() + override fun allEntities(): List> = entities.values.toList() - fun clear() { + override fun clear() { entities.clear() } - fun find(position: IGeoPoint): CircleEntity? { + override fun find(position: IGeoPoint): CircleEntity? { val filtered = allEntities().filter { entity -> val centerPos = entity.state.center @@ -53,36 +67,4 @@ class CircleManager { } return maxEntity } - -// private fun isPolygonContains(path: MutableList, point: LatLng?): Boolean { -// var wn = 0 -// val visibleRegion: VisibleRegion = projection.getVisibleRegion() -// val bounds: LatLngBounds = visibleRegion.latLngBounds -// val sw: Point = projection.toScreenLocation(bounds.southwest) -// -// val touchPoint: Point = projection.toScreenLocation(point) -// touchPoint.y = sw.y - touchPoint.y -// var vt: Double -// -// for (i in 0.. touchPoint.y)) { -// vt = (touchPoint.y.toDouble() - a.y.toDouble()) / (b.y.toDouble() - a.y.toDouble()) -// if (touchPoint.x < (a.x.toDouble() + (vt * (b.x.toDouble() - a.x.toDouble())))) { -// wn++ -// } -// } else if ((a.y > touchPoint.y) && (b.y <= touchPoint.y)) { -// vt = (touchPoint.y.toDouble() - a.y.toDouble()) / (b.y.toDouble() - a.y.toDouble()) -// if (touchPoint.x < (a.x.toDouble() + (vt * (b.x.toDouble() - a.x.toDouble())))) { -// wn-- -// } -// } -// } -// -// return (wn != 0) -// } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt new file mode 100644 index 00000000..2b11b69b --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt @@ -0,0 +1,23 @@ +package com.mapconductor.core.circle + +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +val LocalCircleCollector = + compositionLocalOf>> { + error("Circle must be under the ") + } + +class CircleOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewController, + ) { + (controller as? CircleCapable)?.compositionCircles(data) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt index 74a9fd86..3e0409c1 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt @@ -24,7 +24,7 @@ class CircleOverlayManagerImpl( val onChange: suspend (List>) -> List, val onRemove: suspend (List>) -> Unit, val onPostProcess: (suspend () -> Unit)? = null, - val circleManager: CircleManager = CircleManager(), + val circleManager: CircleManagerImpl = CircleManagerImpl(), ) : CircleOverlayManager { val semaphore = Semaphore(1) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt new file mode 100644 index 00000000..50346fdf --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.circle + +interface CircleOverlayRenderer { + interface AddParams { + val state: CircleState + } + + interface ChangeParams { + val current: CircleEntity + val prev: CircleEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt index 49840852..50f034dd 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt @@ -3,15 +3,6 @@ package com.mapconductor.core.circle import com.mapconductor.core.map.MapViewHolder import kotlinx.coroutines.CoroutineScope -interface CircleRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): CircleOverlayManager -} - interface CircleRenderer { interface UpdateParams { val entity: CircleEntity diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt new file mode 100644 index 00000000..6b3b694f --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -0,0 +1,22 @@ +package com.mapconductor.core.controller + +import com.mapconductor.core.map.OnCameraMoveHandler +import com.mapconductor.core.map.OnMapEventHandler + +abstract class BaseMapViewController : MapViewController { + protected var cameraMoveCallback: OnCameraMoveHandler? = null + protected var mapClickCallback: OnMapEventHandler? = null + protected var mapLongClickCallback: OnMapEventHandler? = null + + override fun setCameraMoveListener(listener: OnCameraMoveHandler?) { + this.cameraMoveCallback = listener + } + + override fun setMapClickListener(listener: OnMapEventHandler?) { + this.mapClickCallback = listener + } + + override fun setMapLongClickListener(listener: OnMapEventHandler?) { + this.mapClickCallback = listener + } +} 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 6fccbc27..811ceda8 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 @@ -1,46 +1,15 @@ package com.mapconductor.core.controller -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -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 import kotlinx.coroutines.CoroutineScope -interface MapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope - val markerOverlayManager: MarkerOverlayManager - val hexGeocell: HexGeocell - val polylineOverlayManager: PolylineOverlayManager - val circleOverlayManager: CircleOverlayManager - val polygonOverlayManager: PolygonOverlayManager - - suspend fun addMarkers(data: List) - - suspend fun updateMarker(state: MarkerState) - - suspend fun addPolylines(data: List) - - suspend fun updatePolyline(state: PolylineState) - - suspend fun addCircles(data: List) - - suspend fun updateCircle(state: CircleState) suspend fun clearOverlays() @@ -50,177 +19,14 @@ interface MapViewController - -interface OverlayRenderer { - interface Changes { - val current: EntityType - val prev: EntityType - } - - suspend fun onAdd(data: List): List - - suspend fun onChange(data: List>): List - - suspend fun onRemove(data: List) - - suspend fun onPostProcess() -} - -interface OverlayController { - val zIndex: Int - - // val overlayManager: OverlayManager - val renderer: OverlayRenderer - - suspend fun add(data: List) - - suspend fun update(state: StateType) - - suspend fun clear() - - var clickListener: ((EventType) -> Unit)? - - fun find(position: IGeoPoint): EntityType? -} - -interface OverlayManager { - suspend fun add(states: List) - - suspend fun update(state: StateType) - - suspend fun clear() - - fun getById(id: String): StateType? - - fun allEntities(): List - - fun find(position: IGeoPoint): EntityType? -} - -abstract class BaseMapViewController : - MapViewController { - abstract val markerRenderer: MarkerRenderer - - override val markerOverlayManager: MarkerOverlayManager by lazy { - createMarkerOverlayManager().also { overlayManager -> - markerRenderer.init(overlayManager) - onMarkerOverlayManagerInitialized(overlayManager) - } - } - - abstract val polylineRenderer: PolylineRenderer - - override val polylineOverlayManager: PolylineOverlayManager by lazy { - createPolylineOverlayManager().also { overlayManager -> - polylineRenderer.init(overlayManager) - onPolylineOverlayManagerInitialized(overlayManager) - } - } - - abstract val polygonRenderer: PolygonRenderer - - override val polygonOverlayManager: PolygonOverlayManager by lazy { - createPolygonOverlayManager().also { overlayManager -> - polygonRenderer.init(overlayManager) - onPolygonOverlayManagerInitialized(overlayManager) - } - } - - abstract val circleRenderer: CircleRenderer - - override val circleOverlayManager: CircleOverlayManager by lazy { - createCircleOverlayManager().also { overlayManager -> - circleRenderer.init(overlayManager) - onCircleOverlayManagerInitialized(overlayManager) - } - } - - protected abstract fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) - - protected abstract fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) - - protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) - - protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - - protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager - - protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager - - protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager - - protected abstract fun createCircleOverlayManager(): CircleOverlayManager - - protected var cameraMoveCallback: OnCameraMoveHandler? = null - protected var mapClickCallback: OnMapEventHandler? = null - protected var mapLongClickCallback: OnMapEventHandler? = null - protected var markerClickCallback: OnMarkerEventHandler? = null - protected var markerDragStartCallback: OnMarkerEventHandler? = null - protected var markerDragCallback: OnMarkerEventHandler? = null - protected var markerDragEndCallback: OnMarkerEventHandler? = null - protected var circleClickCallback: OnCircleEventHandler? = null - protected var polylineClickCallback: OnPolylineEventHandler? = null - - abstract fun setupListeners() - - override fun setCameraMoveListener(listener: OnCameraMoveHandler?) { - this.cameraMoveCallback = listener - } - - override fun setMapClickListener(listener: OnMapEventHandler?) { - this.mapClickCallback = listener - } - - override fun setMapLongClickListener(listener: OnMapEventHandler?) { - this.mapClickCallback = listener - } - - override fun setMarkerClickListener(listener: OnMarkerEventHandler?) { - this.markerClickCallback = listener - } - - override fun setMarkerDragStartListener(listener: OnMarkerEventHandler?) { - this.markerDragStartCallback = listener - } - - override fun setMarkerDragListener(listener: OnMarkerEventHandler?) { - this.markerDragCallback = listener - } - - override fun setMarkerDragEndListener(listener: OnMarkerEventHandler?) { - this.markerDragEndCallback = listener - } - - override fun setCircleClickListener(listener: OnCircleEventHandler?) { - this.circleClickCallback = listener - } - - override fun setPolylineClickListener(listener: OnPolylineEventHandler?) { - this.polylineClickCallback = listener - } - - override fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) = - markerRenderer - .setOnMarkerAnimationStart(listener) + fun moveCamera( + dstPosition: MapCameraPosition, + listener: MapViewState.MoveCameraCallback? = null, + ) - override fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) = - markerRenderer - .setOnMarkerAnimationEnd(listener) + fun animateCamera( + dstPosition: MapCameraPosition, + duration: Long, + listener: MapViewState.MoveCameraCallback? = null, + ) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt new file mode 100644 index 00000000..9f7545fa --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt @@ -0,0 +1,17 @@ +package com.mapconductor.core.controller + +import com.mapconductor.core.features.IGeoPoint + +interface OverlayController { + val zIndex: Int + + suspend fun add(data: List) + + suspend fun update(state: StateType) + + suspend fun clear() + + var clickListener: ((EventType) -> Unit)? + + fun find(position: IGeoPoint): EntityType? +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt new file mode 100644 index 00000000..8e9db140 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.controller + +interface OverlayRenderer { + interface ChangeParams { + val current: EntityType + val prev: EntityType + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt index 825991c6..c48df6b6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt @@ -1,7 +1,6 @@ package com.mapconductor.core.features import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.mapconductor.core.spherical.Spherical import com.mapconductor.core.toFixed @@ -13,15 +12,11 @@ interface IGeoPoint { val altitude: Double? } -class GeoPoint( - latitude: Double, - longitude: Double, - altitude: Double = 0.0, +data class GeoPoint( + override val latitude: Double, + override val longitude: Double, + override val altitude: Double = 0.0, ) : IGeoPoint { - override var latitude by mutableStateOf(latitude) - override var longitude by mutableStateOf(longitude) - override var altitude by mutableStateOf(altitude) - fun toUrlValue(precision: Int = 6): String = "${latitude.toFixed(precision)},${longitude.toFixed(precision)}" override fun equals(other: Any?): Boolean { @@ -46,17 +41,6 @@ class GeoPoint( return result } - fun copy( - latitude: Double? = null, - longitude: Double? = null, - altitude: Double? = null, - ): GeoPoint = - GeoPoint( - latitude = latitude ?: this.latitude, - longitude = longitude ?: this.longitude, - altitude = altitude ?: this.altitude, - ) - companion object { fun fromLatLong( latitude: Double, @@ -80,6 +64,68 @@ class GeoPoint( } } } +// class GeoPoint( +// latitude: Double, +// longitude: Double, +// altitude: Double = 0.0, +// ) : IGeoPoint { +// override var latitude by mutableStateOf(latitude) +// override var longitude by mutableStateOf(longitude) +// override var altitude by mutableStateOf(altitude) +// +// fun toUrlValue(precision: Int = 6): String = "${latitude.toFixed(precision)},${longitude.toFixed(precision)}" +// +// override fun equals(other: Any?): Boolean { +// if (this === other) return true +// if (other !is GeoPoint) return false +// +// val tolerance = 1e-7 +// return abs(latitude - other.latitude) < tolerance && +// abs(longitude - other.longitude) < tolerance && +// abs(altitude - other.altitude) < tolerance +// } +// +// override fun hashCode(): Int { +// // 誤差許容しているため、丸めた値を使って hash を安定させる +// val latHash = (latitude * 1e7).toLong() +// val lngHash = (longitude * 1e7).toLong() +// val altHash = (altitude * 1e7).toLong() +// +// var result = latHash.hashCode() +// result = 31 * result + lngHash.hashCode() +// result = 31 * result + altHash.hashCode() +// return result +// } +// +// fun copy( +// latitude: Double? = null, +// longitude: Double? = null, +// altitude: Double? = null, +// ): GeoPoint = +// GeoPoint( +// latitude = latitude ?: this.latitude, +// longitude = longitude ?: this.longitude, +// altitude = altitude ?: this.altitude, +// ) +// +// companion object { +// fun fromLatLong( +// latitude: Double, +// longitude: Double, +// ) = GeoPoint(latitude, longitude) +// +// fun fromLongLat( +// longitude: Double, +// latitude: Double, +// ) = GeoPoint(latitude, longitude) +// +// fun from(position: IGeoPoint) = GeoPoint( +// latitude = position.latitude, +// longitude = position.longitude, +// altitude = position.altitude ?: 0.0, +// ) +// } +// } /** * Extension function to create a normalized GeoPoint with clamped/normalized coordinates diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt index 74b61ca6..7c1e6a65 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt @@ -112,26 +112,27 @@ data class GeoRectBounds( return withinLat && withinLng } - fun getCenter(): GeoPoint? { - if (isEmpty) return null + val center: GeoPoint? + get() { + if (isEmpty) return null - val sw = southWest!! - val ne = northEast!! + val sw = southWest!! + val ne = northEast!! - val centerLat = (sw.latitude + ne.latitude) / 2.0 + val centerLat = (sw.latitude + ne.latitude) / 2.0 - val lng1 = sw.longitude - val lng2 = ne.longitude - val centerLng = - if (lng1 <= lng2) { - (lng1 + lng2) / 2.0 - } else { - val mid = (lng1 + (lng2 + 360)) / 2.0 - if (mid > 180) mid - 360 else mid - } + val lng1 = sw.longitude + val lng2 = ne.longitude + val centerLng = + if (lng1 <= lng2) { + (lng1 + lng2) / 2.0 + } else { + val mid = (lng1 + (lng2 + 360)) / 2.0 + if (mid > 180) mid - 360 else mid + } - return GeoPoint(centerLat, centerLng) - } + return GeoPoint(centerLat, centerLng) + } fun union(other: GeoRectBounds): GeoRectBounds { if (other.isEmpty) return this diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt new file mode 100644 index 00000000..7854571a --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt @@ -0,0 +1,46 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.map.MapViewHolder +import kotlin.collections.forEach +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractGroundImageOverlayRenderer : GroundImageOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun createGroundImage(state: GroundImageState): ActualGroundImage? + + abstract suspend fun updateGroundImageProperties( + groundImage: ActualGroundImage, + current: GroundImageEntity, + prev: GroundImageEntity, + ): ActualGroundImage? + + abstract suspend fun removeGroundImage(entity: GroundImageEntity) + + override suspend fun onAdd(data: List): List = + data.map { params -> + createGroundImage(params.state) + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updateGroundImageProperties( + groundImage = params.prev.groundImage, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removeGroundImage(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 0f7068f3..a2b72dd5 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -68,7 +68,7 @@ data class GroundImageFingerPrint( data class GroundImageEvent( val state: GroundImageState, - val position: GeoPoint?, + val clicked: GeoPoint?, ) typealias OnGroundImageEventHandler = (GroundImageEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt new file mode 100644 index 00000000..cab5cba3 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.groundimage + +interface GroundImageCapable { + suspend fun compositionGroundImages(data: List) + + suspend fun updateGroundImage(state: GroundImageState) + + fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 0a9d34b1..5260b3e9 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -1,80 +1,81 @@ package com.mapconductor.core.groundimage import com.mapconductor.core.controller.OverlayController -import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -interface GroundImageCapable { - suspend fun compositionGroundImages(data: List) - - suspend fun updateGroundImage(state: GroundImageState) -} - -class GroundImageController( - override val renderer: OverlayRenderer>, +abstract class GroundImageController( + val groundImageManager: GroundImageManager, + open val renderer: GroundImageOverlayRenderer, override var clickListener: OnGroundImageEventHandler? = null, ) : OverlayController< - ActualGroundImage, GroundImageState, GroundImageEntity, GroundImageEvent, > { override val zIndex: Int = 2 - val entities = mutableMapOf>() val semaphore = Semaphore(1) override suspend fun add(data: List) { semaphore.withPermit { - val previous = entities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>>() + val modifiedEntities = mutableListOf>() + val previous = groundImageManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() val removed = mutableListOf>() - data.forEach { - if (previous.contains(it.id)) { - val prevEntity = entities[it.id]!! + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = groundImageManager.getEntity(state.id)!! updated.add( - object : OverlayRenderer.Changes> { - override val current = + object : GroundImageOverlayRenderer.ChangeParams { + override val current: GroundImageEntity = GroundImageEntityImpl( groundImage = prevEntity.groundImage, - state = it, + state = state, ) - override val prev = prevEntity + override val prev: GroundImageEntity = prevEntity }, ) - previous.remove(it.id) + previous.remove(state.id) } else { - added.add(it) - previous.remove(it.id) + added.add( + object : GroundImageOverlayRenderer.AddParams { + override val state: GroundImageState = state + }, + ) + previous.remove(state.id) } } previous.forEach { remainId -> - entities.remove(remainId)?.let { removedEntity -> + groundImageManager.removeEntity(remainId)?.let { removedEntity -> removed.add(removedEntity) } } + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + if (added.isNotEmpty()) { val actualOverlays = renderer.onAdd(added) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { - val state = added[index] val entity = GroundImageEntityImpl( groundImage = it, - state = state, + state = added[index].state, ) - entities[state.id] = entity + groundImageManager.registerEntity(entity) + modifiedEntities.add(entity) } } } if (updated.isNotEmpty()) { - val actualOverlays = renderer.onChange(updated.toList()) + val actualOverlays: List = renderer.onChange(updated) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { val state = updated[index].current.state @@ -83,59 +84,57 @@ class GroundImageController( groundImage = it, state = state, ) - entities[state.id] = entity + groundImageManager.registerEntity(entity) } } } - if (removed.isNotEmpty()) { - renderer.onRemove(removed) - } - renderer.onPostProcess() } } override suspend fun update(state: GroundImageState) { semaphore.withPermit { - val updated = mutableListOf>>() - val prevEntity = entities[state.id]!! - updated.add( - object : OverlayRenderer.Changes> { - override val current: GroundImageEntity = - GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = state, - ) - override val prev: GroundImageEntity = prevEntity - }, - ) + val prevEntity = groundImageManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinder = prevEntity.fingerPrint + if (currentFinger == prevFinder) { + return + } - val actualOverlays: List = renderer.onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - GroundImageEntityImpl( - groundImage = it, - state = state, - ) - entities[state.id] = entity + val groundImage = prevEntity.groundImage + val entity = + GroundImageEntityImpl( + groundImage = groundImage, + state = state, + ) + val groundImageParams = + object : GroundImageOverlayRenderer.ChangeParams { + override val current: GroundImageEntity = entity + override val prev: GroundImageEntity = prevEntity } + val groundImages = renderer.onChange(listOf(groundImageParams)) + + groundImages[0]?.let { + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + groundImageManager.registerEntity(entity) } + renderer.onPostProcess() } } override suspend fun clear() { semaphore.withPermit { - renderer.onRemove(entities.values.toList()) - entities.clear() + val entities: List> = groundImageManager.allEntities() + renderer.onRemove(entities) + renderer.onPostProcess() + groundImageManager.clear() } } - override fun find(position: IGeoPoint): GroundImageEntity? { - // TODO: Improve this implementation later - return entities.values.find { entity -> - entity.state.bounds.contains(position) - } - } + override fun find(position: IGeoPoint): GroundImageEntity? = groundImageManager.find(position) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt new file mode 100644 index 00000000..f4c222f6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt @@ -0,0 +1,37 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.features.IGeoPoint + +interface GroundImageManager { + fun registerEntity(entity: GroundImageEntity) + + fun removeEntity(id: String): GroundImageEntity? + + fun getEntity(id: String): GroundImageEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): GroundImageEntity? +} + +class GroundImageManagerImpl : GroundImageManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: GroundImageEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): GroundImageEntity? = entities.remove(id) + + override fun getEntity(id: String): GroundImageEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): GroundImageEntity? = entities.values.firstOrNull() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt index 5ed59d24..8aec54d6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -1,16 +1,23 @@ package com.mapconductor.core.groundimage -import com.mapconductor.core.controller.MapViewControllerAlias +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +val LocalGroundImageCollector = + compositionLocalOf>> { + error("GroundImage must be under the ") + } + class GroundImageOverlay( override val flow: StateFlow>, ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { - (controller as? GroundImageCapable<*>)?.compositionGroundImages(data) + (controller as? GroundImageCapable)?.compositionGroundImages(data) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt new file mode 100644 index 00000000..06efdcc6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.groundimage + +interface GroundImageOverlayRenderer { + interface AddParams { + val state: GroundImageState + } + + interface ChangeParams { + val current: GroundImageEntity + val prev: GroundImageEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 17e312aa..65b910af 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -22,18 +22,23 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import com.mapconductor.core.CollectAndRenderOverlays -import com.mapconductor.core.LocalCircleCollector -import com.mapconductor.core.LocalGroundImageCollector -import com.mapconductor.core.LocalMarkerCollector -import com.mapconductor.core.LocalPolylineCollector import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.LocalCircleCollector +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.groundimage.GroundImageCapable +import com.mapconductor.core.groundimage.LocalGroundImageCollector import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.LocalMarkerCollector +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.LocalPolygonCollector +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.LocalPolylineCollector +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.settings.Settings import android.view.View import android.view.ViewGroup @@ -50,7 +55,7 @@ fun < SpecificState : MapViewState<*>, // Replace Any with a base MapViewController if you have one // Generic type for the actual Android Map View (e.g., com.google.android.gms.maps.MapView) - SpecificController : MapViewControllerAlias, + SpecificController : MapViewController, ActualMapView : View, // Generic type for the actual Map SDK object (e.g., GoogleMap, HereMapSDK.MapController) ActualMap : Any, @@ -88,25 +93,28 @@ fun < LocalInfoBubbleCollector provides scope.bubbleFlow, LocalCircleCollector provides scope.circleFlow, LocalPolylineCollector provides scope.polylineFlow, + LocalPolygonCollector provides scope.polygonFlow, LocalGroundImageCollector provides scope.groundImageFlow, ) { with(scope) { content?.invoke(this) } } - val markers = scope.markerFlow.collectAsState() - markers.value.forEach { markerState -> - LaunchedEffect(markerState.id) { - markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateMarker(markerState) + (controller as? GroundImageCapable)?.let { + val groundImage = scope.groundImageFlow.collectAsState() + groundImage.value.forEach { groundImageState -> + LaunchedEffect(groundImageState.id) { + groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + controller.updateGroundImage(groundImageState) + } } } } - val circles = scope.circleFlow.collectAsState() - circles.value.forEach { circleState -> - LaunchedEffect(circleState.id) { - circleState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateCircle(circleState) + val polygons = scope.polygonFlow.collectAsState() + polygons.value.forEach { polygonState -> + LaunchedEffect(polygonState.id) { + polygonState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + (controller as? PolygonCapable)?.updatePolygon(polygonState) } } } @@ -114,17 +122,23 @@ fun < polylines.value.forEach { polylineState -> LaunchedEffect(polylineState.id) { polylineState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updatePolyline(polylineState) + (controller as? PolylineCapable)?.updatePolyline(polylineState) } } } - (controller as? GroundImageCapable<*>)?.let { - val groundImage = scope.groundImageFlow.collectAsState() - groundImage.value.forEach { groundImageState -> - LaunchedEffect(groundImageState.id) { - groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateGroundImage(groundImageState) - } + val circles = scope.circleFlow.collectAsState() + circles.value.forEach { circleState -> + LaunchedEffect(circleState.id) { + circleState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + (controller as? CircleCapable)?.updateCircle(circleState) + } + } + } + val markers = scope.markerFlow.collectAsState() + markers.value.forEach { markerState -> + LaunchedEffect(markerState.id) { + markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + (controller as? MarkerCapable)?.updateMarker(markerState) } } } @@ -185,7 +199,7 @@ fun < } } - if (controller != null && cameraPosition != null && bubbles.isNotEmpty()) { + if (controller != null && bubbles.isNotEmpty() && cameraPosition != null) { Box( modifier = Modifier diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt index c651e923..2a338aeb 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt @@ -4,9 +4,9 @@ import androidx.compose.ui.geometry.Offset import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.IGeoPoint -interface MapViewHolder { - val mapView: TMapView - val map: TMap +interface MapViewHolder { + val mapView: ActualMapViewType + val map: ActualMapType fun toScreenOffset(position: IGeoPoint): Offset? diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt index 109c5438..0f0a47c2 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt @@ -1,6 +1,6 @@ package com.mapconductor.core.map -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import android.util.Log import kotlinx.coroutines.CoroutineScope @@ -19,21 +19,19 @@ enum class InitState { interface MapViewState { interface MoveCameraCallback { - fun onComplete(result: Boolean) + fun onComplete() } val id: String val initCameraPosition: MapCameraPosition val isInitialized: StateFlow - val cameraPosition: StateFlow + val cameraPosition: StateFlow var mapDesignType: ActualMapDesignType fun initAsync(init: suspend () -> Boolean) fun resetInitState() - fun changeMapDesignType(value: ActualMapDesignType) - fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long = 0, @@ -45,12 +43,13 @@ interface MapViewState { durationMs: Long = 0, listener: MoveCameraCallback? = null, ) + + fun getMapViewHolder(): MapViewHolder<*, *>? } -abstract class MapViewStateImpl( - protected val mainCoroutine: CoroutineScope = - CoroutineScope(Dispatchers.Main), -) : MapViewState { +abstract class MapViewStateImpl( + protected val mainCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : MapViewState { private val tag = this.javaClass.name private val _isInitialized = MutableStateFlow(InitState.NotStarted) @@ -89,7 +88,7 @@ interface MapOverlay { suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt new file mode 100644 index 00000000..cc0aac69 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt @@ -0,0 +1,215 @@ +package com.mapconductor.core.marker + +import com.mapconductor.core.controller.OverlayController +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +interface MarkerCapable { + suspend fun compositionMarkers(data: List) + + suspend fun updateMarker(state: MarkerState) + + fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) + + fun setOnMarkerDrag(listener: OnMarkerEventHandler?) + + fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) + + fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) + + fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) + + fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) +} + +interface MarkerOverlayRenderer { + var animateStartListener: OnMarkerEventHandler? + var animateEndListener: OnMarkerEventHandler? + + interface AddParams { + val state: MarkerState + val bitmapIcon: BitmapIcon + } + + interface ChangeParams { + val current: MarkerEntity + val bitmapIcon: BitmapIcon + val prev: MarkerEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onAnimate(entity: MarkerEntity) + + suspend fun onPostProcess() +} + +abstract class AbstractMarkerController( + val markerManager: MarkerManager, + open val renderer: MarkerOverlayRenderer, + override var clickListener: OnMarkerEventHandler? = null, +) : OverlayController< + MarkerState, + MarkerEntity, + MarkerState, + > { + override val zIndex: Int = 10 + val semaphore = Semaphore(1) + + var dragStartListener: ((MarkerState) -> Unit)? = null + var dragListener: ((MarkerState) -> Unit)? = null + var dragEndListener: ((MarkerState) -> Unit)? = null + + protected fun setDraggingState( + markerState: MarkerState, + dragging: Boolean, + ) { + // Since this "isDragging" property is internal accessor, + // childViewControllers must call this method instead of "isDragging = true/false". + markerState.isDragging = dragging + } + + override suspend fun add(data: List) { + semaphore.withPermit { + val defaultIcon = DefaultIcon() + val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() + val modifiedEntities = mutableListOf>() + val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = markerManager.getEntity(state.id)!! + val markerIcon = state.icon ?: defaultIcon + updated.add( + object : MarkerOverlayRenderer.ChangeParams { + override val current: MarkerEntity = + MarkerEntityImpl( + state = state, + marker = prevEntity.marker, + ) + override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() + override val prev: MarkerEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : MarkerOverlayRenderer.AddParams { + override val state: MarkerState = state + override val bitmapIcon: BitmapIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + markerManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove markers + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new markers + if (added.isNotEmpty()) { + val actualMarkers: List = renderer.onAdd(added) + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val entity = + MarkerEntityImpl( + marker = actualMarker, + state = added[index].state, + ) + markerManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed markers + if (updated.isNotEmpty()) { + val actualMarkers: List = renderer.onChange(updated) + + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val params = updated[index] + val entity = + MarkerEntityImpl( + state = params.current.state, + marker = actualMarker, + ) + markerManager.registerEntity(entity) + } + } + } + modifiedEntities.forEach { entity -> + entity.state.getAnimation()?.let { + renderer.onAnimate(entity) + } + } + renderer.onPostProcess() + } + } + + override suspend fun update(state: MarkerState) { + semaphore.withPermit { + val prevEntity = markerManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val marker = prevEntity.marker + val defaultIcon = DefaultIcon() + val markerIcon = state.icon ?: defaultIcon + + val entity = + MarkerEntityImpl( + marker = marker, + state = state, + ) + val markerParams = + object : MarkerOverlayRenderer.ChangeParams { + override val current: MarkerEntity = entity + override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() + override val prev: MarkerEntity = prevEntity + } + val markers = renderer.onChange(listOf(markerParams)) + + markers[0]?.let { + val entity = + MarkerEntityImpl( + marker = it, + state = state, + ) + markerManager.registerEntity(entity) + + // Execute the animation property + if (prevFinger.animation != currentFinger.animation) { + state.getAnimation()?.let { + renderer.onAnimate(entity) + } + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = markerManager.allEntities() + renderer.onRemove(entities) + markerManager.clear() + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt new file mode 100644 index 00000000..652da368 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt @@ -0,0 +1,135 @@ +package com.mapconductor.core.marker + +import androidx.compose.ui.geometry.Offset +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.settings.Settings +import kotlin.math.min +import kotlin.math.pow +import android.os.SystemClock +import android.view.animation.BounceInterpolator +import android.view.animation.LinearInterpolator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach + +abstract class AbstractMarkerOverlayRenderer< + MapViewHolderType : MapViewHolder<*, *>, + ActualMarker, +>( + val holder: MapViewHolderType, + val coroutine: CoroutineScope, + val tileSize: Int = 256, + val dropAnimateDuration: Int = Settings.Default.markerDropAnimateDuration, + val bounceAnimateDuration: Int = Settings.Default.markerBounceAnimateDuration, +) : MarkerOverlayRenderer { + override var animateStartListener: OnMarkerEventHandler? = null + override var animateEndListener: OnMarkerEventHandler? = null + + abstract fun setMarkerPosition( + markerEntity: MarkerEntity, + position: GeoPoint, + ) + + override suspend fun onAnimate(entity: MarkerEntity) { + val animation = entity.state.getAnimation() + when (animation) { + MarkerAnimation.Drop -> + animateMarkerDrop( + entity = entity, + duration = dropAnimateDuration, + ) + MarkerAnimation.Bounce -> + animateMarkerBounce( + entity = entity, + duration = bounceAnimateDuration, + ) + else -> throw IllegalArgumentException("No animation is available: $animation") + } + } + + fun zoomToMetersPerPixel(zoom: Double): Double { + val earthCircumference = 40075016.686 + return earthCircumference / (tileSize * 2.0.pow(zoom)) + } + + fun animateMarkerDrop( + entity: MarkerEntity, + duration: Int, + ) { + // アニメーションの最終的な目標地点(地理座標) + val target = entity.state.position + + // 線形補間 + val interpolator = LinearInterpolator() + + // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn + val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return + + animateStartListener?.invoke(entity.state) + + flow { + val startTime = SystemClock.uptimeMillis() + var t = 0f + while (t < 1f) { + val elapsed = SystemClock.uptimeMillis() - startTime + t = min(1f, elapsed.toFloat() / duration) + emit(interpolator.getInterpolation(t)) + delay(16L) + } + }.onEach { t: Float -> + // 開始時の画面座標から緯度経度に戻す(垂直方向アニメーション起点) + val startLatLng = holder.fromScreenOffset(startPoint)!! + + // 緯度・経度を線形補間 + val lat = t * target.latitude + (1f - t) * startLatLng.latitude + val lng = t * target.longitude + (1f - t) * startLatLng.longitude + + // 現在の座標をマーカーに適用 + val newPosition = GeoPoint.fromLatLong(lat, lng) + setMarkerPosition(entity, newPosition) + }.onCompletion { + entity.state.position = target + entity.state.setAnimation(null) + animateEndListener?.invoke(entity.state) + }.launchIn(coroutine) + } + + fun animateMarkerBounce( + entity: MarkerEntity, + duration: Int, + ) { + val startTime = SystemClock.uptimeMillis() + + val target = entity.state.position + val interpolator = BounceInterpolator() + val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return + + animateStartListener?.invoke(entity.state) + flow { + var t = 0f + while (t < 1f) { + val elapsed = SystemClock.uptimeMillis() - startTime + t = interpolator.getInterpolation(min(1f, elapsed.toFloat() / duration)) + emit(t) + delay(16L) + } + }.onEach { t -> + val startLatLng = holder.fromScreenOffset(startPoint) ?: return@onEach + val lng = t * target.longitude + (1f - t) * startLatLng.longitude + val lat = t * target.latitude + (1f - t) * startLatLng.latitude + + // 現在の座標をマーカーに適用 + val newPosition = GeoPoint.fromLatLong(lat, lng) + setMarkerPosition(entity, newPosition) + }.onCompletion { + // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) + entity.state.position = target + entity.state.setAnimation(null) + animateEndListener?.invoke(entity.state) + }.launchIn(coroutine) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt index e4e6f0de..ce75d008 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt @@ -80,15 +80,17 @@ abstract class AbstractDefaultIcon( return it } - // 適応的スケーリング情報を使用してアイコンを描画 - val canvasSize = ResourceProvider.dpToPx(iconSize.value) - val bitmap = createBitmap(canvasSize.toInt(), canvasSize.toInt()) + // Calculate canvas size with scale applied + val baseCanvasSize = ResourceProvider.dpToPx(iconSize.value) + val canvasSize = (baseCanvasSize * scale).toInt() + + val bitmap = createBitmap(canvasSize, canvasSize) val canvas = Canvas(bitmap) - // マーカーの描画(適応的スケール適用) + // Draw marker (scale is already applied in canvasSize) drawMarker(canvas, canvasSize.toFloat(), scale) - // ラベルの描画 + // Draw label drawLabel( canvas = canvas, canvasSize = canvasSize.toFloat(), @@ -137,11 +139,35 @@ abstract class AbstractDefaultIcon( iconScale: Float, ): Path { val originalSize = Size(23.5f, 25.6f) - val markerScale = minOf(canvasSize / originalSize.width, canvasSize / originalSize.height) + + // Since canvasSize is already scaled (baseCanvasSize * scale), + // we don't need to apply iconScale again to the markerScale calculation + val scaledStrokeWidth = + ResourceProvider + .dpToPx(strokeWidth.value * iconScale) + .toFloat() + + // Reserve space for stroke on sides and top, but not bottom (point should touch edge) + val epsilon = 0.75f + val padding = (scaledStrokeWidth / 2f - epsilon).coerceAtLeast(0f) + val availableWidth = canvasSize - (padding * 2f) + val availableHeight = canvasSize - padding // Only top padding, bottom point touches edge + + // Calculate scale to fit marker within available space + // DO NOT multiply by iconScale here as canvasSize already includes it + val markerScale = + minOf( + availableWidth / originalSize.width, + availableHeight / originalSize.height, + ) + val scaledWidth = originalSize.width * markerScale val scaledHeight = originalSize.height * markerScale + + // Center horizontally, align bottom point to canvas edge + // The path's bottom point should touch the canvas bottom, the stroke will extend beyond val offsetX = (canvasSize - scaledWidth) / 2f - val offsetY = canvasSize - scaledHeight + ResourceProvider.dpToPx(strokeWidth.value).toFloat() + val offsetY = (canvasSize - scaledHeight + (strokeWidth.value * markerScale)) / 2f return Path().apply { moveTo(12f * markerScale + offsetX, 0f * markerScale + offsetY) @@ -216,25 +242,16 @@ abstract class AbstractDefaultIcon( Paint().apply { color = strokeColor.toArgb() style = Paint.Style.STROKE - strokeWidth = ResourceProvider.dpToPx(this@AbstractDefaultIcon.strokeWidth.value * iconScale).toFloat() + strokeWidth = + ResourceProvider + .dpToPx( + this@AbstractDefaultIcon.strokeWidth.value * iconScale, + ).toFloat() isAntiAlias = true + strokeJoin = Paint.Join.ROUND // 追加 + strokeCap = Paint.Cap.ROUND // 追加(尖り部のにじみ軽減) } - /** - * デバッグ用の枠描画 - */ - private fun drawDebugFrame(canvas: Canvas) { - Paint() - .apply { - isAntiAlias = true - strokeWidth = 1f - color = Color.Black.toArgb() - style = Paint.Style.STROKE - }.also { - canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), it) - } - } - /** * ラベルテキストの描画 */ diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt index 39f24c18..84ad9fe9 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt @@ -11,8 +11,8 @@ import androidx.compose.ui.geometry.Size import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.IGeoPoint import java.io.ByteArrayOutputStream +import java.io.Serializable import android.graphics.Bitmap -import android.os.Parcelable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged class MarkerState( position: IGeoPoint, id: String? = null, - var extra: Parcelable? = null, + var extra: Serializable? = null, icon: MarkerIcon? = null, animation: MarkerAnimation? = null, clickable: Boolean = true, @@ -35,6 +35,7 @@ class MarkerState( icon?.hashCode() ?: 0, clickable.hashCode(), draggable.hashCode(), + animation?.hashCode() ?: 0, ), ) ).toString() @@ -60,7 +61,13 @@ class MarkerState( } } - var animation by mutableStateOf(animation) + private var internalAnimation by mutableStateOf(animation) + + fun setAnimation(animation: MarkerAnimation?) { + internalAnimation = animation + } + + internal fun getAnimation(): MarkerAnimation? = internalAnimation var position by mutableStateOf(position) @@ -71,7 +78,7 @@ class MarkerState( fun copy( id: String? = this.id, position: IGeoPoint = this.position, - extra: Parcelable? = this.extra, + extra: Serializable? = this.extra, icon: MarkerIcon? = this.icon, clickable: Boolean? = this.clickable, draggable: Boolean? = this.draggable, @@ -96,7 +103,7 @@ class MarkerState( result = 31 * result + draggable.hashCode() result = 31 * result + position.hashCode() result = 31 * result + (icon?.hashCode() ?: 0) - result = 31 * result + (ResourceProvider.spToPx(1.0).hashCode() ?: 0) + result = 31 * result + ResourceProvider.spToPx(1.0).hashCode() return result } @@ -108,7 +115,7 @@ class MarkerState( clickable.hashCode(), draggable.hashCode(), internalPosition.hashCode(), - animation.hashCode(), + internalAnimation.hashCode(), ) fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index ddc22005..9a61db0f 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import com.mapconductor.core.MapViewScope import com.mapconductor.core.features.IGeoPoint -import android.os.Parcelable +import java.io.Serializable @Composable fun MapViewScope.Marker(state: MarkerState) { @@ -19,7 +19,7 @@ fun MapViewScope.Marker( clickable: Boolean = true, draggable: Boolean = false, icon: MarkerIcon? = null, - extra: Parcelable? = null, + extra: Serializable? = null, ) { val state = MarkerState( diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerIcon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerIcon.kt index a2879bcc..38c6acc0 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerIcon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerIcon.kt @@ -1,11 +1,14 @@ package com.mapconductor.core.marker import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.core.graphics.createBitmap import androidx.core.graphics.scale import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Paint import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable @@ -25,6 +28,21 @@ abstract class AbstractMarkerIcon : MarkerIcon { abstract override val iconSize: Dp abstract override val infoAnchor: Offset abstract override val debug: Boolean + + /** + * デバッグ用の枠描画 + */ + protected fun drawDebugFrame(canvas: Canvas) { + Paint() + .apply { + isAntiAlias = true + strokeWidth = 1f + color = Color.Black.toArgb() + style = Paint.Style.STROKE + }.also { + canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), it) + } + } } abstract class AndroidDrawableIcon( @@ -35,17 +53,18 @@ abstract class AndroidDrawableIcon( width: Int, height: Int, ): Bitmap { - return when (drawable) { - is BitmapDrawable -> { - drawable.bitmap.scale(width, height) - } - else -> { - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - return bitmap + return if (drawable is BitmapDrawable && !debug) { + drawable.bitmap.scale(width, height) + } else { + val bitmap = createBitmap(width, height) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + if (debug) { + drawDebugFrame(canvas) } + return bitmap } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt new file mode 100644 index 00000000..1e4dc393 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt @@ -0,0 +1,23 @@ +package com.mapconductor.core.marker + +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +val LocalMarkerCollector = + compositionLocalOf>> { + error("Marker must be under the ") + } + +class MarkerOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewController, + ) { + (controller as? MarkerCapable)?.compositionMarkers(data) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt deleted file mode 100644 index 77c034ae..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.mapconductor.core.marker - -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface MarkerOverlayManager { - val markerManager: MarkerManager - - suspend fun addMarkers(markerList: List) - - suspend fun updateMarker(marker: MarkerState) - - suspend fun clearOverlays() - - fun getMarkerState(id: String): MarkerState? -} - -class MarkerOverlayManagerImpl( - override val markerManager: MarkerManager, - val onRemove: suspend (List>) -> Unit, - val onAdd: suspend (List>) -> List, - val onChange: suspend (List>) -> List, - val onPostProcess: (suspend () -> Unit)? = null, - val onAnimate: suspend (entity: MarkerEntity) -> Unit, -) : MarkerOverlayManager { - val semaphore = Semaphore(1) - - override suspend fun addMarkers(markerList: List) { - semaphore.withPermit { - val defaultIcon = DefaultIcon() - val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() - val modifiedEntities = mutableListOf>() - val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - markerList.forEach { state -> - if (previous.contains(state.id)) { - val prevEntity = markerManager.getEntity(state.id)!! - val markerIcon = state.icon ?: defaultIcon - updated.add( - object : UpdateParams { - override val entity: MarkerEntity = - MarkerEntityImpl( - state = state, - marker = prevEntity.marker, - ) - override val bitmapIcon: BitmapIcon - get() { - return markerIcon.toBitmapIcon() - } - override val prevEntity: MarkerEntity = prevEntity - }, - ) - previous.remove(state.id) - return@forEach - } - added.add(state) - previous.remove(state.id) - } - previous.forEach { remainId -> - markerManager.removeEntity(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - // Remove markers - if (removed.isNotEmpty()) { - onRemove(removed) - } - - // Add new markers - if (added.isNotEmpty()) { - val addedList = added.toList() - - addedList - .map { state -> - val markerIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon - Pair(state, markerIcon) - }.also { - val actualMarkers: List = onAdd(it) - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val entity = - MarkerEntityImpl( - marker = actualMarker, - state = addedList[index], - ) - markerManager.registerEntity(entity) - modifiedEntities.add(entity) - } - } - } - } - - // Update changed markers - if (updated.isNotEmpty()) { - val actualMarkers: List = onChange(updated) - - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val params = updated[index] - val entity = - MarkerEntityImpl( - state = params.entity.state, - marker = actualMarker, - ) - markerManager.registerEntity(entity) - } - } - } - modifiedEntities.forEach { entity -> - entity.state.animation?.let { - onAnimate(entity) - } - } - onPostProcess?.invoke() - } - } - - override suspend fun updateMarker(state: MarkerState) { - val prevEntity = markerManager.getEntity(state.id) ?: return - val currentFinger = state.fingerPrint() - val prevFinger = prevEntity.fingerPrint - if (currentFinger == prevFinger) { - return - } - - semaphore.withPermit { - val marker = prevEntity.marker - val defaultIcon = DefaultIcon() - val markerIcon = state.icon ?: defaultIcon - - val entity = - MarkerEntityImpl( - marker = marker, - state = state, - ) - val markerParams = - object : UpdateParams { - override val entity: MarkerEntity = entity - override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() - override val prevEntity: MarkerEntity = prevEntity - } - val markers = onChange(listOf(markerParams)) - - markers[0]?.let { - val entity = - MarkerEntityImpl( - marker = it, - state = state, - ) - markerManager.registerEntity(entity) - - // Execute the animation property - if (prevFinger.animation != currentFinger.animation) { - state.animation?.let { - onAnimate(entity) - } - } - } - } - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities: List> = markerManager.allEntities() - markerManager.clear() - onRemove(entities) - markerManager.clear() - } - } - - override fun getMarkerState(id: String): MarkerState? = markerManager.getEntity(id)?.state -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt deleted file mode 100644 index 346faa16..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.mapconductor.core.marker - -import androidx.compose.ui.geometry.Offset -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapViewHolder -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.spherical.haversineDistance -import com.mapconductor.settings.Settings -import kotlin.math.min -import kotlin.math.pow -import android.os.SystemClock -import android.view.animation.BounceInterpolator -import android.view.animation.LinearInterpolator -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach - -interface MarkerRendererFactory { - fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): MarkerOverlayManager -} - -interface MarkerRenderer { - interface UpdateParams { - val entity: MarkerEntity - val bitmapIcon: BitmapIcon - val prevEntity: MarkerEntity - } - - fun init(markerOverlayManager: MarkerOverlayManager) - - suspend fun addIcons(newMarkers: List>): List - - suspend fun removeIcons(removeEntities: List>) - - suspend fun changeIcons(changes: List>): List - - fun animate(entity: MarkerEntity) - - fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) - - fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) - - fun setDraggingState( - markerState: MarkerState, - dragging: Boolean, - ) - - fun findNearestMarker( - position: IGeoPoint, - tolerance: Double, - zoom: Double, - ): MarkerEntity? -} - -abstract class AbstractMarkerRenderer : MarkerRenderer { - protected val defaultIcon: BitmapIcon - protected var markerAnimationStartHandler: ((state: MarkerState) -> Unit)? = null - protected var markerAnimationEndHandler: ((state: MarkerState) -> Unit)? = null - - protected lateinit var markerOverlayManager: MarkerOverlayManager - - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) { - this.markerAnimationStartHandler = listener - } - - override fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) { - this.markerAnimationEndHandler = listener - } - - init { - defaultIcon = DefaultIcon().toBitmapIcon() - } - - override fun findNearestMarker( - position: IGeoPoint, - tolerance: Double, - zoom: Double, - ): MarkerEntity? { -// val acceptDPI = tolerance.value * ResourceProvider.density - -// clearPolyline() -// -// // 検索範囲の詳細分析 -// val searchAnalysis = analyzeSearchRange(position, zoom, acceptDPI.toDouble()) -// -// // 可視化レイヤーを選択 -// drawSearchOutline(searchAnalysis) - - return findMarkerFromPoint( - position = position, - zoom = zoom, - tolerance = tolerance, - ) - } - - protected abstract fun setMarkerPosition( - markerEntity: MarkerEntity, - position: GeoPoint, - ) - - override fun init(markerManager: MarkerOverlayManager) { - this.markerOverlayManager = markerManager - } - - override fun animate(entity: MarkerEntity) { - when (entity.state.animation) { - MarkerAnimation.Drop -> animateMarkerDrop(entity) - MarkerAnimation.Bounce -> animateMarkerBounce(entity) - else -> throw IllegalArgumentException("No animation is available: ${entity.state.animation}") - } - } - - protected fun zoomToMetersPerPixel(zoom: Double): Double { - val earthCircumference = 40075016.686 - val tileSize = 256 - return earthCircumference / (tileSize * 2.0.pow(zoom)) - } - - override fun setDraggingState( - markerState: MarkerState, - dragging: Boolean, - ) { - // Since this "isDragging" property is internal accessor, - // childViewControllers must call this method instead of "isDragging = true/false". - markerState.isDragging = dragging - } - - protected fun findMarkerFromPoint( - position: IGeoPoint, - zoom: Double, - tolerance: Double, - ): MarkerEntity? { - val meterInMapPixel = zoomToMetersPerPixel(zoom) - val radius = tolerance * meterInMapPixel - val entity = markerOverlayManager.markerManager.findNearest(position) ?: return null - val distance = haversineDistance(position, entity.state.position) - return if (distance <= radius) { - entity - } else { - null - } - } - - protected fun animateMarkerDrop( - markerEntity: MarkerEntity, // ラップしたMarkerオブジェクト - duration: Int = Settings.Default.markerDropAnimateDuration, // アニメションする時間(ms) - ) { - // アニメーションの最終的な目標地点(地理座標) - val target = markerEntity.state.position - - // 線形補間 - val interpolator = LinearInterpolator() - - // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn - val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return - - markerAnimationStartHandler?.invoke(markerEntity.state) - - // ここからアニメ本体 - flow { - val startTime = SystemClock.uptimeMillis() - var t = 0f - while (t < 1f) { - val elapsed = SystemClock.uptimeMillis() - startTime - t = min(1f, elapsed.toFloat() / duration) - emit(interpolator.getInterpolation(t)) - delay(16L) - } - }.onEach { t: Float -> - // 開始時の画面座標から緯度経度に戻す(垂直方向アニメーション起点) - val startLatLng = holder.fromScreenOffset(startPoint)!! - - // 緯度・経度を線形補間 - val lat = t * target.latitude + (1f - t) * startLatLng.latitude - val lng = t * target.longitude + (1f - t) * startLatLng.longitude - - // 現在の座標をマーカーに適用 - val newPosition = GeoPoint.fromLatLong(lat, lng) - setMarkerPosition(markerEntity, newPosition) - }.onCompletion { - // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) - markerEntity.state.position = target - markerEntity.state.animation = null - markerAnimationEndHandler?.invoke(markerEntity.state) - }.launchIn(coroutine) - } - - protected fun animateMarkerBounce( - markerEntity: MarkerEntity, - duration: Int = Settings.Default.markerBounceAnimateDuration, // アニメションする時間(ms) - ) { - val startTime = SystemClock.uptimeMillis() - - // アニメーションの最終的な目標地点(地理座標) - val target = markerEntity.state.position - - // 線形補間 - val interpolator = BounceInterpolator() - - // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn - val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return - - markerAnimationStartHandler?.invoke(markerEntity.state) - flow { - var t = 0f - while (t < 1f) { - val elapsed = SystemClock.uptimeMillis() - startTime - t = interpolator.getInterpolation(min(1f, elapsed.toFloat() / duration)) - emit(t) - delay(16L) - } - }.onEach { t -> - val startLatLng = holder.fromScreenOffset(startPoint) ?: return@onEach - val lng = t * target.longitude + (1f - t) * startLatLng.longitude - val lat = t * target.latitude + (1f - t) * startLatLng.latitude - - // 現在の座標をマーカーに適用 - val newPosition = GeoPoint.fromLatLong(lat, lng) - setMarkerPosition(markerEntity, newPosition) - }.onCompletion { - // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) - markerEntity.state.position = target - markerEntity.state.animation = null - markerAnimationEndHandler?.invoke(markerEntity.state) - }.launchIn(coroutine) - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt new file mode 100644 index 00000000..eeb888d6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt @@ -0,0 +1,43 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractPolygonOverlayRenderer : PolygonOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun removePolygon(entity: PolygonEntity) + + abstract suspend fun createPolygon(state: PolygonState): ActualPolygon? + + abstract suspend fun updatePolygonProperties( + polygon: ActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): ActualPolygon? + + override suspend fun onAdd(data: List): List = + data.map { params -> createPolygon(params.state) } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updatePolygonProperties( + polygon = params.prev.polygon, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removePolygon(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt index 22de0030..b18c8c67 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt @@ -40,7 +40,6 @@ class PolygonState( var fillColor by mutableStateOf(fillColor) var geodesic by mutableStateOf(geodesic) var points by StateFlowDelegate>(points) - var extra by mutableStateOf(extra) private fun polygonId(hashCodes: List): Int = @@ -95,4 +94,9 @@ data class PolygonFingerPrint( val extra: Int, ) -typealias OnPolygonEventHandler = (PolygonState) -> Unit +data class PolygonEvent( + val state: PolygonState, + val clicked: IGeoPoint?, +) + +typealias OnPolygonEventHandler = (PolygonEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt new file mode 100644 index 00000000..6bef04bf --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.polygon + +interface PolygonCapable { + suspend fun compositionPolygons(data: List) + + suspend fun updatePolygon(state: PolygonState) + + fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt new file mode 100644 index 00000000..9fde87e0 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -0,0 +1,141 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +abstract class PolygonController( + val polygonManager: PolygonManager, + open val renderer: PolygonOverlayRenderer, + override var clickListener: OnPolygonEventHandler? = null, +) : OverlayController< + PolygonState, + PolygonEntity, + PolygonEvent, + > { + override val zIndex: Int = 3 + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val modifiedEntities = mutableListOf>() + val previous = polygonManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = polygonManager.getEntity(state.id)!! + updated.add( + object : PolygonOverlayRenderer.ChangeParams { + override val current: PolygonEntity = + PolygonEntityImpl( + state = state, + polygon = prevEntity.polygon, + ) + override val prev: PolygonEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : PolygonOverlayRenderer.AddParams { + override val state: PolygonState = state + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + polygonManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove polygon + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new polygons + if (added.isNotEmpty()) { + val actualPolygons: List = renderer.onAdd(added) + actualPolygons.forEachIndexed { index, polygon -> + polygon?.let { + val entity = + PolygonEntityImpl( + polygon = polygon, + state = added[index].state, + ) + polygonManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed polygons + if (updated.isNotEmpty()) { + val actualPolygons: List = renderer.onChange(updated) + actualPolygons.forEachIndexed { index, polygon -> + polygon?.let { + val params = updated[index] + val entity = + PolygonEntityImpl( + state = params.current.state, + polygon = polygon, + ) + polygonManager.registerEntity(entity) + } + } + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: PolygonState) { + semaphore.withPermit { + val prevEntity = polygonManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val polygon = prevEntity.polygon + val entity = + PolygonEntityImpl( + polygon = polygon, + state = state, + ) + val polygonParams = + object : PolygonOverlayRenderer.ChangeParams { + override val current: PolygonEntity = entity + override val prev: PolygonEntity = prevEntity + } + val polygons = renderer.onChange(listOf(polygonParams)) + + polygons[0]?.let { + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + polygonManager.registerEntity(entity) + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = polygonManager.allEntities() + renderer.onRemove(entities) + polygonManager.clear() + } + } + + override fun find(position: IGeoPoint): PolygonEntity? = polygonManager.find(position) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt new file mode 100644 index 00000000..a61f29b4 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt @@ -0,0 +1,37 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.features.IGeoPoint + +interface PolygonManager { + fun registerEntity(entity: PolygonEntity) + + fun removeEntity(id: String): PolygonEntity? + + fun getEntity(id: String): PolygonEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): PolygonEntity? +} + +class PolygonManagerImpl : PolygonManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: PolygonEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): PolygonEntity? = entities.remove(id) + + override fun getEntity(id: String): PolygonEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): PolygonEntity? = entities.values.firstOrNull() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt new file mode 100644 index 00000000..4b532c6f --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt @@ -0,0 +1,23 @@ +package com.mapconductor.core.polygon + +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +val LocalPolygonCollector = + compositionLocalOf>> { + error("Polygon must be under the ") + } + +class PolygonOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewController, + ) { + (controller as? PolygonCapable)?.compositionPolygons(data) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt deleted file mode 100644 index 41a992be..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.mapconductor.core.polygon - -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface PolygonOverlayManager { - suspend fun addPolygons(polygons: List) - - suspend fun updatePolygon(polygon: PolygonState) - - suspend fun clearOverlays() - - fun getPolygonState(id: String): PolygonState? - - fun getAllEntities(): List> -} - -class PolygonOverlayManagerImpl( - val onAdd: suspend (List) -> List, - val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, - val onPostProcess: (suspend () -> Unit)? = null, -) : PolygonOverlayManager { - val polygonEntities = mutableMapOf>() - - val semaphore = Semaphore(1) - - override suspend fun addPolygons(polygons: List) { - semaphore.withPermit { - val previous = polygonEntities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - polygons.forEach { - if (previous.contains(it.id)) { - val prevEntity = polygonEntities.get(it.id)!! - updated.add( - object : UpdateParams { - override val entity: PolygonEntity = - PolygonEntityImpl( - state = it, - polygon = prevEntity.polygon, - ) - override val prevEntity: PolygonEntity = prevEntity - }, - ) - previous.remove(it.id) - return@forEach - } - added.add(it) - previous.remove(it.id) - } - previous.forEach { remainId -> - polygonEntities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualPolygons = onAdd(added) - actualPolygons.forEachIndexed { index, actualPolygon -> - actualPolygon?.let { - val state = added[index] - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - polygonEntities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualPolygons: List = onChange(updated) - actualPolygons.forEachIndexed { index, actualPolygon -> - actualPolygon?.let { - val state = updated[index].entity.state - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - polygonEntities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - onRemove(removed) - } - onPostProcess?.invoke() - } - } - - override suspend fun updatePolygon(polygon: PolygonState) { - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities = polygonEntities.values.toList() - onRemove(entities) - polygonEntities.clear() - } - } - - override fun getPolygonState(id: String): PolygonState? = polygonEntities.get(id)?.state - - override fun getAllEntities(): List> = polygonEntities.values.toList() -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt new file mode 100644 index 00000000..25681647 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.polygon + +interface PolygonOverlayRenderer { + interface AddParams { + val state: PolygonState + } + + interface ChangeParams { + val current: PolygonEntity + val prev: PolygonEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt deleted file mode 100644 index b9fc8a23..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mapconductor.core.polygon - -import com.mapconductor.core.map.MapViewHolder -import kotlinx.coroutines.CoroutineScope - -interface PolygonRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): PolygonOverlayManager -} - -interface PolygonRenderer { - interface UpdateParams { - val entity: PolygonEntity - val prevEntity: PolygonEntity - } - - fun init(polygonOverlayManager: PolygonOverlayManager) - - suspend fun addPolygons(newPolygons: List): List - - suspend fun removePolygons(removeEntities: List>) - - suspend fun changePolygon(changes: List>): List -} - -abstract class AbstractPolygonRenderer : PolygonRenderer { - protected lateinit var polygonOverlayManager: PolygonOverlayManager - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun init(polygonOverlayManager: PolygonOverlayManager) { - this.polygonOverlayManager = polygonOverlayManager - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt new file mode 100644 index 00000000..a55188af --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt @@ -0,0 +1,45 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractPolylineOverlayRenderer : PolylineOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun createPolyline(state: PolylineState): ActualPolyline? + + abstract suspend fun updatePolylineProperties( + polyline: ActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): ActualPolyline? + + abstract suspend fun removePolyline(entity: PolylineEntity) + + override suspend fun onAdd(data: List): List = + data.map { params -> + createPolyline(params.state) + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updatePolylineProperties( + polyline = params.prev.polyline, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removePolyline(entity) + } + } +} 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 d241594f..48267c6e 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 @@ -16,7 +16,7 @@ class PolylineState( points: List, id: String? = null, strokeColor: Color = Color.Black, - strokeWidth: Dp = 2.dp, + strokeWidth: Dp = 1.dp, geodesic: Boolean = false, extra: Parcelable? = null, ) { @@ -104,4 +104,9 @@ data class PolylineFingerPrint( val extra: Int, ) -typealias OnPolylineEventHandler = (PolylineState) -> Unit +data class PolylineEvent( + val state: PolylineState, + val clicked: IGeoPoint, +) + +typealias OnPolylineEventHandler = (PolylineEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt new file mode 100644 index 00000000..de2a9406 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.polyline + +interface PolylineCapable { + suspend fun compositionPolylines(data: List) + + suspend fun updatePolyline(state: PolylineState) + + fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt new file mode 100644 index 00000000..ceb76bed --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -0,0 +1,144 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +abstract class PolylineController( + val polylineManager: PolylineManager, + open val renderer: PolylineOverlayRenderer, + override var clickListener: OnPolylineEventHandler? = null, +) : OverlayController< + PolylineState, + PolylineEntity, + PolylineEvent, + > { + override val zIndex: Int = 5 + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val modifiedEntities = mutableListOf>() + val previous = polylineManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = polylineManager.getEntity(state.id)!! + updated.add( + object : PolylineOverlayRenderer.ChangeParams { + override val current: PolylineEntity = + PolylineEntityImpl( + state = state, + polyline = prevEntity.polyline, + ) + override val prev: PolylineEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : PolylineOverlayRenderer.AddParams { + override val state: PolylineState = state + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + polylineManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove polylines + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new polylines + if (added.isNotEmpty()) { + val actualPolylines: List = renderer.onAdd(added) + actualPolylines.forEachIndexed { index, polyline -> + polyline?.let { + val entity = + PolylineEntityImpl( + polyline = polyline, + state = added[index].state, + ) + polylineManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed polylines + if (updated.isNotEmpty()) { + val actualPolylines: List = renderer.onChange(updated) + + actualPolylines.forEachIndexed { index, polyline -> + polyline?.let { + val params = updated[index] + val entity = + PolylineEntityImpl( + state = params.current.state, + polyline = polyline, + ) + polylineManager.registerEntity(entity) + } + } + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: PolylineState) { + semaphore.withPermit { + val prevEntity = polylineManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val polyline = prevEntity.polyline + val entity = + PolylineEntityImpl( + polyline = polyline, + state = state, + ) + val polylineParams = + object : PolylineOverlayRenderer.ChangeParams { + override val current: PolylineEntity = entity + override val prev: PolylineEntity = prevEntity + } + val polylines = renderer.onChange(listOf(polylineParams)) + + polylines[0]?.let { + val entity = + PolylineEntityImpl( + polyline = it, + state = state, + ) + polylineManager.registerEntity(entity) + } + renderer.onPostProcess() + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = polylineManager.allEntities() + renderer.onRemove(entities) + renderer.onPostProcess() + polylineManager.clear() + } + } + + override fun find(position: IGeoPoint): PolylineEntity? = polylineManager.find(position) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt new file mode 100644 index 00000000..e2eaf223 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt @@ -0,0 +1,37 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.features.IGeoPoint + +interface PolylineManager { + fun registerEntity(entity: PolylineEntity) + + fun removeEntity(id: String): PolylineEntity? + + fun getEntity(id: String): PolylineEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): PolylineEntity? +} + +class PolylineManagerImpl : PolylineManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: PolylineEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): PolylineEntity? = entities.remove(id) + + override fun getEntity(id: String): PolylineEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): PolylineEntity? = entities.values.firstOrNull() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt new file mode 100644 index 00000000..6a0e030e --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt @@ -0,0 +1,23 @@ +package com.mapconductor.core.polyline + +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +val LocalPolylineCollector = + compositionLocalOf>> { + error("Polyline must be under the ") + } + +class PolylineOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewController, + ) { + (controller as? PolylineCapable)?.compositionPolylines(data) + } +} 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 deleted file mode 100644 index 096562ab..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.mapconductor.core.polyline - -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface PolylineOverlayManager { - suspend fun addPolylines(polylines: List) - - suspend fun updatePolyline(polyline: PolylineState) - - suspend fun clearOverlays() - - fun getPolylineState(id: String): PolylineState? - - fun getAllEntities(): List> -} - -class PolylineOverlayManagerImpl( - val onAdd: suspend (List) -> List, - val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, - val onPostProcess: (suspend () -> Unit)? = null, -) : PolylineOverlayManager { - val polylineEntities = mutableMapOf>() - - val semaphore = Semaphore(1) - - override suspend fun addPolylines(polylines: List) { - semaphore.withPermit { - val previous = polylineEntities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - polylines.forEach { - if (previous.contains(it.id)) { - val prevEntity = polylineEntities.get(it.id)!! - updated.add( - object : UpdateParams { - override val entity: PolylineEntity = - PolylineEntityImpl( - state = it, - polyline = prevEntity.polyline, - ) - override val prevEntity: PolylineEntity = prevEntity - }, - ) - previous.remove(it.id) - return@forEach - } - added.add(it) - previous.remove(it.id) - } - previous.forEach { remainId -> - polylineEntities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualPolylines = onAdd(added) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { - val state = added[index] - val entity = - PolylineEntityImpl( - polyline = it, - state = state, - ) - polylineEntities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualPolylines: List = onChange(updated) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { - val state = updated[index].entity.state - val entity = - PolylineEntityImpl( - polyline = it, - state = state, - ) - polylineEntities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - onRemove(removed) - } - onPostProcess?.invoke() - } - } - - 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 - } - } - } - onPostProcess?.invoke() - } - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities = polylineEntities.values.toList() - onRemove(entities) - polylineEntities.clear() - } - } - - override fun getPolylineState(id: String): PolylineState? = polylineEntities.get(id)?.state - - override fun getAllEntities(): List> = polylineEntities.values.toList() -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt new file mode 100644 index 00000000..7b2ecad7 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.polyline + +interface PolylineOverlayRenderer { + interface AddParams { + val state: PolylineState + } + + interface ChangeParams { + val current: PolylineEntity + val prev: PolylineEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} 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 deleted file mode 100644 index d588459c..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mapconductor.core.polyline - -import com.mapconductor.core.map.MapViewHolder -import kotlinx.coroutines.CoroutineScope - -interface PolylineRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): PolylineOverlayManager -} - -interface PolylineRenderer { - interface UpdateParams { - val entity: PolylineEntity - val prevEntity: PolylineEntity - } - - fun init(polylineOverlayManager: PolylineOverlayManager) - - suspend fun addPolylines(newLines: List): List - - suspend fun removePolylines(removeEntities: List>) - - suspend fun changePolylines(changes: List>): List -} - -abstract class AbstractPolylineRenderer : PolylineRenderer { - protected lateinit var polylineOverlayManager: PolylineOverlayManager - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun init(polylineOverlayManager: PolylineOverlayManager) { - this.polylineOverlayManager = polylineOverlayManager - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt index a5b079eb..485caa50 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt @@ -3,11 +3,13 @@ package com.mapconductor.arcgis import com.arcgismaps.mapping.BasemapStyle import com.mapconductor.core.map.MapDesignType -interface ArcGISDesignType : MapDesignType +interface ArcGISDesignType : MapDesignType { + val elevationSources: List +} data class ArcGISDesign( override val id: String, - val elevationSources: List = emptyList(), + override val elevationSources: List = emptyList(), ) : ArcGISDesignType { override fun getValue(): String = id 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 9add681d..da9be8bf 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 @@ -7,31 +7,30 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler -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.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler @Composable fun ArcGISMapView( - state: ArcGISMapViewState, + state: ArcGISMapViewStateImpl, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onPolylineClick: OnPolylineEventHandler? = {}, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { ArcGISMapViewScope() } // Use specific scope val context = LocalContext.current // Context will be available from MapViewBase too if needed val registry = remember { scope.buildRegistry() } @@ -60,25 +59,23 @@ fun ArcGISMapView( id = state.id, options = options, ) - state.controller = controller controller.holder.mapView.onCreate(owner) controller.holder.mapView.onResume(owner) controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) - controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnCircleClickListener(onCircleClick) + controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) + state.setController(controller) - state.controller = controller - - val restoreCameraPosition = - state.cameraPosition.value - ?: MapCameraPosition.from(state.initCameraPosition) + val restoreCameraPosition = state.cameraPosition.value controller.moveCamera(restoreCameraPosition) controllerRef.value = controller 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 3a672bc7..7d8f33aa 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 @@ -1,385 +1,20 @@ package com.mapconductor.arcgis -import com.arcgismaps.mapping.Basemap -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.arcgismaps.mapping.view.LongPressEvent -import com.arcgismaps.mapping.view.PanChangeEvent -import com.arcgismaps.mapping.view.SingleTapConfirmedEvent -import com.arcgismaps.mapping.view.SurfacePlacement -import com.arcgismaps.mapping.view.UpEvent -import com.arcgismaps.mapping.view.extensions.motionEvent -import com.mapconductor.arcgis.circle.ArcGISCircleRenderer -import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer -import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer -import com.mapconductor.arcgis.marker.DefaultArcGISMarkerRender -import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer -import com.mapconductor.arcgis.polygon.DefaultArcGISPolygonRenderer -import com.mapconductor.arcgis.polyline.ArcGISPolylineRenderer -import com.mapconductor.arcgis.polyline.DefaultArcGISPolylineRenderer -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewState -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.PolygonRenderer -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.settings.Settings -import android.view.MotionEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable -interface IArcGISMapViewController : - MapViewController< - ArcGISActualMarker, - ArcGISActualCircle, - ArcGISActualPolyline, - ArcGISActualPolygon, - > { - fun changeMapDesign(value: String) +typealias ArcGISDesignTypeChangeHandler = (ArcGISDesignType) -> Unit - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) +interface ArcGISMapViewController : + MapViewController, + MarkerCapable, + PolylineCapable, + PolygonCapable, + CircleCapable { + fun setMapDesignType(value: ArcGISDesignType) - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback? = null, - ) -} - -internal data class SelectedMarker( - val state: MarkerState, - val graphic: Graphic, -) - -class ArcGISMapViewController( - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.Relative - }, - private val circleLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat - }, - private val polylineLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded - }, - private val polygonLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded - }, - private val markerRendererFactory: MarkerRendererFactory = DefaultArcGISMarkerRender(), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultArcGISPolylineRenderer(), - private val polygonRendererFactory: PolygonRendererFactory = - DefaultArcGISPolygonRenderer(), - private val circleRendererFactory: CircleRendererFactory = - DefaultArcGISCircleRenderer(), -) : BaseMapViewController< - ArcGISActualMarker, - ArcGISActualCircle, - ArcGISActualPolyline, - ArcGISActualPolygon, - >(), - IArcGISMapViewController { - override val markerRenderer: MarkerRenderer = - ArcGISMarkerRenderer( - markerLayer = markerLayer, - holder = holder, - coroutine = coroutine, - ) - - private var selectedMarker: SelectedMarker? = null - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - - override val polylineRenderer: PolylineRenderer = - ArcGISPolylineRenderer( - polylineLayer = polylineLayer, - holder = holder, - coroutine = coroutine, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - - override val polygonRenderer: PolygonRenderer = - ArcGISPolygonRenderer( - polygonLayer = polygonLayer, - holder = holder, - coroutine = coroutine, - ) - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - - override val circleRenderer: CircleRenderer = - ArcGISCircleRenderer( - circleLayer = circleLayer, - holder = holder, - coroutine = coroutine, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - - init { - markerRenderer.init(markerOverlayManager) - holder.map.graphicsOverlays.clear() - holder.map.graphicsOverlays.add(circleLayer) - holder.map.graphicsOverlays.add(polylineLayer) - holder.map.graphicsOverlays.add(markerLayer) - setupListeners() - } - - override fun setupListeners() { - coroutine.launch { - holder.map.onSingleTapConfirmed.collect { onMapTap(it) } - } - coroutine.launch { - holder.map.viewpointChanged.collect { onViewpointChange() } - } - coroutine.launch { - holder.map.onLongPress.collect { onMapLongPress(it) } - } - coroutine.launch { - holder.map.onUp.collect { onMapUp(it) } - } - coroutine.launch { - holder.map.onPan.collect { onMapPan(it) } - } - } - - private fun onViewpointChange() { - this.cameraMoveCallback?.let { - val mapCamera = holder.map.getCurrentViewpointCamera().toMapCameraPosition() - it(mapCamera) - } - } - - private suspend fun onMapPan(event: PanChangeEvent) { - selectedMarker?.also { - val screenPoint = event.screenCoordinate - val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return - val position = point.toGeoPoint() - it.graphic.geometry = point - it.state.position = position - markerDragCallback?.invoke(it.state) - } - } - - private suspend fun onMapUp(event: UpEvent) { - selectedMarker?.also { - val screenPoint = event.screenCoordinate - val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return - val position = point.toGeoPoint() - it.graphic.geometry = point - it.state.position = position - - // Restore the recomposition for the position property - markerRenderer.setDraggingState(it.state, false) - - markerDragEndCallback?.invoke(it.state) - with(holder.map) { - interactionOptions.isPanEnabled = true - interactionOptions.isRotateEnabled = true - interactionOptions.isZoomEnabled = true - } - } - selectedMarker = null - } - - private suspend fun onMapLongPress(event: LongPressEvent) { - if (event.motionEvent.action != MotionEvent.ACTION_MOVE) return - - val screenPoint = event.screenCoordinate - val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return - val position = point.toGeoPoint() - val identifyResult = - holder.map.identifyGraphicsOverlay( - graphicsOverlay = markerLayer, - screenCoordinate = screenPoint, - tolerance = - Settings.Default.tapTolerance.value - .toDouble(), - returnPopupsOnly = false, - ) - val graphics = identifyResult.getOrNull()?.graphics - val graphic = graphics?.firstOrNull() - if (graphic == null) { - mapLongClickCallback?.invoke(position) - return - } - val markerId = (graphic.attributes.get("id") as? String) ?: return - val state = markerOverlayManager.getMarkerState(markerId) ?: return - selectedMarker = - SelectedMarker( - state = state, - graphic = graphic, - ) - // 3Dナビゲーションを無効化 - with(holder.map) { - interactionOptions.isPanEnabled = false - interactionOptions.isRotateEnabled = false - interactionOptions.isZoomEnabled = false - } - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(state, true) - - markerDragStartCallback?.invoke(state) - } - - private suspend fun onMapTap(event: SingleTapConfirmedEvent) { - val screenPoint = event.screenCoordinate - val touchPosition = - holder.map - .screenToLocation(screenPoint) - .getOrNull() - ?.toGeoPoint() ?: return - - val markerEntity = - markerRenderer.findNearestMarker( - position = touchPosition, - tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity(), - zoom = holder.map.getCurrentViewpointCamera().getZoomLevel(), - ) - if (markerEntity != null) { - markerClickCallback?.invoke(markerEntity.state) - return - } - - val circleEntity = circleOverlayManager.find(touchPosition) - circleEntity?.let { - val event = - CircleClickEvent( - state = circleEntity.state, - position = touchPosition, - ) - circleClickCallback?.invoke(event) - } - - holder.map.screenToLocation(screenPoint).getOrNull()?.also { - mapClickCallback?.invoke(it.toGeoPoint()) - } - } - - override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() - polylineOverlayManager.clearOverlays() - } - - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) - - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) - - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) - - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) - - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - - override fun changeMapDesign(value: String) { - coroutine.launch { - holder.map.scene!!.setBasemap( - Basemap( - ArcGISDesign.toBasemapStyle( - ArcGISDesign(value), - ), - ), - ) - } - } - - override fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback?, - ) { - val dstCameraPosition = dstPosition.toCamera() - - holder.map.setViewpointCamera( - camera = dstCameraPosition, - ) - listener?.onComplete(true) - } - - override fun animateCamera( - dstPosition: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback?, - ) { - val dstCameraPosition = dstPosition.toCamera() - - coroutine.launch { - val result = - holder.map.setViewpointCameraAnimated( - camera = dstCameraPosition, - duration = duration.toFloat() / 1000.0f, - ) - listener?.onComplete(result.isSuccess) - } - } + fun setMapDesignTypeChangeListener(listener: ArcGISDesignTypeChangeHandler) } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt new file mode 100644 index 00000000..f4e5e1a1 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -0,0 +1,299 @@ +package com.mapconductor.arcgis + +import com.arcgismaps.mapping.Basemap +import com.arcgismaps.mapping.view.LongPressEvent +import com.arcgismaps.mapping.view.PanChangeEvent +import com.arcgismaps.mapping.view.SingleTapConfirmedEvent +import com.arcgismaps.mapping.view.UpEvent +import com.arcgismaps.mapping.view.extensions.motionEvent +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayController +import com.mapconductor.arcgis.marker.ArcGISMarkerController +import com.mapconductor.arcgis.marker.SelectedMarker +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayController +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayController +import com.mapconductor.core.circle.CircleEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineEvent +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.settings.Settings +import android.view.MotionEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ArcGISMapViewControllerImpl( + override val holder: ArcGISMapViewHolder, + private val markerController: ArcGISMarkerController, + private val polylineController: ArcGISPolylineOverlayController, + private val polygonController: ArcGISPolygonOverlayController, + private val circleController: ArcGISCircleOverlayController, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : BaseMapViewController(), + ArcGISMapViewController { + init { + holder.map.graphicsOverlays.clear() + holder.map.graphicsOverlays.add(circleController.renderer.circleLayer) + holder.map.graphicsOverlays.add(polygonController.renderer.polygonLayer) + holder.map.graphicsOverlays.add(polylineController.renderer.polylineLayer) + holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) + setupListeners() + } + + fun setupListeners() { + coroutine.launch { + holder.map.onSingleTapConfirmed.collect { onMapTap(it) } + } + coroutine.launch { + holder.map.viewpointChanged.collect { onViewpointChange() } + } + coroutine.launch { + holder.map.onLongPress.collect { onMapLongPress(it) } + } + coroutine.launch { + holder.map.onUp.collect { onMapUp(it) } + } + coroutine.launch { + holder.map.onPan.collect { onMapPan(it) } + } + } + + private fun onViewpointChange() { + this.cameraMoveCallback?.let { + val mapCamera = holder.map.getCurrentViewpointCamera().toMapCameraPosition() + it(mapCamera) + } + } + + private suspend fun onMapPan(event: PanChangeEvent) { + markerController.selectedMarker?.also { + val screenPoint = event.screenCoordinate + val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return + val position = point.toGeoPoint() + it.graphic.geometry = point + it.state.position = position + markerController.dragListener?.invoke(it.state) + } + } + + private suspend fun onMapUp(event: UpEvent) { + markerController.selectedMarker?.also { + val screenPoint = event.screenCoordinate + val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return + val position = point.toGeoPoint() + it.graphic.geometry = point + it.state.position = position + + markerController.selectedMarker = null + markerController.dragEndListener?.invoke(it.state) + + with(holder.map) { + interactionOptions.isPanEnabled = true + interactionOptions.isRotateEnabled = true + interactionOptions.isZoomEnabled = true + } + } + } + + private suspend fun onMapLongPress(event: LongPressEvent) { + if (event.motionEvent.action != MotionEvent.ACTION_MOVE) return + + val screenPoint = event.screenCoordinate + val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return + val position = point.toGeoPoint() + val identifyResult = + holder.map.identifyGraphicsOverlay( + graphicsOverlay = markerController.renderer.markerLayer, + screenCoordinate = screenPoint, + tolerance = + Settings.Default.tapTolerance.value + .toDouble(), + returnPopupsOnly = false, + ) + val graphics = identifyResult.getOrNull()?.graphics + graphics?.firstOrNull()?.let { graphic -> + (graphic.attributes.get("id") as? String)?.let { markerId -> + markerController.markerManager.getEntity(markerId)?.let { entity -> + if (entity.state.draggable) { + markerController.selectedMarker = + SelectedMarker( + state = entity.state, + graphic = graphic, + ) + // 3Dナビゲーションを無効化 + with(holder.map) { + interactionOptions.isPanEnabled = false + interactionOptions.isRotateEnabled = false + interactionOptions.isZoomEnabled = false + } + markerController.dragStartListener?.invoke(entity.state) + return + } + } + } + } + mapLongClickCallback?.invoke(position) + } + + private suspend fun onMapTap(event: SingleTapConfirmedEvent) { + val screenPoint = event.screenCoordinate + val touchPosition = + holder.map + .screenToLocation(screenPoint) + .getOrNull() + ?.toGeoPoint() ?: return + + markerController.find(touchPosition)?.let { markerEntity -> + markerController.clickListener?.invoke(markerEntity.state) + return + } + + circleController.find(touchPosition)?.let { circleEntity -> + val event = + CircleEvent( + state = circleEntity.state, + clicked = touchPosition, + ) + circleController.clickListener?.invoke(event) + return + } + + polylineController.find(touchPosition)?.let { polylineEntity -> + val event = + PolylineEvent( + state = polylineEntity.state, + clicked = touchPosition, + ) + polylineController.clickListener?.invoke(event) + return + } + + polygonController.find(touchPosition)?.let { polygonEntity -> + val event = + PolygonEvent( + state = polygonEntity.state, + clicked = touchPosition, + ) + polygonController.clickListener?.invoke(event) + return + } + + holder.map.screenToLocation(screenPoint).getOrNull()?.also { + mapClickCallback?.invoke(it.toGeoPoint()) + } + } + + override suspend fun clearOverlays() { + markerController.clear() + polylineController.clear() + polygonController.clear() + } + + override suspend fun compositionMarkers(data: List) = markerController.add(data) + + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) + + override suspend fun compositionPolylines(data: List) = polylineController.add(data) + + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + + override suspend fun compositionCircles(data: List) = circleController.add(data) + + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } + + override fun moveCamera( + dstPosition: MapCameraPosition, + listener: MapViewState.MoveCameraCallback?, + ) { + val dstCameraPosition = dstPosition.toCamera() + + holder.map.setViewpointCamera( + camera = dstCameraPosition, + ) + listener?.onComplete() + } + + override fun animateCamera( + dstPosition: MapCameraPosition, + duration: Long, + listener: MapViewState.MoveCameraCallback?, + ) { + val dstCameraPosition = dstPosition.toCamera() + + coroutine.launch { + val result = + holder.map.setViewpointCameraAnimated( + camera = dstCameraPosition, + duration = duration.toFloat() / 1000.0f, + ) + listener?.onComplete() + } + } + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + this.markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + this.markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + this.markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + this.markerController.clickListener = listener + } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + this.polylineController.clickListener = listener + } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + this.polygonController.clickListener = listener + } + + private var mapDesignType: ArcGISDesignType = ArcGISDesign.Streets + private var mapDesignTypeChangeListener: ArcGISDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: ArcGISDesignType) { + holder.map.scene?.let { scene -> + val baseMapStyle = ArcGISDesign.toBasemapStyle(value) + val baseMap = Basemap(baseMapStyle) + coroutine.launch { + scene.setBasemap(baseMap) + } + } + } + + override fun setMapDesignTypeChangeListener(listener: ArcGISDesignTypeChangeHandler) { + mapDesignTypeChangeListener = listener + listener(mapDesignType) + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt similarity index 57% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt index 01739a96..ba2f5ba2 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt @@ -22,24 +22,44 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -class ArcGISMapViewState( +interface ArcGISMapViewState : MapViewState + +class ArcGISMapViewStateImpl( override val id: String, + mapDesignType: ArcGISDesignType, override val initCameraPosition: MapCameraPosition, - override var mapDesignType: ArcGISDesign, -) : MapViewStateImpl() { +) : MapViewStateImpl(), + ArcGISMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() - internal var controller: IArcGISMapViewController? = null - // Camera position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - override fun changeMapDesignType(value: ArcGISDesign) { - this.mapDesignType = value - this.controller?.changeMapDesign(value.getValue()) + private var controller: ArcGISMapViewController? = null + private var _mapDesignType: ArcGISDesignType = mapDesignType + + override var mapDesignType: ArcGISDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType + + internal fun setController(controller: ArcGISMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: ArcGISDesignType) { + _mapDesignType = value } override fun moveCameraTo( @@ -47,20 +67,19 @@ class ArcGISMapViewState( durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - controller?.let { - if (durationMs == 0L) { - it.moveCamera(dstCameraPosition, listener) - } else { - it.animateCamera(dstCameraPosition, durationMs.toInt(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return } - } ?: listener?.onComplete(false) + } + _cameraPosition.value = cameraPosition + listener?.onComplete() } override fun moveCameraTo( @@ -68,30 +87,46 @@ class ArcGISMapViewState( durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - // Do nothing here + if (this.isInitialized.value != InitState.Initialized) { + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() + return + } + val currentPosition = this.cameraPosition.value + val newPosition = + currentPosition.copy( + position = position, + ) + this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): ArcGISMapViewHolder? = controller?.holder as? ArcGISMapViewHolder + internal fun onCameraChange(cameraPosition: MapCameraPosition) { this._cameraPosition.value = cameraPosition } } -class ArcGISMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: ArcGISMapViewState): MapCameraPosition? = state.cameraPosition.value +class ArcGISMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: ArcGISMapViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: ArcGISMapViewState, + state: ArcGISMapViewStateImpl, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + bundle.putString("id", state.mapDesignType?.id ?: ArcGISDesign.Streets.id) } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): ArcGISMapViewState = - ArcGISMapViewState( + ): ArcGISMapViewStateImpl = + ArcGISMapViewStateImpl( id = stateId, mapDesignType = ArcGISDesign.Create( @@ -100,14 +135,14 @@ class ArcGISMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: ArcGISMapViewState): String = state.id + override fun getStateId(state: ArcGISMapViewStateImpl): String = state.id } @Composable fun rememberArcGISMapViewState( mapDesign: ArcGISDesign = ArcGISDesign.Streets, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): ArcGISMapViewState { +): ArcGISMapViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -117,7 +152,7 @@ fun rememberArcGISMapViewState( stateSaver = ArcGISMapViewSaver().createSaver(), ) { mutableStateOf( - ArcGISMapViewState( + ArcGISMapViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index 185a9c8e..0b6578ad 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -1,21 +1,34 @@ package com.mapconductor.arcgis +import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.SceneView +import com.arcgismaps.mapping.view.SurfacePlacement +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayController +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayRenderer +import com.mapconductor.arcgis.marker.ArcGISMarkerController +import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayController +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayRenderer +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayController +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayRenderer +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator import android.content.Context typealias ArcGISMapViewHolder = MapViewHolder object ArcGISViewControllerStore : - StaticHolder() { + StaticHolder() { fun hasCache(id: String): Boolean = this.has(id) fun getOrCreate( context: Context, id: String, options: ArcGISMapViewInitOptions, - ): ArcGISMapViewController { + ): ArcGISMapViewControllerImpl { val existing = this.get(id) if (existing != null) return existing @@ -26,10 +39,98 @@ object ArcGISViewControllerStore : ) val controller = - ArcGISMapViewController( + ArcGISMapViewControllerImpl( holder = holder, + markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), + circleController = getCircleController(holder), ) this.set(id, controller) return controller } + + private fun getCircleController(holder: ArcGISMapViewHolder): ArcGISCircleOverlayController { + val circleLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat + } + + val renderer = + ArcGISCircleOverlayRenderer( + circleLayer = circleLayer, + holder = holder, + ) + + val controller = + ArcGISCircleOverlayController( + renderer = renderer, + ) + return controller + } + + private fun getPolylineController(holder: ArcGISMapViewHolder): ArcGISPolylineOverlayController { + val polylineLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded + } + + val renderer = + ArcGISPolylineOverlayRenderer( + polylineLayer = polylineLayer, + holder = holder, + ) + + val controller = + ArcGISPolylineOverlayController( + renderer = renderer, + ) + return controller + } + + private fun getPolygonController(holder: ArcGISMapViewHolder): ArcGISPolygonOverlayController { + val polygonLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded + } + + val renderer = + ArcGISPolygonOverlayRenderer( + polygonLayer = polygonLayer, + holder = holder, + ) + + val controller = + ArcGISPolygonOverlayController( + renderer = renderer, + ) + return controller + } + + private fun getMarkerController(holder: ArcGISMapViewHolder): ArcGISMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val markerLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.Relative + } + + val renderer = + ArcGISMarkerRenderer( + markerLayer = markerLayer, + holder = holder, + ) + + val controller = + ArcGISMarkerController( + markerManager = manager, + renderer = renderer, + ) + return controller + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt new file mode 100644 index 00000000..151bad11 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.circle + +import com.mapconductor.arcgis.ArcGISActualCircle +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleManagerImpl + +class ArcGISCircleOverlayController( + circleManager: CircleManager = CircleManagerImpl(), + override val renderer: ArcGISCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt new file mode 100644 index 00000000..2a1b06ab --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt @@ -0,0 +1,116 @@ +package com.mapconductor.arcgis.circle + +import com.arcgismaps.geometry.GeodeticCurveType +import com.arcgismaps.geometry.GeometryEngine +import com.arcgismaps.geometry.LinearUnit +import com.arcgismaps.geometry.LinearUnitId +import com.arcgismaps.mapping.symbology.SimpleFillSymbol +import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualCircle +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ArcGISCircleOverlayRenderer( + val circleLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): ArcGISActualCircle? = + withContext(coroutine.coroutineContext) { + val spec = + holder.mapView.sceneView.scene + ?.spatialReference + val centerPoint = GeoPoint.from(state.center).toPoint(spec) + val circleGeometry = + GeometryEngine.bufferGeodeticOrNull( + geometry = centerPoint, + distance = state.radiusMeters, + distanceUnit = LinearUnit(LinearUnitId.Meters), + maxDeviation = Double.NaN, + curveType = GeodeticCurveType.NormalSection, + ) + val stroke = + SimpleLineSymbol( + style = SimpleLineSymbolStyle.Solid, + color = state.strokeColor.toArcGISColor(), + width = state.strokeWidth.value, + ) + val fillSymbol = + SimpleFillSymbol( + style = SimpleFillSymbolStyle.Solid, + color = state.fillColor.toArcGISColor(), + outline = stroke, + ) + val circle = Graphic(circleGeometry, fillSymbol) + + circleLayer.graphics.add(circle) + circle + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + val circles = listOf(entity.circle) + circleLayer.graphics.removeAll(circles) + } + } + + override suspend fun updateCircleProperties( + circle: ArcGISActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): ArcGISActualCircle? = + withContext(coroutine.coroutineContext) { + val spec = + holder.mapView.sceneView.scene + ?.spatialReference + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + val graphic = current.circle + + if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { + val centerPoint = GeoPoint.from(current.state.center).toPoint(spec) + + val newGeometry = + GeometryEngine.bufferGeodeticOrNull( + geometry = centerPoint, + distance = current.state.radiusMeters, + distanceUnit = LinearUnit(LinearUnitId.Meters), + maxDeviation = Double.NaN, + curveType = GeodeticCurveType.NormalSection, + ) + newGeometry?.let { + graphic.geometry = it + } + } + + (graphic.symbol as SimpleFillSymbol).let { symbol -> + if (finger.fillColor != prevFinger.fillColor) { + symbol.color = + current.state.fillColor.toArcGISColor() + } + symbol.outline?.let { outline -> + if (finger.strokeColor != prevFinger.strokeColor) { + outline.color = + current.state.strokeColor.toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + outline.width = current.state.strokeWidth.value + } + } + } + return@withContext graphic + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt deleted file mode 100644 index 620dddb2..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.mapconductor.arcgis.circle - -import com.arcgismaps.geometry.GeodeticCurveType -import com.arcgismaps.geometry.GeometryEngine -import com.arcgismaps.geometry.LinearUnit -import com.arcgismaps.geometry.LinearUnitId -import com.arcgismaps.mapping.symbology.SimpleFillSymbol -import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultArcGISCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class ArcGISCircleRenderer( - val circleLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - override suspend fun addCircles(newCircles: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newCircles.map { state -> - val spec = - holder.mapView.sceneView.scene - ?.spatialReference - val centerPoint = GeoPoint.from(state.center).toPoint(spec) - val circleGeometry = - GeometryEngine.bufferGeodeticOrNull( - geometry = centerPoint, - distance = state.radiusMeters, - distanceUnit = LinearUnit(LinearUnitId.Meters), - maxDeviation = Double.NaN, - curveType = GeodeticCurveType.NormalSection, - ) - val stroke = - SimpleLineSymbol( - style = SimpleLineSymbolStyle.Solid, - color = state.strokeColor.toArcGISColor(), - width = state.strokeWidth.value, - ) - val fillSymbol = - SimpleFillSymbol( - style = SimpleFillSymbolStyle.Solid, - color = state.fillColor.toArcGISColor(), - outline = stroke, - ) - val circle = Graphic(circleGeometry, fillSymbol) - - circleLayer.graphics.add(circle) - circle - } - } - } - - override suspend fun removeCircles(removeEntities: List>) { - val circles = removeEntities.map { it.circle } - coroutine.launch { - circleLayer.graphics.removeAll(circles) - } - } - - override suspend fun changeCircle(changes: List>): List { - return withContext(coroutine.coroutineContext) { - val spec = - holder.mapView.sceneView.scene - ?.spatialReference - return@withContext changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - val graphic = params.entity.circle - - if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { - val centerPoint = GeoPoint.from(params.entity.state.center).toPoint(spec) - - val newGeometry = - GeometryEngine.bufferGeodeticOrNull( - geometry = centerPoint, - distance = params.entity.state.radiusMeters, - distanceUnit = LinearUnit(LinearUnitId.Meters), - maxDeviation = Double.NaN, - curveType = GeodeticCurveType.NormalSection, - ) - newGeometry?.let { - graphic.geometry = it - } - } - - (graphic.symbol as SimpleFillSymbol).let { symbol -> - if (finger.fillColor != prevFinger.fillColor) { - symbol.color = - params.entity.state.fillColor - .toArcGISColor() - } - symbol.outline?.let { outline -> - if (finger.strokeColor != prevFinger.strokeColor) { - outline.color = - params.entity.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - outline.width = params.entity.state.strokeWidth.value - } - } - } - graphic - } - } - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt new file mode 100644 index 00000000..462d6906 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt @@ -0,0 +1,63 @@ +package com.mapconductor.arcgis.marker + +import com.arcgismaps.mapping.view.Graphic +import com.mapconductor.arcgis.ArcGISActualMarker +import com.mapconductor.arcgis.getZoomLevel +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.settings.Settings + +internal data class SelectedMarker( + val state: MarkerState, + val graphic: Graphic, +) + +class ArcGISMarkerController( + markerManager: MarkerManager, + override val renderer: ArcGISMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: SelectedMarker? = null + + internal var selectedMarker: SelectedMarker? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + // Restore the recomposition for the position property + setDraggingState(it.state, false) + } + return + } + internalSelectedMarker = value + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + } + get() = internalSelectedMarker + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val zoom = + renderer.holder.map + .getCurrentViewpointCamera() + .getZoomLevel() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt index 45759e64..2b31ae4c 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt @@ -4,48 +4,27 @@ import androidx.core.graphics.drawable.toDrawable import com.arcgismaps.mapping.symbology.PictureMarkerSymbol import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualMarker import com.mapconductor.arcgis.ArcGISMapViewHolder import com.mapconductor.arcgis.toPoint import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultArcGISMarkerRender : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onAdd = onIconAdd, - onChange = onIconChange, - onRemove = onIconRemove, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class ArcGISMarkerRenderer( - private val markerLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { + val markerLayer: GraphicsOverlay, + holder: ArcGISMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer( + holder = holder, + coroutine = coroutine, + ) { override fun setMarkerPosition( markerEntity: MarkerEntity, position: GeoPoint, @@ -55,16 +34,18 @@ class ArcGISMarkerRenderer( } } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - newMarkers + data .map { params -> - val bitmapDrawable = params.second.bitmap.toDrawable(holder.mapView.context.resources) + val bitmapDrawable = params.bitmapIcon.bitmap.toDrawable(holder.mapView.context.resources) val density = ResourceProvider.getDensity() - val width = ((params.second.size.width * (params.first.icon?.scale ?: 1.0f)) / density) - val height = ((params.second.size.height * (params.first.icon?.scale ?: 1.0f)) / density) - val anchorX = (0.5 - params.second.anchor.x) * width - val anchorY = (params.second.anchor.y - 0.5) * height +// val width = ((params.bitmapIcon.size.width * (params.state.icon?.scale ?: 1.0f)) / density) +// val height = ((params.bitmapIcon.size.height * (params.state.icon?.scale ?: 1.0f)) / density) + val width = params.bitmapIcon.size.width / density + val height = params.bitmapIcon.size.height / density + val anchorX = (0.5 - params.bitmapIcon.anchor.x) * width + val anchorY = (params.bitmapIcon.anchor.y - 0.5) * height val pictureSymbolFuture = PictureMarkerSymbol.createWithImage(bitmapDrawable).also { @@ -76,10 +57,10 @@ class ArcGISMarkerRenderer( val marker = Graphic( - geometry = GeoPoint.from(params.first.position).toPoint(holder.map.scene?.spatialReference), + geometry = GeoPoint.from(params.state.position).toPoint(holder.map.scene?.spatialReference), symbol = pictureSymbolFuture, ).also { - it.attributes.set("id", params.first.id) + it.attributes.set("id", params.state.id) } return@map marker }.also { @@ -88,18 +69,24 @@ class ArcGISMarkerRenderer( } } - override suspend fun removeIcons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - val elements = removeEntities.map { params -> params.marker } + val elements = data.map { params -> params.marker } markerLayer.graphics.removeAll(elements) } } - override suspend fun changeIcons(changes: List>): List = + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>, + ): List = withContext(coroutine.coroutineContext) { - changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint + data.map { params -> + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint if (currFinger.icon != prevFinger.icon) { val bitmapDrawable = params.bitmapIcon.bitmap.toDrawable(holder.mapView.context.resources) val density = ResourceProvider.getDensity() @@ -115,16 +102,16 @@ class ArcGISMarkerRenderer( it.offsetX = anchorX.toFloat() it.offsetY = anchorY.toFloat() } - params.entity.marker.symbol = pictureSymbolFuture + params.current.marker.symbol = pictureSymbolFuture } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.geometry = - GeoPoint.from(params.entity.state.position).toPoint() + if (params.current.state.position != params.prev.state.position) { + params.current.marker.geometry = + GeoPoint.from(params.current.state.position).toPoint() } // ArcGISはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker + params.current.marker } } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt new file mode 100644 index 00000000..1519d161 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.polygon + +import com.mapconductor.arcgis.ArcGISActualPolygon +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl + +class ArcGISPolygonOverlayController( + polygonManager: PolygonManager = PolygonManagerImpl(), + override val renderer: ArcGISPolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt new file mode 100644 index 00000000..8724d3ab --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt @@ -0,0 +1,104 @@ +package com.mapconductor.arcgis.polygon + +import com.arcgismaps.geometry.Geometry +import com.arcgismaps.geometry.PolygonBuilder +import com.arcgismaps.mapping.symbology.SimpleFillSymbol +import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualPolygon +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonState +import kotlin.collections.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ArcGISPolygonOverlayRenderer( + val polygonLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolygonOverlayRenderer() { + override suspend fun createPolygon(state: PolygonState): ArcGISActualPolygon? = + withContext(coroutine.coroutineContext) { + val geometry = createGeometry(state) + val outlineSymbol = + SimpleLineSymbol().apply { + style = SimpleLineSymbolStyle.Solid + color = state.strokeColor.toArcGISColor() + width = state.strokeWidth.value.toFloat() + } + + val fillSymbol = + SimpleFillSymbol().apply { + style = SimpleFillSymbolStyle.Solid + color = state.fillColor.toArcGISColor() + outline = outlineSymbol + } + + val graphic = + Graphic(geometry, fillSymbol).also { + it.attributes.set("id", state.id) + } + + polygonLayer.graphics.add(graphic) + graphic + } + + override suspend fun updatePolygonProperties( + polygon: ArcGISActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): ArcGISActualPolygon? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + if (finger.points != prevFinger.points) { + current.polygon.geometry = createGeometry(current.state) + } + + (current.polygon.symbol as SimpleFillSymbol).let { symbol -> + if (finger.fillColor != prevFinger.fillColor) { + symbol.color = + current.state.fillColor + .toArcGISColor() + } + symbol.outline?.let { outline -> + if (finger.strokeColor != prevFinger.strokeColor) { + outline.color = + current.state.strokeColor + .toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + outline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + } + } + polygon + } + + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + polygonLayer.graphics.remove(entity.polygon) + } + } + + private fun createGeometry(state: PolygonState): Geometry { + val polygonBuilder = + PolygonBuilder().also { builder -> + state.points.forEach { + builder.addPoint(GeoPoint.from(it).toPoint()) + } + } + return polygonBuilder.toGeometry() + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt deleted file mode 100644 index 7a610610..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.mapconductor.arcgis.polygon - -import com.arcgismaps.geometry.Geometry -import com.arcgismaps.geometry.PolygonBuilder -import com.arcgismaps.mapping.symbology.SimpleFillSymbol -import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISActualPolygon -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polygon.PolygonState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultArcGISPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class ArcGISPolygonRenderer( - val polygonLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newPolygons.map { state -> - - val geometry = createGeometry(state) - - val outlineSymbol = - SimpleLineSymbol().apply { - style = SimpleLineSymbolStyle.Solid - color = state.strokeColor.toArcGISColor() - width = state.strokeWidth.value.toFloat() - } - - val fillSymbol = - SimpleFillSymbol().apply { - style = SimpleFillSymbolStyle.Solid - color = state.fillColor.toArcGISColor() - outline = outlineSymbol - } - - val graphic = - Graphic(geometry, fillSymbol).also { - it.attributes.set("id", state.id) - } - - polygonLayer.graphics.add(graphic) - - return@map graphic - } - } - } - - override suspend fun removePolygons(removeEntities: List>) { - val polygons = removeEntities.map { it.polygon } - coroutine.launch { - polygonLayer.graphics.removeAll(polygons) - } - } - - override suspend fun changePolygon(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() - if (finger.points != prevFinger.points) { - params.entity.polygon.geometry = createGeometry(params.entity.state) - } - - (params.entity.polygon.symbol as SimpleFillSymbol).let { symbol -> - if (finger.fillColor != prevFinger.fillColor) { - symbol.color = - params.entity.state.fillColor - .toArcGISColor() - } - symbol.outline?.let { outline -> - if (finger.strokeColor != prevFinger.strokeColor) { - outline.color = - params.entity.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - outline.width = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - } - } - } - return@map params.entity.polygon - } - } - } - - private fun createGeometry(state: PolygonState): Geometry { - val polygonBuilder = - PolygonBuilder().also { builder -> - state.points.forEach { - builder.addPoint(GeoPoint.from(it).toPoint()) - } - } - return polygonBuilder.toGeometry() - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt new file mode 100644 index 00000000..742d0f85 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.polyline + +import com.mapconductor.arcgis.ArcGISActualPolyline +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl + +class ArcGISPolylineOverlayController( + polylineManager: PolylineManager = PolylineManagerImpl(), + override val renderer: ArcGISPolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt new file mode 100644 index 00000000..85799970 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt @@ -0,0 +1,109 @@ +package com.mapconductor.arcgis.polyline + +import com.arcgismaps.geometry.Geometry +import com.arcgismaps.geometry.PolylineBuilder +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualPolyline +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.spherical.Spherical +import kotlin.collections.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ArcGISPolylineOverlayRenderer( + val polylineLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): ArcGISActualPolyline? = + withContext(coroutine.coroutineContext) { + val geometry = createGeometry(state) + + val lineSymbol = + SimpleLineSymbol().apply { + style = SimpleLineSymbolStyle.Solid + color = state.strokeColor.toArcGISColor() + width = state.strokeWidth.value.toFloat() + } + + val graphic = + Graphic(geometry, lineSymbol).also { + it.attributes.set("id", state.id) + } + + polylineLayer.graphics.add(graphic) + graphic + } + + override suspend fun updatePolylineProperties( + polyline: ArcGISActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): ArcGISActualPolyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + polyline.geometry = createGeometry(current.state) + } + + (polyline.symbol as SimpleLineSymbol).let { symbol -> + if (finger.strokeColor != prevFinger.strokeColor) { + symbol.color = current.state.strokeColor.toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + symbol.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + polylineLayer.graphics.remove(entity.polyline) + } + } + + private fun createGeometry(state: PolylineState): Geometry { + val polylineBuilder = + PolylineBuilder().also { builder -> + if (state.geodesic) { + state.points.forEach { + builder.addPoint(GeoPoint.from(it).toPoint()) + } + return@also + } + + builder.addPoint(GeoPoint.from(state.points[0]).toPoint()) + for (i in 1 until state.points.size) { + var fraction = 0.0 + while (fraction <= 1.0) { + val point = + Spherical.linearInterpolate( + from = state.points[i - 1], + to = state.points[i], + fraction = fraction, + ) + builder.addPoint(point.toPoint()) + fraction += 0.01 + } + builder.addPoint(GeoPoint.from(state.points[i]).toPoint()) + } + } + return polylineBuilder.toGeometry() + } +} 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 deleted file mode 100644 index 9091c7eb..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.mapconductor.arcgis.polyline - -import com.arcgismaps.geometry.Geometry -import com.arcgismaps.geometry.PolylineBuilder -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -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.core.spherical.Spherical -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultArcGISPolylineRenderer : 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 ArcGISPolylineRenderer( - val polylineLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newLines.map { state -> - - val geometry = createGeometry(state) - - val lineSymbol = - SimpleLineSymbol().apply { - style = SimpleLineSymbolStyle.Solid - color = state.strokeColor.toArcGISColor() - width = state.strokeWidth.value.toFloat() // 線の太さ - } - - val graphic = - Graphic(geometry, lineSymbol).also { - it.attributes.set("id", state.id) - } - - polylineLayer.graphics.add(graphic) - - return@map graphic - } - } - } - - override suspend fun removePolylines(removeEntities: List>) { - val polylines = removeEntities.map { it.polyline } - coroutine.launch { - polylineLayer.graphics.removeAll(polylines) - } - } - - override suspend fun changePolylines(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - params.entity.polyline.geometry = createGeometry(params.entity.state) - } - - (params.entity.polyline.symbol as SimpleLineSymbol).let { symbol -> - if (finger.strokeColor != prevFinger.strokeColor) { - symbol.color = - params.entity.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - symbol.width = ResourceProvider.dpToPx(params.prevEntity.state.strokeWidth).toFloat() - } - } - return@map params.prevEntity.polyline - } - } - } - - private fun createGeometry(state: PolylineState): Geometry { - val polylineBuilder = - PolylineBuilder().also { builder -> - if (state.geodesic) { - state.points.forEach { - builder.addPoint(GeoPoint.from(it).toPoint()) - } - return@also - } - - builder.addPoint(GeoPoint.from(state.points[0]).toPoint()) - for (i in 1 until state.points.size) { - var fraction = 0.0 - while (fraction <= 1.0) { - val point = - Spherical.linearInterpolate( - from = state.points[i - 1], - to = state.points[i], - fraction = fraction, - ) - builder.addPoint(point.toPoint()) - fraction += 0.01 - } - builder.addPoint(GeoPoint.from(state.points[i]).toPoint()) - } - } - return polylineBuilder.toGeometry() - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt index 873a17cf..8e810801 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt @@ -33,5 +33,14 @@ sealed class GoogleMapDesign( Terrain.id -> Terrain else -> None } + + fun toMapDesignType(id: Int): GoogleMapDesignType = + when (id) { + MAP_TYPE_NORMAL -> Normal + MAP_TYPE_SATELLITE -> Satellite + MAP_TYPE_HYBRID -> Hybrid + MAP_TYPE_TERRAIN -> Terrain + else -> None + } } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt index 68769060..7b68fdb6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt @@ -6,8 +6,8 @@ import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.Polyline -typealias ActualGoogleMapMarker = Marker -typealias ActualGoogleMapCircle = Circle -typealias ActualGoogleMapPolyline = Polyline -typealias ActualGoogleMapPolygon = Polygon -typealias ActualGoogleMapGroundImage = GroundOverlay +typealias GoogleMapActualMarker = Marker +typealias GoogleMapActualCircle = Circle +typealias GoogleMapActualPolyline = Polyline +typealias GoogleMapActualPolygon = Polygon +typealias GoogleMapActualGroundImage = GroundOverlay 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 3dc97f44..0ed5e9a6 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 @@ -17,27 +17,29 @@ import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import android.view.ViewGroup @Composable fun GoogleMapsView( - state: IGoogleMapViewState, + state: GoogleMapViewStateImpl, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = {}, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, + onGroundImageClick: OnGroundImageEventHandler? = null, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { GoogleMapViewScope() } // Use specific scope val context = LocalContext.current // Context will be available from MapViewBase too if needed val registry = remember { scope.buildRegistry() } @@ -54,20 +56,20 @@ fun GoogleMapsView( // Specific Google Maps initialization logic // This lambda will be executed within state.initAsync by MapViewBase val cameraPosition = - state.cameraPosition.value.let { + state.cameraPosition.value?.let { camera -> CameraPosition .Builder() .apply { - target(GeoPoint.from(it.position).toLatLng()) - zoom(it.zoom.toFloat()) - bearing(it.bearing.toFloat()) - tilt(it.tilt.toFloat()) + target(GeoPoint.from(camera.position).toLatLng()) + zoom(camera.zoom.toFloat()) + bearing(camera.bearing.toFloat()) + tilt(camera.tilt.toFloat()) }.build() } val mapInitOptions = GoogleMapOptions() - .mapType(state.mapDesignType.getValue()) + .mapType(state.mapDesignType?.getValue() ?: GoogleMapDesign.None.getValue()) .camera(cameraPosition) val controller = @@ -76,20 +78,20 @@ fun GoogleMapsView( id = state.id, options = mapInitOptions, ) - (state as? GoogleMapViewState)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + state.setController(controller) + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) - controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnCircleClickListener(onCircleClick) + controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setOnGroundImageClickListener(onGroundImageClick) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) 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 3151b6fc..731e0da4 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 @@ -1,351 +1,22 @@ package com.mapconductor.googlemaps -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.GoogleMap.CancelableCallback -import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener -import com.google.android.gms.maps.GoogleMap.OnCameraMoveCanceledListener -import com.google.android.gms.maps.GoogleMap.OnCameraMoveListener -import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener -import com.google.android.gms.maps.GoogleMap.OnMapClickListener -import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener -import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener -import com.google.android.gms.maps.model.Circle -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.Marker -import com.google.android.gms.maps.model.Polygon -import com.google.android.gms.maps.model.Polyline -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.groundimage.GroundImageCapable -import com.mapconductor.core.groundimage.GroundImageController -import com.mapconductor.core.groundimage.GroundImageEvent -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.core.groundimage.OnGroundImageEventHandler -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewState -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.PolygonRenderer -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.googlemaps.circle.DefaultGoogleMapCircleRenderer -import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer -import com.mapconductor.googlemaps.marker.DefaultGoogleMapMarkerRenderer -import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer -import com.mapconductor.googlemaps.polygon.DefaultGoogleMapPolygonRenderer -import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer -import com.mapconductor.googlemaps.polyline.DefaultGoogleMapPolylineRenderer -import com.mapconductor.googlemaps.polyline.GoogleMapPolylineRenderer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -interface IGoogleMapViewController : - MapViewController, - GroundImageCapable { - fun changeMapDesign(value: Int) - - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback? = null, - ) -} - -class GoogleMapViewController( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultGoogleMapMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), - private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), - private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), - private val groundImageController: GroundImageController, -) : BaseMapViewController< - ActualGoogleMapMarker, - ActualGoogleMapCircle, - ActualGoogleMapPolyline, - ActualGoogleMapPolygon, - >(), - IGoogleMapViewController, - OnCameraMoveStartedListener, - OnCameraMoveCanceledListener, - OnCameraMoveListener, - OnCameraIdleListener, - OnMarkerClickListener, - OnMapClickListener, - OnMarkerDragListener { - override val markerRenderer: MarkerRenderer = - GoogleMapMarkerRenderer( - 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 = - GoogleMapPolylineRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - - override val polygonRenderer: PolygonRenderer = - GoogleMapPolygonRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - - override val circleRenderer: CircleRenderer = - GoogleMapCircleRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - - init { - setupListeners() - markerRenderer.init(markerOverlayManager) - } - - override fun setupListeners() { - holder.map.setOnCameraMoveStartedListener(this) - holder.map.setOnCameraMoveCanceledListener(this) - holder.map.setOnCameraMoveListener(this) - holder.map.setOnCameraIdleListener(this) - holder.map.setOnMarkerClickListener(this) - holder.map.setOnMapClickListener(this) - holder.map.setOnMarkerDragListener(this) - } - - override fun changeMapDesign(value: Int) { - coroutine.launch { - holder.map.mapType = value - } - } - - override fun moveCamera( - position: MapCameraPosition, - listener: MapViewState.MoveCameraCallback?, - ) { - coroutine.launch { - val dstCameraPosition = position.toCameraPosition() - val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) - holder.map.moveCamera(cameraUpdate) - listener?.onComplete(true) - } - } - - override fun animateCamera( - position: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback?, - ) { - val dstCameraPosition = position.toCameraPosition() - coroutine.launch { - val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) - holder.map.animateCamera( - cameraUpdate, - duration, - object : CancelableCallback { - override fun onCancel() { - listener?.onComplete(false) - } - - override fun onFinish() { - listener?.onComplete(true) - } - }, - ) - } - } - - override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() - polylineOverlayManager.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) - - override fun onCameraMove() { - cameraMoveCallback?.let { - val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() - coroutine.launch { it(mapCameraPosition) } - } - } - - override fun onCameraIdle() { - cameraMoveCallback?.let { - val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() - coroutine.launch { it(mapCameraPosition) } - } - } - - override fun onCameraMoveStarted(p0: Int) { - cameraMoveCallback?.let { - val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() - coroutine.launch { it(mapCameraPosition) } - } - } - - override fun onCameraMoveCanceled() { - cameraMoveCallback?.let { - val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() - coroutine.launch { it(mapCameraPosition) } - } - } - - override fun onMarkerClick(marker: Marker): Boolean { - val key = marker.tag?.toString() ?: return true - val state = markerOverlayManager.getMarkerState(key) ?: return true - if (!state.clickable) return true - markerClickCallback?.let { - coroutine.launch { - it(state) - } - } - return true - } - - override fun onMapClick(position: LatLng) { - val touchPosition = position.toGeoPoint() - - circleOverlayManager.find(touchPosition)?.let { entity -> - val event = - CircleClickEvent( - state = entity.state, - position = touchPosition, - ) - circleClickCallback?.invoke(event) - return - } - - groundImageController.find(touchPosition)?.let { entity -> - val event = - GroundImageEvent( - state = entity.state, - position = touchPosition, - ) - coroutine.launch { - groundImageController.clickListener?.invoke(event) - } - return - } - - mapClickCallback?.let { - coroutine.launch { it(position.toGeoPoint()) } - } - } - - private fun getMarkerStateFrom(marker: Marker): MarkerState? { - val markerId = marker.tag as? String ?: return null - return markerOverlayManager.getMarkerState(markerId) - } - - override fun onMarkerDrag(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(state, true) - - state.position = marker.position.toGeoPoint() - markerDragCallback?.invoke(state) - } - } - - override fun onMarkerDragEnd(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> - state.position = marker.position.toGeoPoint() - markerDragEndCallback?.invoke(state) - } - } - - override fun onMarkerDragStart(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> - state.position = marker.position.toGeoPoint() - - // Restore the recomposition for the position property - markerRenderer.setDraggingState(state, false) - - markerDragStartCallback?.invoke(state) - } - } - - override suspend fun compositionGroundImages(data: List) = groundImageController.add(data) - - override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) - - fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { - this.groundImageController.clickListener = listener - } +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable + +typealias GoogleMapDesignTypeChangeHandler = (GoogleMapDesignType) -> Unit + +interface GoogleMapViewController : + MapViewController, + GroundImageCapable, + PolygonCapable, + MarkerCapable, + PolylineCapable, + CircleCapable { + fun setMapDesignType(value: GoogleMapDesignType) + + fun setMapDesignTypeChangeListener(listener: GoogleMapDesignTypeChangeHandler) } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt new file mode 100644 index 00000000..f25603a4 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt @@ -0,0 +1,262 @@ +package com.mapconductor.googlemaps + +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.GoogleMap.CancelableCallback +import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener +import com.google.android.gms.maps.GoogleMap.OnCameraMoveCanceledListener +import com.google.android.gms.maps.GoogleMap.OnCameraMoveListener +import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener +import com.google.android.gms.maps.GoogleMap.OnMapClickListener +import com.google.android.gms.maps.model.LatLng +import com.mapconductor.core.circle.CircleEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.groundimage.GroundImageEvent +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.circle.GoogleMapCircleController +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController +import com.mapconductor.googlemaps.marker.GoogleMapMarkerController +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonController +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class GoogleMapViewControllerImpl( + override val holder: GoogleMapViewHolder, + private val markerController: GoogleMapMarkerController, + private val polylineController: GoogleMapPolylineController, + private val polygonController: GoogleMapPolygonController, + private val groundImageController: GoogleMapGroundImageController, + private val circleController: GoogleMapCircleController, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : BaseMapViewController(), + GoogleMapViewController, + OnCameraMoveStartedListener, + OnCameraMoveCanceledListener, + OnCameraMoveListener, + OnCameraIdleListener, + OnMapClickListener, + GoogleMap.OnMapLoadedCallback { + init { + setupListeners() + } + + fun setupListeners() { + holder.map.setOnCameraMoveStartedListener(this) + holder.map.setOnCameraMoveCanceledListener(this) + holder.map.setOnCameraMoveListener(this) + holder.map.setOnCameraIdleListener(this) + holder.map.setOnMapClickListener(this) + holder.map.setOnMapLoadedCallback(this) + } + + override fun moveCamera( + dstPosition: MapCameraPosition, + listener: MapViewState.MoveCameraCallback?, + ) { + coroutine.launch { + val dstCameraPosition = dstPosition.toCameraPosition() + val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) + holder.map.moveCamera(cameraUpdate) + listener?.onComplete() + } + } + + override fun animateCamera( + dstPosition: MapCameraPosition, + duration: Long, + listener: MapViewState.MoveCameraCallback?, + ) { + val dstCameraPosition = dstPosition.toCameraPosition() + coroutine.launch { + val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) + holder.map.animateCamera( + cameraUpdate, + duration.toInt(), + object : CancelableCallback { + override fun onCancel() { + listener?.onComplete() + } + + override fun onFinish() { + listener?.onComplete() + } + }, + ) + } + } + + override suspend fun clearOverlays() { + markerController.clear() + groundImageController.clear() + polylineController.clear() + polygonController.clear() + circleController.clear() + } + + override suspend fun compositionMarkers(data: List) = markerController.add(data) + + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) + + override suspend fun compositionCircles(data: List) = circleController.add(data) + + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } + + override suspend fun compositionPolylines(data: List) = polylineController.add(data) + + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + + override fun onCameraMove() { + cameraMoveCallback?.let { + val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() + coroutine.launch { it(mapCameraPosition) } + } + } + + override fun onCameraIdle() { + cameraMoveCallback?.let { + val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() + coroutine.launch { it(mapCameraPosition) } + } + } + + override fun onCameraMoveStarted(p0: Int) { + cameraMoveCallback?.let { + val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() + coroutine.launch { it(mapCameraPosition) } + } + } + + override fun onCameraMoveCanceled() { + cameraMoveCallback?.let { + val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() + coroutine.launch { it(mapCameraPosition) } + } + } + + override fun onMapClick(position: LatLng) { + val touchPosition = position.toGeoPoint() + + circleController.find(touchPosition)?.let { entity -> + val event = + CircleEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + circleController.clickListener?.invoke(event) + } + return + } + + groundImageController.find(touchPosition)?.let { entity -> + val event = + GroundImageEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + groundImageController.clickListener?.invoke(event) + } + return + } + + polygonController.find(touchPosition)?.let { entity -> + val event = + PolygonEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + polygonController.clickListener?.invoke(event) + } + return + } + + mapClickCallback?.let { + coroutine.launch { it(position.toGeoPoint()) } + } + } + + override suspend fun compositionGroundImages(data: List) = groundImageController.add(data) + + override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) + + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + this.markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + this.markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + this.markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + this.markerController.clickListener = listener + } + + override fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { + this.groundImageController.clickListener = listener + } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + this.polylineController.clickListener = listener + } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + this.polygonController.clickListener = listener + } + + private var _mapDesignType: GoogleMapDesignType = GoogleMapDesign.None + private var _mapDesignTypeChangeListener: GoogleMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: GoogleMapDesignType) { + coroutine.launch { + holder.map.mapType = value.getValue() + } + _mapDesignType = value + _mapDesignTypeChangeListener?.invoke(value) + } + + override fun setMapDesignTypeChangeListener(listener: GoogleMapDesignTypeChangeHandler) { + _mapDesignTypeChangeListener = listener + listener(_mapDesignType) + } + + override fun onMapLoaded() { + val mapDesignType = GoogleMapDesign.toMapDesignType(holder.map.mapType) + _mapDesignTypeChangeListener?.invoke(mapDesignType) + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index ced55963..adba5359 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -3,22 +3,33 @@ package com.mapconductor.googlemaps import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.MapView -import com.mapconductor.core.groundimage.GroundImageController +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder -import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.googlemaps.circle.GoogleMapCircleController +import com.mapconductor.googlemaps.circle.GoogleMapCircleOverlayRenderer +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageOverlayRenderer +import com.mapconductor.googlemaps.marker.GoogleMapMarkerController +import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonController +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonOverlayRenderer +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper typealias GoogleMapViewHolder = MapViewHolder -object GoogleMapViewControllerStore : StaticHolder() { +object GoogleMapViewControllerStore : StaticHolder() { suspend fun getOrCreate( context: Context, id: String, options: GoogleMapOptions, - ): GoogleMapViewController { + ): GoogleMapViewControllerImpl { val existing = this.get(id) if (existing != null) { return existing @@ -30,25 +41,93 @@ object GoogleMapViewControllerStore : StaticHolder() { options = options, ) - val groundImageRenderer = - GoogleMapGroundImageRenderer( + val controller = + GoogleMapViewControllerImpl( + markerController = getMarkerController(holder), + groundImageController = getGroundImageController(holder), + polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), + circleController = getCircleController(holder), holder = holder, ) + this.set(id, controller) + + return controller + } - val groundImageController = - GroundImageController( - renderer = groundImageRenderer, + private fun getPolygonController(holder: GoogleMapViewHolder): GoogleMapPolygonController { + val renderer = + GoogleMapPolygonOverlayRenderer( + holder = holder, ) val controller = - GoogleMapViewController( - groundImageController = groundImageController, + GoogleMapPolygonController( + renderer = renderer, + ) + return controller + } + + private fun getGroundImageController(holder: GoogleMapViewHolder): GoogleMapGroundImageController { + val renderer = + GoogleMapGroundImageOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapGroundImageController( + renderer = renderer, + ) + return controller + } + + private fun getCircleController(holder: GoogleMapViewHolder): GoogleMapCircleController { + val renderer = + GoogleMapCircleOverlayRenderer( holder = holder, ) - this.set(id, controller) + val controller = + GoogleMapCircleController( + renderer = renderer, + ) + return controller + } + + private fun getPolylineController(holder: GoogleMapViewHolder): GoogleMapPolylineController { + val renderer = + GoogleMapPolylineOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapPolylineController( + renderer = renderer, + ) return controller } + + private fun getMarkerController(holder: GoogleMapViewHolder): GoogleMapMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + GoogleMapMarkerRenderer( + holder = holder, + ) + + val markerController = + GoogleMapMarkerController( + markerManager = manager, + renderer = renderer, + ) + + return markerController + } } internal fun Context.findActivity(): Activity? = diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt similarity index 56% rename from mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt rename to mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index d1da3939..eb708ea3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -18,27 +18,44 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IGoogleMapViewState : MapViewState +interface GoogleMapViewState : MapViewState -class GoogleMapViewState( +class GoogleMapViewStateImpl( override val id: String, - override var mapDesignType: GoogleMapDesign, + mapDesignType: GoogleMapDesignType, override val initCameraPosition: MapCameraPosition, -) : MapViewStateImpl(), - IGoogleMapViewState { +) : MapViewStateImpl(), + GoogleMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() // Camera position - private val _cameraPosition = MutableStateFlow(initCameraPosition) + private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - internal var controller: IGoogleMapViewController? = null + private var _mapDesignType: GoogleMapDesignType = mapDesignType - override fun changeMapDesignType(value: GoogleMapDesign) { - this.mapDesignType = value - this.controller?.changeMapDesign(value.getValue()) + override var mapDesignType: GoogleMapDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType + private var controller: GoogleMapViewController? = null + + internal fun setController(controller: GoogleMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: GoogleMapDesignType) { + _mapDesignType = value } override fun moveCameraTo( @@ -47,81 +64,66 @@ class GoogleMapViewState( listener: MapViewState.MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() return } - val currCameraPosition = this.cameraPosition.value + val currentPosition = this.cameraPosition.value val newPosition = - currCameraPosition.copy( + currentPosition.copy( position = position, ) this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): GoogleMapViewHolder? = controller?.holder as? GoogleMapViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - controller?.let { - if (durationMs == 0L) { - it.moveCamera(dstCameraPosition, listener) - } else { - it.animateCamera(dstCameraPosition, durationMs.toInt(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return } - } ?: listener?.onComplete(false) + } + _cameraPosition.value = cameraPosition + listener?.onComplete() } internal fun onCameraChange(cameraPosition: MapCameraPosition) { this._cameraPosition.value = cameraPosition } - -// override fun onCameraMoveStart(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onCameraMove(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onCameraMoveEnd(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onMarkerAdd(state: MarkerState) { -// // Do nothing here -// } -// -// override fun onMarkerRemove(id: String) { -// // Do nothing here -// } } // GoogleMapViewSaver implementation -class GoogleMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: GoogleMapViewState): MapCameraPosition? = state.cameraPosition.value +class GoogleMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: GoogleMapViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: GoogleMapViewState, + state: GoogleMapViewStateImpl, bundle: Bundle, ) { - bundle.putInt("id", state.mapDesignType.id) + bundle.putInt("id", state.mapDesignType?.id ?: GoogleMapDesign.None.id) } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): GoogleMapViewState = - GoogleMapViewState( + ): GoogleMapViewStateImpl = + GoogleMapViewStateImpl( id = stateId, mapDesignType = GoogleMapDesign.Create( @@ -130,14 +132,14 @@ class GoogleMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: GoogleMapViewState): String = state.id + override fun getStateId(state: GoogleMapViewStateImpl): String = state.id } @Composable fun rememberGoogleMapViewState( mapDesign: GoogleMapDesign = GoogleMapDesign.Normal, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): GoogleMapViewState { +): GoogleMapViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -147,7 +149,7 @@ fun rememberGoogleMapViewState( stateSaver = GoogleMapViewSaver().createSaver(), ) { mutableStateOf( - GoogleMapViewState( + GoogleMapViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt new file mode 100644 index 00000000..013bd86f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualCircle + +class GoogleMapCircleController( + circleManager: CircleManager = CircleManagerImpl(), + renderer: GoogleMapCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt new file mode 100644 index 00000000..7ba268f8 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt @@ -0,0 +1,72 @@ +package com.mapconductor.googlemaps.circle + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.CircleOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.googlemaps.GoogleMapActualCircle +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapCircleOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): GoogleMapActualCircle? = + withContext(coroutine.coroutineContext) { + val center = GeoPoint.from(state.center).toLatLng() + val options = + CircleOptions() + .center(center) + .radius(state.radiusMeters) + .strokeColor(state.strokeColor.toArgb()) + .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .fillColor(state.fillColor.toArgb()) + .clickable(false) + holder.map.addCircle(options).also { + it.tag = state.id + } + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + entity.circle.remove() + } + } + + override suspend fun updateCircleProperties( + circle: GoogleMapActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): GoogleMapActualCircle? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.center != prevFinger.center) { + circle.center = GeoPoint.from(current.state.center).toLatLng() + } + if (finger.radiusMeters != prevFinger.radiusMeters) { + circle.radius = current.state.radiusMeters + } + if (finger.strokeColor != prevFinger.strokeColor) { + circle.strokeColor = + current.state.strokeColor.toArgb() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + circle.strokeWidth = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + if (finger.fillColor != prevFinger.fillColor) { + circle.fillColor = + current.state.fillColor.toArgb() + } + circle + } +} 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 deleted file mode 100644 index 657fa7f6..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.mapconductor.googlemaps.circle - -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Circle -import com.google.android.gms.maps.model.CircleOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultGoogleMapCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class GoogleMapCircleRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - override suspend fun addCircles(newCircles: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newCircles.map { state -> - val center = GeoPoint.from(state.center).toLatLng() - val options = - CircleOptions() - .center(center) - .radius(state.radiusMeters) - .strokeColor(state.strokeColor.toArgb()) - .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .fillColor(state.fillColor.toArgb()) - .clickable(false) - holder.map.addCircle(options).also { - it.tag = state.id - } - } - } - } - - override suspend fun removeCircles(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { entity -> entity.circle.remove() } - } - } - - override suspend fun changeCircle(changes: List>): List { - Log.d("debug", "--->changeCircle") - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val circle = params.entity.circle - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - Log.d("debug", " ---> ${finger.hashCode()} = ${prevFinger.hashCode()}") - - if (finger.center != prevFinger.center) { - circle.center = GeoPoint.from(params.entity.state.center).toLatLng() - } - if (finger.radiusMeters != prevFinger.radiusMeters) { - circle.radius = params.entity.state.radiusMeters - } - if (finger.strokeColor != prevFinger.strokeColor) { - circle.strokeColor = - params.entity.state.strokeColor - .toArgb() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - circle.strokeWidth = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - } - if (finger.fillColor != prevFinger.fillColor) { - circle.fillColor = - params.entity.state.fillColor - .toArgb() - } - return@map circle - } - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt new file mode 100644 index 00000000..d3f9bcba --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.groundimage + +import com.mapconductor.core.groundimage.GroundImageController +import com.mapconductor.core.groundimage.GroundImageManager +import com.mapconductor.core.groundimage.GroundImageManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualGroundImage + +class GoogleMapGroundImageController( + groundImageManager: GroundImageManager = GroundImageManagerImpl(), + renderer: GoogleMapGroundImageOverlayRenderer, +) : GroundImageController(groundImageManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt new file mode 100644 index 00000000..b493eeac --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt @@ -0,0 +1,65 @@ +package com.mapconductor.googlemaps.groundimage + +import androidx.core.graphics.drawable.toBitmap +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.GroundOverlayOptions +import com.mapconductor.core.groundimage.AbstractGroundImageOverlayRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.googlemaps.GoogleMapActualGroundImage +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLngBounds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapGroundImageOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractGroundImageOverlayRenderer() { + override suspend fun createGroundImage(state: GroundImageState): GoogleMapActualGroundImage? = + withContext(coroutine.coroutineContext) { + val bounds = state.bounds.toLatLngBounds() ?: return@withContext null + val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) + val opacity = state.opacity + val options = + GroundOverlayOptions() + .image(image) + .positionFromBounds(bounds) + .transparency(1.0f - opacity) + holder.map.addGroundOverlay(options)?.also { + it.tag = state.id + } + } + + override suspend fun removeGroundImage(entity: GroundImageEntity) { + coroutine.launch { + entity.groundImage.remove() + } + } + + override suspend fun updateGroundImageProperties( + groundImage: GoogleMapActualGroundImage, + current: GroundImageEntity, + prev: GroundImageEntity, + ): GoogleMapActualGroundImage? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + if (finger.bounds != prevFinger.bounds) { + current.state.bounds.toLatLngBounds()?.let { + groundImage.setPositionFromBounds(it) + } + } + groundImage.transparency = 1.0f - current.state.opacity + if (finger.image != prevFinger.image) { + val bitmap = + current.state.image + .toBitmap() + val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) + groundImage.setImage(bitmapDesc) + } + groundImage + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt deleted file mode 100644 index 4d831a7b..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.mapconductor.googlemaps.groundimage - -import androidx.core.graphics.drawable.toBitmap -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import com.google.android.gms.maps.model.GroundOverlay -import com.google.android.gms.maps.model.GroundOverlayOptions -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.googlemaps.ActualGoogleMapGroundImage -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLngBounds -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class GoogleMapGroundImageRenderer( - val holder: GoogleMapViewHolder, - val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { state -> - val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() - val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) - val opacity = state.opacity - val options = - GroundOverlayOptions() - .image(image) - .positionFromBounds(bounds) - .transparency(1.0f - opacity) - holder.map.addGroundOverlay(options)?.also { - it.tag = state.id - } - } - } - } - - override suspend fun onRemove(data: List>) { - coroutine.launch { - data.forEach { params -> params.groundImage.remove() } - } - } - - override suspend fun onPostProcess() { - // Do nothing here - } - - override suspend fun onChange( - data: List>>, - ): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { params -> - val groundOverlay = params.current.groundImage - val finger = params.current.fingerPrint - val prevFinger = params.prev.fingerPrint - if (finger.bounds != prevFinger.bounds) { - params.current.state.bounds.toLatLngBounds()?.let { - groundOverlay.setPositionFromBounds(it) - } - } - groundOverlay.transparency = 1.0f - params.current.state.opacity - if (finger.image != prevFinger.image) { - val bitmap = - params.current.state.image - .toBitmap() - val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) - groundOverlay.setImage(bitmapDesc) - } - return@map groundOverlay - } - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt new file mode 100644 index 00000000..7ff3b3bc --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt @@ -0,0 +1,88 @@ +package com.mapconductor.googlemaps.marker + +import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener +import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.googlemaps.GoogleMapActualMarker +import com.mapconductor.googlemaps.toGeoPoint +import com.mapconductor.settings.Settings + +class GoogleMapMarkerController( + markerManager: MarkerManager, + override val renderer: GoogleMapMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ), + OnMarkerClickListener, + OnMarkerDragListener { + init { + renderer.holder.map.apply { + setOnMarkerClickListener(this@GoogleMapMarkerController) + setOnMarkerDragListener(this@GoogleMapMarkerController) + } + } + + override fun onMarkerClick(marker: GoogleMapActualMarker): Boolean { + val stateId = (marker.tag as? String) ?: return false + markerManager.getEntity(stateId)?.also { entity -> + if (!entity.state.clickable) return true + clickListener?.invoke(entity.state) + } + return true + } + + override fun onMarkerDrag(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + + // Suppress the recomposition for the position property + setDraggingState(entity.state, true) + + entity.state.position = marker.position.toGeoPoint() + dragListener?.invoke(entity.state) + } + } + + override fun onMarkerDragEnd(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + entity.state.position = marker.position.toGeoPoint() + dragEndListener?.invoke(entity.state) + } + } + + override fun onMarkerDragStart(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + entity.state.position = marker.position.toGeoPoint() + // Restore the recomposition for the position property + setDraggingState(entity.state, false) + dragStartListener?.invoke(entity.state) + } + } + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = + renderer.holder.map.cameraPosition.zoom + .toDouble() + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt index 36e950d7..47dc7350 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt @@ -4,95 +4,80 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer +import com.mapconductor.googlemaps.GoogleMapActualMarker import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLng import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultGoogleMapMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onRemove = onIconRemove, - onAdd = onIconAdd, - onChange = onIconChange, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class GoogleMapMarkerRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { + holder: GoogleMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer( + holder = holder, + coroutine = coroutine, + ) { override fun setMarkerPosition( - markerEntity: MarkerEntity, + markerEntity: MarkerEntity, position: GeoPoint, ) { markerEntity.marker.position = position.toLatLng() } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - newMarkers.map { params -> - val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.second.bitmap) + data.map { params -> + val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.bitmapIcon.bitmap) val options = MarkerOptions() - .position(GeoPoint.from(params.first.position).toLatLng()) + .position(GeoPoint.from(params.state.position).toLatLng()) .anchor( - params.second.anchor.x, - params.second.anchor.y, + params.bitmapIcon.anchor.x, + params.bitmapIcon.anchor.y, ).icon(bitmapDescriptor) - .draggable(params.first.draggable) + .draggable(params.state.draggable) val marker = holder.map.addMarker(options)?.also { - it.tag = params.first.id + it.tag = params.state.id } return@map marker } } } - override suspend fun removeIcons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - removeEntities.forEach { params -> params.marker.remove() } + data.forEach { params -> params.marker.remove() } } } - override suspend fun changeIcons(changes: List>): List = + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + changes: List>, + ): List = withContext(coroutine.coroutineContext) { changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currentFinger = params.entity.fingerPrint + val prevFinger = params.prev.fingerPrint + val currentFinger = params.current.fingerPrint if (prevFinger.icon != currentFinger.icon) { val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.bitmapIcon.bitmap) - params.entity.marker.setIcon(bitmapDescriptor) + params.current.marker.setIcon(bitmapDescriptor) } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.position = - GeoPoint.from(params.entity.state.position).toLatLng() + if (params.current.state.position != params.prev.state.position) { + params.current.marker.position = + GeoPoint.from(params.current.state.position).toLatLng() } // Google Mapsはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker + params.current.marker } } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt new file mode 100644 index 00000000..c114fa63 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.polygon + +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualPolygon + +class GoogleMapPolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + renderer: GoogleMapPolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt new file mode 100644 index 00000000..0bf3acc7 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt @@ -0,0 +1,67 @@ +package com.mapconductor.googlemaps.polygon + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.PolygonOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.googlemaps.GoogleMapActualPolygon +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapPolygonOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolygonOverlayRenderer() { + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + entity.polygon.remove() + } + } + + override suspend fun createPolygon(state: PolygonState) = + withContext(coroutine.coroutineContext) { + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolygonOptions() + .addAll(points) + .strokeColor(state.strokeColor.toArgb()) + .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .fillColor(state.fillColor.toArgb()) + .clickable(true) + holder.map.addPolygon(options)?.also { + it.tag = state.id + } + } + + override suspend fun updatePolygonProperties( + polygon: GoogleMapActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): GoogleMapActualPolygon? = + withContext(coroutine.coroutineContext) { + val polygon = current.polygon + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + Log.d("GoogleMaps", "----->$finger, $prevFinger") + if (finger.points != prevFinger.points) { + val points = + current.state.points + .map { GeoPoint.from(it).toLatLng() } + polygon.points = points + } + polygon.strokeWidth = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + polygon.strokeColor = + current.state.strokeColor.toArgb() + polygon.fillColor = + current.state.fillColor.toArgb() + polygon + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt deleted file mode 100644 index 8fedaffa..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.mapconductor.googlemaps.polygon - -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Polygon -import com.google.android.gms.maps.model.PolygonOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultGoogleMapPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class GoogleMapPolygonRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newPolygons.map { state -> - val points = state.points.map { GeoPoint.from(it).toLatLng() } - val options = - PolygonOptions() - .addAll(points) - .strokeColor(state.strokeColor.toArgb()) - .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .fillColor(state.fillColor.toArgb()) - .clickable(true) - holder.map.addPolygon(options).also { - it.tag = state.id - } - } - } - } - - override suspend fun removePolygons(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { params -> params.polygon.remove() } - } - } - - override suspend fun changePolygon(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val polygon = params.entity.polygon - 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() } - polygon.points = points - } - polygon.strokeWidth = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - polygon.strokeColor = - params.entity.state.strokeColor - .toArgb() - polygon.fillColor = - params.entity.state.fillColor - .toArgb() - return@map polygon - } - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt new file mode 100644 index 00000000..8425fc69 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.polyline + +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualPolyline + +class GoogleMapPolylineController( + polylineManager: PolylineManager = PolylineManagerImpl(), + renderer: GoogleMapPolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt new file mode 100644 index 00000000..7c18076f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt @@ -0,0 +1,73 @@ +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.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.GoogleMapActualPolyline +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapPolylineOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): GoogleMapActualPolyline? = + withContext(coroutine.coroutineContext) { + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolylineOptions() + .addAll(points) + .color(state.strokeColor.toArgb()) + .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .geodesic(state.geodesic) + .clickable(true) + + holder.map.addPolyline(options).also { + it.tag = state.id + } + } + + override suspend fun updatePolylineProperties( + polyline: GoogleMapActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): Polyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val points = current.state.points.map { GeoPoint.from(it).toLatLng() } + polyline.points = points + } + + if (finger.geodesic != prevFinger.geodesic) { + polyline.isGeodesic = current.state.geodesic + } + + if (finger.strokeWidth != prevFinger.strokeWidth) { + polyline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + + if (finger.strokeColor != prevFinger.strokeColor) { + polyline.color = current.state.strokeColor.toArgb() + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + entity.polyline.remove() + } + } +} 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 deleted file mode 100644 index 43c9cb3f..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt +++ /dev/null @@ -1,85 +0,0 @@ -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()) - .geodesic(state.geodesic) - .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 - } - polyline.isGeodesic = params.entity.state.geodesic - 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/HereMapTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt deleted file mode 100644 index dc757271..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mapconductor.here - -import com.here.sdk.mapview.MapMarker -import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline - -typealias HereMapActualMarker = MapMarker -typealias HereMapActualCircle = MapPolygon -typealias HereMapActualPolyline = MapPolyline -typealias HereMapActualPolygon = MapPolygon 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 7cdf8e8d..fdc30e26 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 @@ -10,11 +10,11 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler 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.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import android.util.Log import android.view.ViewGroup @@ -24,23 +24,23 @@ import kotlinx.coroutines.suspendCancellableCoroutine @OptIn(ExperimentalCoroutinesApi::class) @Composable fun HereMapView( - state: IHereMapViewState, + state: HereViewStateImpl, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, - content: (@Composable HereMapViewScope.() -> Unit)? = null, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, + content: (@Composable HereViewScope.() -> Unit)? = null, ) { - val holderRef = remember { Ref() } - val scope = remember { HereMapViewScope() } - val controllerRef = remember { Ref() } + val holderRef = remember { Ref() } + val scope = remember { HereViewScope() } + val controllerRef = remember { Ref() } val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle val registry = remember { scope.buildRegistry() } @@ -57,7 +57,7 @@ fun HereMapView( HereMapViewControllerStore.initSDK(context) val mapInitOptions = - HereMapViewInitOptions( + HereViewInitOptions( scheme = state.mapDesignType.getValue(), ) @@ -68,23 +68,20 @@ fun HereMapView( options = mapInitOptions, ) - (state as? HereMapViewState)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + controller.setCameraMoveListener(state::onCameraChange) + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) - controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - - (state as? HereMapViewState)?.controller = controller + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) + controller.setOnCircleClickListener(onCircleClick) + controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) + state.setController(controller) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) controller.holder.mapView.mapScene.loadScene(state.mapDesignType.getValue()) { mapError -> if (mapError != null) { @@ -96,13 +93,13 @@ fun HereMapView( controllerRef.value = controller return@MapViewBase suspendCancellableCoroutine { cont -> - val restoreCameraPosition = state.cameraPosition.value ?: state.initCameraPosition + val restoreCameraPosition = state.cameraPosition.value controller.moveCamera( dstPosition = restoreCameraPosition, listener = object : MapViewState.MoveCameraCallback { - override fun onComplete(result: Boolean) { - cont.resume(result) { } + override fun onComplete() { + cont.resume(true) { } } }, ) 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 b8f839a7..f4b7d456 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,358 +1,19 @@ -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.MapScheme -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.circle.CircleCapable 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 changeMapDesign(value: MapScheme) - - 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< - 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 changeMapDesign(value: MapScheme) { - coroutine.launch { - holder.mapView.mapScene.loadScene(value) {} - } - } - - 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, - ) - - cameraMoveCallback?.let { - val mapCameraPosition = correctCameraState.toMapCameraPosition() - it(mapCameraPosition) - } - } - - 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) { - markerClickCallback?.invoke(entity.state) - return - } - - circleOverlayManager.find(touchPosition)?.let { entity -> - val event = - CircleClickEvent( - state = entity.state, - position = touchPosition, - ) - circleClickCallback?.invoke(event) - return - } - - // If no overlay is processed, process the tap as onMapClick - mapClickCallback?.invoke(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) - - markerDragStartCallback?.invoke(entity.state) - } - - GestureState.UPDATE.value -> { - selectedMarker?.also { selected -> - holder.mapView.viewToGeoCoordinates(point)?.also { coordinates -> - selected.marker.coordinates = coordinates - selected.state.position = coordinates.toGeoPoint() - } - markerDragCallback?.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) - - markerDragEndCallback?.invoke(selected.state) - selectedMarker = null - } - } - } - } - - private fun getGeoPointFromPoint(point: Point2D): GeoPoint? = - holder.mapView - .viewToGeoCoordinates(point) - ?.toGeoPoint() +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable +import com.mapconductor.here.HereMapDesignType + +typealias HereMapDesignTypeChangeHandler = (HereMapDesignType) -> Unit + +interface HereMapViewController : + MapViewController, + MarkerCapable, + PolygonCapable, + PolylineCapable, + CircleCapable { + fun setMapDesignType(value: HereMapDesignType) + + fun setMapDesignTypeChangeListener(listener: HereMapDesignTypeChangeHandler) } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt new file mode 100644 index 00000000..90e619b8 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt @@ -0,0 +1,288 @@ +package com.mapconductor.here + +import HereMapDesignTypeChangeHandler +import HereMapViewController +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.MapMeasure +import com.here.sdk.mapview.MapScene +import com.here.sdk.mapview.MapView +import com.here.time.Duration +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.CircleEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.core.map.MapViewState.MoveCameraCallback +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.here.circle.HereCircleController +import com.mapconductor.here.marker.HereMarkerController +import com.mapconductor.here.polygon.HerePolygonController +import com.mapconductor.here.polyline.HerePolylineController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class HereMapViewControllerImpl( + private val markerController: HereMarkerController, + private val polylineController: HerePolylineController, + private val polygonController: HerePolygonController, + private val circleController: HereCircleController, + override val holder: MapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : BaseMapViewController(), + CircleCapable, + HereMapViewController, + MapCameraListener, + TapListener, + LongPressListener { + companion object { + private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 + } + + override suspend fun clearOverlays() { + markerController.clear() + polylineController.clear() + polygonController.clear() + circleController.clear() + } + + override suspend fun compositionMarkers(data: List) = markerController.add(data) + + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + markerController.clickListener = listener + } + + override suspend fun compositionCircles(data: List) = circleController.add(data) + + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } + + override suspend fun compositionPolylines(data: List) = polylineController.add(data) + + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + + init { + setupListeners() + } + + 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() + } + + 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() + AnimationState.CANCELLED -> listener?.onComplete() + } + } + } + } + + 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, + ) + + cameraMoveCallback?.let { + val mapCameraPosition = correctCameraState.toMapCameraPosition() + it(mapCameraPosition) + } + } + + override fun onTap(point: Point2D) { + val touchPosition = this.getGeoPointFromPoint(point) ?: return + + markerController.find(touchPosition)?.let { entity -> + markerController.clickListener?.invoke(entity.state) + return + } + + circleController.find(touchPosition)?.let { entity -> + val event = + CircleEvent( + state = entity.state, + clicked = touchPosition, + ) + circleController.clickListener?.invoke(event) + return + } + + polygonController.find(touchPosition)?.let { entity -> + val event = + PolygonEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + polygonController.clickListener?.invoke(event) + } + return + } + + // If no overlay is processed, process the tap as onMapClick + mapClickCallback?.invoke(touchPosition) + } + + override fun onLongPress( + gesture: GestureState, + point: Point2D, + ) { + val position = this.getGeoPointFromPoint(point) ?: return + + when (gesture.value) { + GestureState.BEGIN.value -> { + markerController.find(position)?.let { entity -> + if (entity.state.draggable) { + entity.state.position = position + markerController.selectedMarker = entity + markerController.dragStartListener?.invoke(entity.state) + return + } + } + mapLongClickCallback?.invoke(position) + } + + GestureState.UPDATE.value -> { + markerController.selectedMarker?.also { selected -> + holder.mapView.viewToGeoCoordinates(point)?.also { coordinates -> + selected.marker.coordinates = coordinates + selected.state.position = coordinates.toGeoPoint() + } + markerController.dragListener?.invoke(selected.state) + } + } + + GestureState.END.value, GestureState.CANCEL.value -> { + markerController.selectedMarker?.also { selected -> + markerController.markerManager.updateEntity(selected) + markerController.dragEndListener?.invoke(selected.state) + markerController.selectedMarker = null + markerController.selectedMarker = null + } + } + } + } + + private fun getGeoPointFromPoint(point: Point2D): GeoPoint? = + holder.mapView + .viewToGeoCoordinates(point) + ?.toGeoPoint() + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + polylineController.clickListener = listener + } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + polygonController.clickListener = listener + } + + private var _mapDesignType: HereMapDesignType = HereMapDesign.NormalDay + private var _mapDesignTypeChangeListener: HereMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: HereMapDesignType) { + val scene = value.getValue() + coroutine.launch { + holder.mapView.mapScene.loadScene(scene) { + _mapDesignType = value + _mapDesignTypeChangeListener?.invoke(value) + } + } + } + + override fun setMapDesignTypeChangeListener(listener: HereMapDesignTypeChangeHandler) { + _mapDesignTypeChangeListener = listener + listener(_mapDesignType) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt new file mode 100644 index 00000000..5184eadf --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt @@ -0,0 +1,10 @@ +package com.mapconductor.here + +import com.here.sdk.mapview.MapMarker +import com.here.sdk.mapview.MapPolygon +import com.here.sdk.mapview.MapPolyline + +typealias HereActualMarker = MapMarker +typealias HereActualCircle = MapPolygon +typealias HereActualPolyline = MapPolyline +typealias HereActualPolygon = MapPolygon diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt similarity index 54% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt index a1c7e8c3..aa4efbe7 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt @@ -1,18 +1,29 @@ package com.mapconductor.here +import HerePolygonOverlayRenderer import com.here.sdk.core.engine.AuthenticationMode import com.here.sdk.core.engine.SDKNativeEngine import com.here.sdk.core.engine.SDKOptions import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.here.circle.HereCircleController +import com.mapconductor.here.circle.HereCircleOverlayRenderer +import com.mapconductor.here.marker.HereMarkerController +import com.mapconductor.here.marker.HereMarkerRenderer +import com.mapconductor.here.polygon.HerePolygonController +import com.mapconductor.here.polyline.HerePolylineController +import com.mapconductor.here.polyline.HerePolylineOverlayRenderer import android.content.Context import android.content.pm.PackageManager -typealias HereMapViewHolder = MapViewHolder +typealias HereViewHolder = MapViewHolder -object HereMapViewControllerStore : StaticHolder() { +object HereMapViewControllerStore : StaticHolder() { private var mapCount: Int = 0 fun initSDK(context: Context) { @@ -47,8 +58,8 @@ object HereMapViewControllerStore : StaticHolder() { fun getOrCreate( context: Context, id: String, - options: HereMapViewInitOptions, - ): HereMapViewController { + options: HereViewInitOptions, + ): HereMapViewControllerImpl { val existing = this.get(id) if (existing != null) { return existing @@ -56,7 +67,7 @@ object HereMapViewControllerStore : StaticHolder() { initSDK(context.applicationContext) val holder = - HereMapViewHolderImpl.create( + HereViewHolderImpl.create( context.applicationContext, ) @@ -79,13 +90,77 @@ object HereMapViewControllerStore : StaticHolder() { // } val controller = - HereMapViewController( + HereMapViewControllerImpl( holder = holder, + markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), + circleController = getHereCircleController(holder), ) this.set(id, controller) return controller } + private fun getPolylineController(holder: HereViewHolder): HerePolylineController { + val renderer = + HerePolylineOverlayRenderer( + holder = holder, + ) + + val controller = + HerePolylineController( + renderer = renderer, + ) + return controller + } + + private fun getMarkerController(holder: HereViewHolder): HereMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + HereMarkerRenderer( + holder = holder, + ) + + val controller = + HereMarkerController( + markerManager = manager, + renderer = renderer, + ) + return controller + } + + private fun getHereCircleController(holder: HereViewHolder): HereCircleController { + val renderer = + HereCircleOverlayRenderer( + holder = holder, + ) + + val controller = + HereCircleController( + renderer = renderer, + ) + return controller + } + + private fun getPolygonController(holder: HereViewHolder): HerePolygonController { + val renderer = + HerePolygonOverlayRenderer( + holder = holder, + ) + + val controller = + HerePolygonController( + renderer = renderer, + ) + return controller + } + // fun release() { // mapCount-- // if (mapCount > 0) return diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt similarity index 93% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt index 8f28d72f..0a21d727 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt @@ -11,7 +11,7 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.map.MapViewHolder import android.content.Context -internal class HereMapViewHolderImpl private constructor( +internal class HereViewHolderImpl private constructor( override val mapView: MapView, ) : MapViewHolder { override lateinit var map: MapScene @@ -48,7 +48,7 @@ internal class HereMapViewHolderImpl private constructor( onResume() } - val holder = HereMapViewHolderImpl(mapView) + val holder = HereViewHolderImpl(mapView) holder.map = mapView.mapScene return holder } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt similarity index 77% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt index 5671a4a3..1ac85769 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt @@ -2,6 +2,6 @@ package com.mapconductor.here import com.here.sdk.mapview.MapScheme -data class HereMapViewInitOptions( +data class HereViewInitOptions( val scheme: MapScheme = MapScheme.NORMAL_DAY, ) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt similarity index 64% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt index 22a4984d..9fa4ab94 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt @@ -2,4 +2,4 @@ package com.mapconductor.here import com.mapconductor.core.MapViewScope -class HereMapViewScope : MapViewScope() +class HereViewScope : MapViewScope() diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt similarity index 61% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt index 397257be..bd282a3e 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt @@ -1,5 +1,6 @@ package com.mapconductor.here +import HereMapViewController import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,24 +24,29 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IHereMapViewState : MapViewState +interface HereViewState : MapViewState -class HereMapViewState( +class HereViewStateImpl( override val id: String, - override var mapDesignType: HereMapDesignType, + mapDesignType: HereMapDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), - IHereMapViewState { - internal var controller: IHereMapViewController? = null + HereViewState { + private var controller: HereMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - - override fun changeMapDesignType(value: HereMapDesignType) { - this.mapDesignType = value - this.controller?.changeMapDesign(value.getValue()) - } + private var _mapDesignType: HereMapDesignType = mapDesignType + + override var mapDesignType: HereMapDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType override fun moveCameraTo( position: GeoPoint, @@ -48,51 +54,66 @@ class HereMapViewState( listener: MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() return } - val currCameraPosition = this.cameraPosition.value + val currentPosition = this.cameraPosition.value val newPosition = - currCameraPosition.copy( + currentPosition.copy( position = position, ) this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): HereViewHolder? = controller?.holder as? HereViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return + } } + _cameraPosition.value = cameraPosition + listener?.onComplete() + } - if (controller == null) { - listener?.onComplete(false) - return - } + internal fun onCameraChange(cameraState: MapCameraPosition) { + this._cameraPosition.value = cameraState + } - if (durationMs == 0L) { - controller!!.moveCamera(cameraPosition, listener) - } else { - controller!!.animateCamera(cameraPosition, durationMs.toLong(), listener) + internal fun setController(controller: HereMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) } + controller.moveCamera(_cameraPosition.value) } - internal fun onCameraChange(cameraState: MapCameraPosition) { - this._cameraPosition.value = cameraState + internal fun onMapDesignTypeChange(value: HereMapDesignType) { + _mapDesignType = value } } -class HereMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: HereMapViewState): MapCameraPosition? = state.cameraPosition.value +class HereMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: HereViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: HereMapViewState, + state: HereViewStateImpl, bundle: Bundle, ) { bundle.putInt("id", state.mapDesignType.getValue().value) @@ -102,8 +123,8 @@ class HereMapViewSaver : BaseMapViewSaver() { stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): HereMapViewState = - HereMapViewState( + ): HereViewStateImpl = + HereViewStateImpl( id = stateId, mapDesignType = HereMapDesign.CreateById( @@ -114,14 +135,14 @@ class HereMapViewSaver : BaseMapViewSaver() { override fun getCameraPaddings(): MapPaddings? = MapPaddingsImpl.Zeros - override fun getStateId(state: HereMapViewState): String = state.id + override fun getStateId(state: HereViewStateImpl): String = state.id } @Composable fun rememberHereMapViewState( mapDesign: HereMapDesign = HereMapDesign.NormalDay, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): HereMapViewState { +): HereViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -131,7 +152,7 @@ fun rememberHereMapViewState( stateSaver = HereMapViewSaver().createSaver(), ) { mutableStateOf( - HereMapViewState( + HereViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt new file mode 100644 index 00000000..09342ef9 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt @@ -0,0 +1,10 @@ +package com.mapconductor.here.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManagerImpl +import com.mapconductor.here.HereActualCircle + +class HereCircleController( + circleManager: CircleManagerImpl = CircleManagerImpl(), + renderer: HereCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt new file mode 100644 index 00000000..ec263a65 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt @@ -0,0 +1,147 @@ +package com.mapconductor.here.circle + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoCircle +import com.here.sdk.core.GeoCoordinates +import com.here.sdk.core.GeoPolygon +import com.here.sdk.mapview.MapPolygon +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.here.HereActualCircle +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlin.math.cos +import kotlin.math.sin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HereCircleOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): HereActualCircle? { + val geoCircle = createCirclePolygon(state) + val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) + val mapCircle = + MapPolygon( + geoCircle, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) + coroutine.launch { + holder.map.addMapPolygon(mapCircle) + } + return mapCircle + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + holder.map.removeMapPolygon(entity.circle) + } + } + + override suspend fun updateCircleProperties( + circle: HereActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): HereActualCircle? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + // Update geometry if center or radius changed + if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { + val geoCircle = createCirclePolygon(current.state) + current.circle.geometry = geoCircle + } + + // Update stroke color + if (finger.strokeColor != prevFinger.strokeColor) { + current.circle.outlineColor = + Color.valueOf( + current.state.strokeColor + .toArgb(), + ) + } + + // Update stroke width + if (finger.strokeWidth != prevFinger.strokeWidth) { + val lineWidth = + ResourceProvider.dpToPx( + current.state.strokeWidth.value + .toDouble(), + ) + current.circle.outlineWidth = lineWidth + } + + // Update fill color + if (finger.fillColor != prevFinger.fillColor) { + current.circle.fillColor = + Color.valueOf( + current.state.fillColor + .toArgb(), + ) + } + current.circle.outlineWidth = + current.state.strokeWidth.value + .toDouble() + + circle + } + + /** + * Creates a circle that approximates a circle by generating points around the circumference + */ + private fun createCirclePolygon(state: CircleState): GeoPolygon { + val center = GeoPoint.from(state.center).toGeoCoordinates() + // val radiusMeters = state.radiusMeters + +// val points = mutableListOf() +// +// // Generate points around the circle +// for (i in 0 until CIRCLE_POINT_COUNT) { +// val angle = 2.0 * PI * i / CIRCLE_POINT_COUNT +// val point = calculateCirclePoint(center, radiusMeters, angle) +// points.add(point) +// } +// +// // Close the circle by adding the first point at the end +// if (points.isNotEmpty()) { +// points.add(points.first()) +// } + val geoCircle = GeoCircle(center, state.radiusMeters) + val geoPolygon = GeoPolygon(geoCircle) + + return geoPolygon + } + + /** + * Calculate a point on a circle given center, radius and angle + * Uses approximate conversion from meters to degrees for small circles + */ + private fun calculateCirclePoint( + center: GeoCoordinates, + radiusMeters: Double, + angleRadians: Double, + ): GeoCoordinates { + // Approximate conversion: 1 degree latitude ≈ 111,320 meters + // Longitude conversion varies by latitude, use cosine correction + val latDegrees = radiusMeters / 111320.0 + val lonDegrees = radiusMeters / (111320.0 * cos(Math.toRadians(center.latitude))) + + val deltaLat = latDegrees * cos(angleRadians) + val deltaLon = lonDegrees * sin(angleRadians) + + return GeoCoordinates( + center.latitude + deltaLat, + center.longitude + deltaLon, + ) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt deleted file mode 100644 index b30319c9..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.mapconductor.here.circle - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoCoordinates -import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.MapPolygon -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultHereMapCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class HereMapCircleRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - companion object { - // Number of points to approximate a circle - more points = smoother circle - private const val CIRCLE_POINT_COUNT = 64 - } - - override suspend fun addCircles(newCircles: List): List { - val polygons = - newCircles.map { state -> - val geoPolygon = createCirclePolygon(state) - val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) - } - coroutine.launch { - polygons.forEach { holder.map.addMapPolygon(it) } - } - return polygons - } - - override suspend fun removeCircles(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { holder.map.removeMapPolygon(it.circle) } - } - } - - override suspend fun changeCircle(changes: List>): List { - return changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - - // Update geometry if center or radius changed - if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { - val geoPolygon = createCirclePolygon(params.entity.state) - params.entity.circle.geometry = geoPolygon - } - - // Update stroke color - if (finger.strokeColor != prevFinger.strokeColor) { - params.entity.circle.outlineColor = - Color.valueOf( - params.entity.state.strokeColor - .toArgb(), - ) - } - - // Update stroke width - if (finger.strokeWidth != prevFinger.strokeWidth) { - val lineWidth = - ResourceProvider.dpToPx( - params.entity.state.strokeWidth.value - .toDouble(), - ) - params.entity.circle.outlineWidth = lineWidth - } - - // Update fill color - if (finger.fillColor != prevFinger.fillColor) { - params.entity.circle.fillColor = - Color.valueOf( - params.entity.state.fillColor - .toArgb(), - ) - } - - return@map params.entity.circle - } - } - - /** - * Creates a polygon that approximates a circle by generating points around the circumference - */ - private fun createCirclePolygon(state: CircleState): GeoPolygon { - val center = GeoPoint.from(state.center).toGeoCoordinates() - val radiusMeters = state.radiusMeters - - val points = mutableListOf() - - // Generate points around the circle - for (i in 0 until CIRCLE_POINT_COUNT) { - val angle = 2.0 * PI * i / CIRCLE_POINT_COUNT - val point = calculateCirclePoint(center, radiusMeters, angle) - points.add(point) - } - - // Close the polygon by adding the first point at the end - if (points.isNotEmpty()) { - points.add(points.first()) - } - - return GeoPolygon(points) - } - - /** - * Calculate a point on a circle given center, radius and angle - * Uses approximate conversion from meters to degrees for small circles - */ - private fun calculateCirclePoint( - center: GeoCoordinates, - radiusMeters: Double, - angleRadians: Double, - ): GeoCoordinates { - // Approximate conversion: 1 degree latitude ≈ 111,320 meters - // Longitude conversion varies by latitude, use cosine correction - val latDegrees = radiusMeters / 111320.0 - val lonDegrees = radiusMeters / (111320.0 * cos(Math.toRadians(center.latitude))) - - val deltaLat = latDegrees * cos(angleRadians) - val deltaLon = lonDegrees * sin(angleRadians) - - return GeoCoordinates( - center.latitude + deltaLat, - center.longitude + deltaLon, - ) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt deleted file mode 100644 index 74af6fe5..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.mapconductor.here.marker - -import com.here.sdk.core.Metadata -import com.here.sdk.mapview.MapMarker -import com.mapconductor.core.calculateZIndex -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toAnchor2D -import com.mapconductor.here.toGeoCoordinates -import com.mapconductor.here.toMapImage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultHereMapMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onAdd = onIconAdd, - onChange = onIconChange, - onRemove = onIconRemove, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - -class HereMapMarkerRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { - override fun setMarkerPosition( - markerEntity: MarkerEntity, - position: GeoPoint, - ) { - markerEntity.marker.coordinates = position.toGeoCoordinates() - } - - override suspend fun addIcons(newMarkers: List>): List { - val markers = - withContext(coroutine.coroutineContext) { - newMarkers.map { params -> - val marker = - MapMarker( - GeoPoint.from(params.first.position).toGeoCoordinates(), - params.second.toMapImage(), - params.second.toAnchor2D(), - ).apply { - drawOrder = calculateZIndex(params.first.position).toInt() - metadata = - Metadata().apply { - setString("id", params.first.id) - } - } - return@map marker - } - } - - coroutine.launch { - holder.mapView.mapScene.addMapMarkers(markers) - } - return markers - } - - override suspend fun removeIcons(removeEntities: List>) { - coroutine.launch { - val markers: List = removeEntities.map { params -> params.marker } - holder.map.removeMapMarkers(markers) - } - } - - override suspend fun changeIcons(changes: List>): List = - changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint - if (currFinger.icon != prevFinger.icon) { - params.entity.marker.image = params.bitmapIcon.toMapImage() - params.entity.marker.anchor = params.bitmapIcon.toAnchor2D() - } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.coordinates = - GeoPoint.from(params.entity.state.position).toGeoCoordinates() - } - - // Hereはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt new file mode 100644 index 00000000..aec8f5c9 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt @@ -0,0 +1,56 @@ +package com.mapconductor.here.marker + +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.here.HereActualMarker +import com.mapconductor.settings.Settings + +class HereMarkerController( + markerManager: MarkerManager, + override val renderer: HereMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: MarkerEntity? = null + + internal var selectedMarker: MarkerEntity? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + // Restore the recomposition for the position property + setDraggingState(it.state, false) + } + return + } + internalSelectedMarker = value + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + } + get() = internalSelectedMarker + + companion object { + private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 + } + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = renderer.holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt new file mode 100644 index 00000000..bb5682c0 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt @@ -0,0 +1,92 @@ +package com.mapconductor.here.marker + +import com.here.sdk.core.Metadata +import com.here.sdk.mapview.MapMarker +import com.mapconductor.core.calculateZIndex +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerOverlayRenderer +import com.mapconductor.here.HereActualMarker +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toAnchor2D +import com.mapconductor.here.toGeoCoordinates +import com.mapconductor.here.toMapImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HereMarkerRenderer( + holder: HereViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer< + HereViewHolder, + HereActualMarker, + >( + holder = holder, + coroutine = coroutine, + ) { + override fun setMarkerPosition( + markerEntity: MarkerEntity, + position: GeoPoint, + ) { + markerEntity.marker.coordinates = position.toGeoCoordinates() + } + + override suspend fun onAdd(data: List): List { + val markers = + withContext(coroutine.coroutineContext) { + data.map { params -> + val marker = + MapMarker( + GeoPoint.from(params.state.position).toGeoCoordinates(), + params.bitmapIcon.toMapImage(), + params.bitmapIcon.toAnchor2D(), + ).apply { + drawOrder = calculateZIndex(params.state.position).toInt() + metadata = + Metadata().apply { + setString("id", params.state.id) + } + } + return@map marker + } + } + + coroutine.launch { + holder.mapView.mapScene.addMapMarkers(markers) + } + return markers + } + + override suspend fun onRemove(data: List>) { + coroutine.launch { + val markers: List = data.map { params -> params.marker } + holder.map.removeMapMarkers(markers) + } + } + + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + changes: List>, + ): List = + changes.map { params -> + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint + if (currFinger.icon != prevFinger.icon) { + params.current.marker.image = params.bitmapIcon.toMapImage() + params.current.marker.anchor = params.bitmapIcon.toAnchor2D() + } + if (params.current.state.position != params.prev.state.position) { + params.current.marker.coordinates = + GeoPoint.from(params.current.state.position).toGeoCoordinates() + } + + // Hereはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す + params.current.marker + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt deleted file mode 100644 index 15b35c8c..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.mapconductor.here.polygon - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.MapPolygon -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultHereMapPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class HereMapPolygonRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { - val polygons = - newPolygons.map { state -> - val geoPolygon = createGeoPolygon(state) - val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) - } - coroutine.launch { - polygons.forEach { holder.map.addMapPolygon(it) } - } - return polygons - } - - override suspend fun removePolygons(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { holder.map.removeMapPolygon(it.polygon) } - } - } - - override suspend fun changePolygon(changes: List>): List { - return changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() - if (finger.points != prevFinger.points) { - val geoPolygon = createGeoPolygon(params.entity.state) - params.entity.polygon.geometry = geoPolygon - } - if (finger.strokeColor != prevFinger.strokeColor) { - params.entity.polygon.outlineColor = - Color.valueOf( - params.entity.state.strokeColor - .toArgb(), - ) - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - val lineWidth = - ResourceProvider.dpToPx( - params.entity.state.strokeWidth.value - .toDouble(), - ) - params.entity.polygon.outlineWidth = lineWidth - } - if (finger.fillColor != prevFinger.fillColor) { - params.entity.polygon.fillColor = - Color.valueOf( - params.entity.state.fillColor - .toArgb(), - ) - } - return@map params.entity.polygon - } - } - - private fun createGeoPolygon(state: PolygonState): GeoPolygon { - val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } - // Ensure the polygon is closed by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - return GeoPolygon(closedPoints) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt new file mode 100644 index 00000000..bbeed14f --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt @@ -0,0 +1,91 @@ +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolygon +import com.here.sdk.mapview.MapPolygon +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.here.HereActualPolygon +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HerePolygonOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolygonOverlayRenderer() { + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + holder.map.removeMapPolygon(entity.polygon) + } + } + + override suspend fun createPolygon(state: PolygonState): HereActualPolygon? { + val geoPolygon = createGeoPolygon(state) + val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) + val mapPolygon = + MapPolygon( + geoPolygon, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) + coroutine.launch { + holder.map.addMapPolygon(mapPolygon) + } + return mapPolygon + } + + override suspend fun updatePolygonProperties( + polygon: HereActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): HereActualPolygon? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val geoPolygon = createGeoPolygon(current.state) + current.polygon.geometry = geoPolygon + } + if (finger.strokeColor != prevFinger.strokeColor) { + current.polygon.outlineColor = + Color.valueOf( + current.state.strokeColor.toArgb(), + ) + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + val lineWidth = + ResourceProvider.dpToPx( + current.state.strokeWidth.value + .toDouble(), + ) + current.polygon.outlineWidth = lineWidth + } + if (finger.fillColor != prevFinger.fillColor) { + current.polygon.fillColor = + Color.valueOf( + current.state.fillColor.toArgb(), + ) + } + polygon + } + + private fun createGeoPolygon(state: PolygonState): GeoPolygon { + val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } + // Ensure the polygon is closed by adding the first point at the end if not already closed + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return GeoPolygon(closedPoints) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt new file mode 100644 index 00000000..e34963bf --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt @@ -0,0 +1,12 @@ +package com.mapconductor.here.polygon + +import HerePolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl +import com.mapconductor.here.HereActualPolygon + +class HerePolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + renderer: HerePolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) 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 deleted file mode 100644 index a7f56acd..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.mapconductor.here.polyline - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolyline -import com.here.sdk.mapview.LineCap -import com.here.sdk.mapview.MapMeasureDependentRenderSize -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.RenderSize -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -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.here.HereMapActualPolyline -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultHereMapPolylineRenderer : 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 HereMapPolylineRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List { - val polylines = - newLines.map { state -> - val geoPolyline = createGeoPolyline(state) - val representation = createRepresentation(state) - MapPolyline(geoPolyline, representation) - } - coroutine.launch { - holder.map.addMapPolylines(polylines) - } - return polylines - } - - override suspend fun removePolylines(removeEntities: List>) { - val polylines = removeEntities.map { it.polyline } - coroutine.launch { - holder.map.removeMapPolylines(polylines) - } - } - - override suspend fun changePolylines( - changes: List>, - ): List { - val removed = mutableListOf() - val polylines = - changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - removed.add(params.prevEntity.polyline) - val geoPolyline = createGeoPolyline(params.entity.state) - params.entity.polyline.geometry = geoPolyline - } - if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeColor) { - val representation = createRepresentation(params.entity.state) - params.entity.polyline.setRepresentation(representation) - } - return@map params.entity.polyline - } - - coroutine.launch { - holder.map.removeMapPolylines(removed) - holder.map.addMapPolylines(polylines) - } - return polylines - } - - private fun createGeoPolyline(state: PolylineState): GeoPolyline { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - } - val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } - val geoPolyline = GeoPolyline(points) - return geoPolyline - } - - private fun createRepresentation(state: PolylineState): MapPolyline.Representation { - val lineWidth = - MapMeasureDependentRenderSize( - RenderSize.Unit.PIXELS, - ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), - ) - val lineColor = Color.valueOf(state.strokeColor.toArgb()) - val lineCap = LineCap.SQUARE - return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt new file mode 100644 index 00000000..931eb0ac --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.here.polyline + +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.here.HereActualPolyline + +class HerePolylineController( + polylineManager: PolylineManager = PolylineManagerImpl(), + renderer: HerePolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt new file mode 100644 index 00000000..e61844e6 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt @@ -0,0 +1,101 @@ +package com.mapconductor.here.polyline + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolyline +import com.here.sdk.mapview.LineCap +import com.here.sdk.mapview.MapMeasureDependentRenderSize +import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.RenderSize +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.here.HereActualPolyline +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HerePolylineOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): HereActualPolyline? { + val geoPolyline = createGeoPolyline(state) + val representation = createRepresentation(state) + val mapPolyline = MapPolyline(geoPolyline, representation) + + coroutine.launch { + holder.map.addMapPolylines(listOf(mapPolyline)) + } + + return mapPolyline + } + + override suspend fun updatePolylineProperties( + polyline: HereActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): HereActualPolyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + var needsReAdd = false + + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + val geoPolyline = createGeoPolyline(current.state) + polyline.geometry = geoPolyline + needsReAdd = true + } + + if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { + val representation = createRepresentation(current.state) + polyline.setRepresentation(representation) + needsReAdd = true + } + + if (needsReAdd) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(polyline)) + holder.map.addMapPolylines(listOf(polyline)) + } + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(entity.polyline)) + } + } + + private fun createGeoPolyline(state: PolylineState): GeoPolyline { + val geoPoints: List = + when (state.geodesic) { + true -> createInterpolatePoints(state.points) + false -> createLinearInterpolatePoints(state.points) + } + val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } + return GeoPolyline(points) + } + + private fun createRepresentation(state: PolylineState): MapPolyline.Representation { + val lineWidth = + MapMeasureDependentRenderSize( + RenderSize.Unit.PIXELS, + ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), + ) + val lineColor = Color.valueOf(state.strokeColor.toArgb()) + val lineCap = LineCap.SQUARE + return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt index e802769c..40686987 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt @@ -1,6 +1,8 @@ package com.mapconductor.mapbox +import com.mapbox.maps.Style import com.mapconductor.core.map.MapDesignType +import com.mapconductor.mapbox.MapboxMapDesign.Companion.MAPBOX_URL typealias MapboxDesignType = MapDesignType @@ -52,3 +54,21 @@ sealed class MapboxMapDesign( } } } + +fun Style.toMapDesignType(): MapboxDesignType = + when (this.styleURI) { + "$MAPBOX_URL/${MapboxMapDesign.Standard.id}" -> MapboxMapDesign.Standard + "$MAPBOX_URL/${MapboxMapDesign.StandardSatellite.id}" -> MapboxMapDesign.StandardSatellite + "$MAPBOX_URL/${MapboxMapDesign.Streets.id}" -> MapboxMapDesign.Streets + "$MAPBOX_URL/${MapboxMapDesign.Outdoors.id}" -> MapboxMapDesign.Outdoors + "$MAPBOX_URL/${MapboxMapDesign.Light.id}" -> MapboxMapDesign.Light + "$MAPBOX_URL/${MapboxMapDesign.Dark.id}" -> MapboxMapDesign.Dark + "$MAPBOX_URL/${MapboxMapDesign.Satellite.id}" -> MapboxMapDesign.Satellite + "$MAPBOX_URL/${MapboxMapDesign.SatelliteStreets.id}" -> MapboxMapDesign.SatelliteStreets + "$MAPBOX_URL/${MapboxMapDesign.NavigationDay.id}" -> MapboxMapDesign.NavigationDay + "$MAPBOX_URL/${MapboxMapDesign.NavigationNight.id}" -> MapboxMapDesign.NavigationNight + else -> + MapboxMapDesign.Custom( + layerId = this.styleURI.replaceFirst("${MAPBOX_URL}/", ""), + ) + } 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 43721569..74e54ae6 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 @@ -6,35 +6,52 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions +import com.mapconductor.core.circle.CircleManagerImpl import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.mapbox.circle.MapboxCircleController +import com.mapconductor.mapbox.circle.MapboxCircleLayer +import com.mapconductor.mapbox.circle.MapboxCircleOverlayRenderer +import com.mapconductor.mapbox.marker.MapboxMarkerController +import com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer +import com.mapconductor.mapbox.polygon.MapboxPolygonConductor +import com.mapconductor.mapbox.polygon.MapboxPolygonLayer +import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer +import com.mapconductor.mapbox.polyline.MapboxPolylineController +import com.mapconductor.mapbox.polyline.MapboxPolylineLayer +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @Composable fun MapboxMapView( - state: IMapboxMapViewState, + state: MapboxViewStateImpl, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } val context = LocalContext.current - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { MapboxMapViewScope() } val registry = remember { scope.buildRegistry() } @@ -50,7 +67,7 @@ fun MapboxMapView( MapboxInitSDK(context) val cameraOptions = - state.cameraPosition.value?.toCameraOptions() + state.cameraPosition.value.toCameraOptions() val styleUri = state.mapDesignType.getValue() val mapInitOptions = @@ -64,24 +81,26 @@ fun MapboxMapView( val holder = MapboxMapViewHolderImpl.create(context, mapInitOptions) val controller = - MapboxMapViewController( + MapboxMapViewControllerImpl( holder = holder, + markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), + circleController = getCircleController(holder), ) - (state as? MapboxMapViewState)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) - controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnCircleClickListener(onCircleClick) + controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) + state.setController(controller) holderRef.value = holder controllerRef.value = controller @@ -94,6 +113,105 @@ fun MapboxMapView( ) } +internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonConductor { + val polylineLayer: MapboxPolylineLayer = + MapboxPolylineLayer( + sourceId = "polygon-outline-source", + layerId = "polygon-outline-layer", + ) + val polylineManager = PolylineManagerImpl() + val polylineOverlayRenderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val polygonManager = PolygonManagerImpl() + val polygonLayer: MapboxPolygonLayer = + MapboxPolygonLayer( + sourceId = "polygon-fill-source", + layerId = "polygon-fill-layer", + ) + val polygonOverlayRenderer = + MapboxPolygonOverlayRenderer( + layer = polygonLayer, + polygonManager = polygonManager, + holder = holder, + ) + + val conductor = + MapboxPolygonConductor( + polygonOverlay = polygonOverlayRenderer, + polylineOverlay = polylineOverlayRenderer, + ) + return conductor +} + +internal fun getCircleController(holder: MapboxMapViewHolder): MapboxCircleController { + val circleLayer: MapboxCircleLayer = + MapboxCircleLayer( + sourceId = "circle-source", + layerId = "circle-layer", + ) + val circleManager = CircleManagerImpl() + + val renderer = + MapboxCircleOverlayRenderer( + layer = circleLayer, + circleManager = circleManager, + holder = holder, + ) + + val controller = + MapboxCircleController( + renderer = renderer, + ) + return controller +} + +internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { + val polylineLayer: MapboxPolylineLayer = + MapboxPolylineLayer( + sourceId = "polyline-source", + layerId = "polyline-layer", + ) + val polylineManager = PolylineManagerImpl() + + val renderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val controller = + MapboxPolylineController( + renderer = renderer, + ) + return controller +} + +internal fun getMarkerController(holder: MapboxMapViewHolder): MapboxMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + MapboxMarkerOverlayRenderer( + holder = holder, + markerManager = manager, + ) + val controller = + MapboxMarkerController( + renderer = renderer, + ) + return controller +} + internal fun Context.findActivity(): Activity? = when (this) { is Activity -> this 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 94f9cbd4..13314bda 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 @@ -1,441 +1,18 @@ -package com.mapconductor.mapbox - -import androidx.compose.ui.geometry.Offset -import com.mapbox.android.gestures.MoveGestureDetector -import com.mapbox.geojson.Feature -import com.mapbox.geojson.Point -import com.mapbox.maps.CameraChanged -import com.mapbox.maps.CameraChangedCallback -import com.mapbox.maps.CameraOptions -import com.mapbox.maps.CameraState -import com.mapbox.maps.ScreenCoordinate -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.sources.addSource -import com.mapbox.maps.plugin.animation.MapAnimationOptions -import com.mapbox.maps.plugin.animation.flyTo -import com.mapbox.maps.plugin.gestures.OnMapClickListener -import com.mapbox.maps.plugin.gestures.OnMapLongClickListener -import com.mapbox.maps.plugin.gestures.OnMoveListener -import com.mapbox.maps.plugin.gestures.addOnMapClickListener -import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener -import com.mapbox.maps.plugin.gestures.addOnMoveListener -import com.mapbox.maps.plugin.gestures.removeOnMapClickListener -import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener -import com.mapbox.maps.plugin.gestures.removeOnMoveListener -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.circle.CircleCapable import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRenderer -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator -import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer -import com.mapconductor.mapbox.circle.MapboxCircleLayer -import com.mapconductor.mapbox.circle.MapboxCircleRenderer -import com.mapconductor.mapbox.marker.DefaultMapboxMarkerRenderer -import com.mapconductor.mapbox.marker.MapboxMarkerRenderer -import com.mapconductor.mapbox.marker.MarkerDragLayer -import com.mapconductor.mapbox.marker.MarkerLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonRenderer -import com.mapconductor.mapbox.polyline.DefaultMapboxPolylineRenderer -import com.mapconductor.mapbox.polyline.MapboxPolylineLayer -import com.mapconductor.mapbox.polyline.MapboxPolylineRenderer -import com.mapconductor.settings.Settings -import android.animation.Animator -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -interface IMapboxMapViewController : - MapViewController< - MapboxActualMarker, - MapboxActualCircle, - MapboxActualPolyline, - MapboxActualPolygon, - > { - fun changeMapDesign(value: String) - - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Long, - listener: MapViewState.MoveCameraCallback? = null, - ) -} - -internal class MapboxMapViewController( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - // Web Mercator投影法に適したbaseHexSideLengthを使用 - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultMapboxMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultMapboxPolylineRenderer(), - private val markerLayer: MarkerLayer = - MarkerLayer( - sourceId = "markers-source", - layerId = "markers-layer", - ), - private val dragLayer: MarkerDragLayer = - MarkerDragLayer( - sourceId = "marker-drag-source", - layerId = "marker-drag-layer", - ), - private val circleLayer: MapboxCircleLayer = - MapboxCircleLayer( - sourceId = "circle-source", - layerId = "circle-layer", - ), - private val polylineLayer: MapboxPolylineLayer = - MapboxPolylineLayer( - sourceId = "polyline-source", - layerId = "polyline-layer", - ), - private val polygonLayer: MapboxPolygonLayer = - MapboxPolygonLayer( - sourceId = "polygon-source", - layerId = "polygon-layer", - ), - private val circleRendererFactory: CircleRendererFactory = - DefaultMapboxCircleRenderer(), -) : BaseMapViewController< - MapboxActualMarker, - MapboxActualCircle, - MapboxActualPolyline, - MapboxActualPolygon, - >(), - IMapboxMapViewController, - CameraChangedCallback, - OnMapClickListener, - OnMapLongClickListener, - OnMoveListener { - companion object { - private const val ZOOM_ADJUST_VALUE = 1.0 - } - - override val markerRenderer: MapboxMarkerRenderer = - MapboxMarkerRenderer( - holder = holder, - coroutine = coroutine, - markerLayer = markerLayer, - dragLayer = dragLayer, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onPostProcess = markerRenderer::redraw, - onAnimate = markerRenderer::animate, - ) - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - onPostProcess = polylineRenderer::redraw, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager { - TODO("Not yet implemented") - } - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - onPostProcess = circleRenderer::redraw, - ) - - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - holder.map.getStyle { style -> - style.addSource(circleLayer.source) - style.addLayer(circleLayer.layer) - style.addSource(polylineLayer.source) - style.addLayer(polylineLayer.layer) - style.addSource(markerLayer.source) - style.addLayer(markerLayer.layer) - style.addSource(dragLayer.source) - style.addLayer(dragLayer.layer) - } - } - - override val polylineRenderer: MapboxPolylineRenderer = - MapboxPolylineRenderer( - holder = holder, - coroutine = coroutine, - layer = polylineLayer, - ) - - override val polygonRenderer: PolygonRenderer = - MapboxPolygonRenderer( - holder = holder, - coroutine = coroutine, - layer = polygonLayer, - ) - override val circleRenderer: MapboxCircleRenderer = - MapboxCircleRenderer( - holder = holder, - coroutine = coroutine, - layer = circleLayer, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - init { - setupListeners() - } - - override fun setupListeners() { - holder.map.subscribeCameraChanged(this) - holder.map.removeOnMapClickListener(this) - holder.map.addOnMapClickListener(this) - - holder.map.removeOnMapLongClickListener(this) - holder.map.addOnMapLongClickListener(this) - - holder.map.removeOnMoveListener(this) - holder.map.addOnMoveListener(this) - } - - override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() - polylineOverlayManager.clearOverlays() - } - - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) - - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) - - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) - - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) - - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - - override fun run(cameraChanged: CameraChanged) { - cameraMoveCallback?.let { - val mapCameraPosition = - CameraState( - cameraChanged.cameraState.center, - cameraChanged.cameraState.padding, - cameraChanged.cameraState.zoom + ZOOM_ADJUST_VALUE, - cameraChanged.cameraState.bearing, - cameraChanged.cameraState.pitch, - ).toMapCameraPosition() - - it(mapCameraPosition) - } - } - - override fun changeMapDesign(value: String) { - coroutine.launch { - holder.mapView.mapboxMap.loadStyle(value) {} - } - } - - override fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback?, - ) { - val cameraOptions = - CameraOptions - .Builder() - .center(dstPosition.position.toPoint()) - .zoom(dstPosition.zoom - ZOOM_ADJUST_VALUE) - .pitch(dstPosition.tilt) - .bearing(dstPosition.bearing) - .build() - - coroutine.launch { - holder.map.setCamera(cameraOptions) - } - listener?.onComplete(true) - } - - override fun animateCamera( - dstPosition: MapCameraPosition, - duration: Long, - listener: MapViewState.MoveCameraCallback?, - ) { - val targetCamera = dstPosition.toCameraOptions() - val adjustCamera = - CameraOptions - .Builder() - .center(targetCamera.center) - .zoom(targetCamera.zoom!! + ZOOM_ADJUST_VALUE) - .pitch(targetCamera.pitch) - .bearing(targetCamera.pitch) - .build() - - val animationOptions = - MapAnimationOptions - .Builder() - .duration(duration) - .build() - - val animatorListener = - object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator) { - // Do nothing here - } - - override fun onAnimationEnd(animation: Animator) { - listener?.onComplete(true) - } - - override fun onAnimationCancel(animation: Animator) { - listener?.onComplete(false) - } - - override fun onAnimationRepeat(animation: Animator) { - // Do nothing here - } - } - - coroutine.launch { - holder.map.flyTo( - cameraOptions = adjustCamera, - animationOptions = animationOptions, - animatorListener = animatorListener, - ) - } - } - - override fun onMapLongClick(point: Point): Boolean { - val geoPoint = point.toGeoPoint() - val entity = - this.markerRenderer.findNearestMarker( - position = geoPoint, - tolerance = ResourceProvider.dpToPx(Settings.Default.tapTolerance), - zoom = holder.map.cameraState.zoom, - ) - if (entity != null) { - markerRenderer.setDraggingState(entity.state, true) // Suppress the recomposition for the position property - markerOverlayManager.markerManager.removeEntity(entity.state.id) - dragLayer.selected = entity - dragLayer.updatePosition(geoPoint) - markerRenderer.redraw() - markerRenderer.drawDragLayer() - - markerDragStartCallback?.invoke(entity.state) - return true - } - - mapLongClickCallback?.invoke(geoPoint) - return true - } - - override fun onMapClick(point: Point): Boolean { - val touchPosition = point.toGeoPoint() - - this.markerRenderer - .findNearestMarker( - position = touchPosition, - tolerance = ResourceProvider.dpToPx(Settings.Default.tapTolerance), - zoom = holder.map.cameraState.zoom, - )?.let { - markerClickCallback?.invoke(it.state) - return true - } - - val circleEntity = this.circleOverlayManager.find(touchPosition) - circleEntity?.let { - val event = - CircleClickEvent( - state = circleEntity.state, - position = touchPosition, - ) - circleClickCallback?.invoke(event) - return true - } - - mapClickCallback?.invoke(touchPosition) - return true - } - -// override fun drawPolyline(geoPoints: List) { -// val points = -// geoPoints.map { -// GeoPoint.from(it).toPoint() -// } -// lineSource.geometry(LineString.fromLngLats(points)) -// } - - override fun onMove(detector: MoveGestureDetector): Boolean { - dragLayer.selected?.let { entity -> - - val screenCoordinate = - Offset( - detector.focalPoint.x, - detector.focalPoint.y, - ) - - holder.fromScreenOffsetSync(screenCoordinate)?.let { - entity.state.position = it - dragLayer.updatePosition(it) - markerRenderer.drawDragLayer() - } - - markerDragCallback?.invoke(entity.state) - return true - } - return false - } - - override fun onMoveBegin(detector: MoveGestureDetector) { - // Do nothing here - } - - override fun onMoveEnd(detector: MoveGestureDetector) { - dragLayer.selected?.let { entity -> - val screenCoordinate = - ScreenCoordinate( - detector.focalPoint.x.toDouble(), - detector.focalPoint.y.toDouble(), - ) - val point = holder.map.coordinateForPixel(screenCoordinate) - dragLayer.updatePosition(point.toGeoPoint()) - dragLayer.selected = null - markerRenderer.drawDragLayer() - markerRenderer.setDraggingState(entity.state, false) // Restore the recomposition for the position property - markerOverlayManager.markerManager.registerEntity(entity) - markerRenderer.redraw() - markerDragEndCallback?.invoke(entity.state) - } - } +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable +import com.mapconductor.mapbox.MapboxDesignType +import com.mapconductor.mapbox.MapboxMapDesignTypeChangeHandler + +interface MapboxMapViewController : + MapViewController, + MarkerCapable, + PolylineCapable, + PolygonCapable, + CircleCapable { + fun setMapDesignType(value: MapboxDesignType) + + fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt new file mode 100644 index 00000000..0296aecf --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -0,0 +1,355 @@ +package com.mapconductor.mapbox + +import MapboxMapViewController +import androidx.compose.ui.geometry.Offset +import com.mapbox.android.gestures.MoveGestureDetector +import com.mapbox.geojson.Point +import com.mapbox.maps.CameraChanged +import com.mapbox.maps.CameraChangedCallback +import com.mapbox.maps.CameraOptions +import com.mapbox.maps.CameraState +import com.mapbox.maps.ScreenCoordinate +import com.mapbox.maps.StyleLoaded +import com.mapbox.maps.StyleLoadedCallback +import com.mapbox.maps.extension.style.layers.addLayer +import com.mapbox.maps.extension.style.sources.addSource +import com.mapbox.maps.plugin.animation.MapAnimationOptions +import com.mapbox.maps.plugin.animation.flyTo +import com.mapbox.maps.plugin.gestures.OnMapClickListener +import com.mapbox.maps.plugin.gestures.OnMapLongClickListener +import com.mapbox.maps.plugin.gestures.OnMoveListener +import com.mapbox.maps.plugin.gestures.addOnMapClickListener +import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener +import com.mapbox.maps.plugin.gestures.addOnMoveListener +import com.mapbox.maps.plugin.gestures.removeOnMapClickListener +import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener +import com.mapbox.maps.plugin.gestures.removeOnMoveListener +import com.mapconductor.core.circle.CircleEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.circle.MapboxCircleController +import com.mapconductor.mapbox.marker.MapboxMarkerController +import com.mapconductor.mapbox.polygon.MapboxPolygonConductor +import com.mapconductor.mapbox.polyline.MapboxPolylineController +import android.animation.Animator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +typealias MapboxMapDesignTypeChangeHandler = (MapboxDesignType) -> Unit + +internal class MapboxMapViewControllerImpl( + override val holder: MapboxMapViewHolder, + private val markerController: MapboxMarkerController, + private val polylineController: MapboxPolylineController, + private val polygonController: MapboxPolygonConductor, + private val circleController: MapboxCircleController, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : BaseMapViewController(), + MapboxMapViewController, + CameraChangedCallback, + StyleLoadedCallback, + OnMapClickListener, + OnMapLongClickListener, + OnMoveListener { + companion object { + private const val ZOOM_ADJUST_VALUE = 1.0 + } + + init { + holder.map.getStyle { style -> + // Circle + style.addSource(circleController.renderer.layer.source) + style.addLayer(circleController.renderer.layer.layer) + + // Polygon + style.addSource(polygonController.polygonOverlay.layer.source) + style.addLayer(polygonController.polygonOverlay.layer.layer) + style.addSource(polygonController.polylineOverlay.layer.source) + style.addLayer(polygonController.polylineOverlay.layer.layer) + + // Polyline + style.addSource(polylineController.renderer.layer.source) + style.addLayer(polylineController.renderer.layer.layer) + + // Marker + style.addSource(markerController.renderer.markerLayer.source) + style.addLayer(markerController.renderer.markerLayer.layer) + style.addSource(markerController.renderer.dragLayer.source) + style.addLayer(markerController.renderer.dragLayer.layer) + } + setupListeners() + } + + fun setupListeners() { + holder.map.subscribeCameraChanged(this) + holder.map.subscribeStyleLoaded(this) + holder.map.removeOnMapClickListener(this) + holder.map.addOnMapClickListener(this) + + holder.map.removeOnMapLongClickListener(this) + holder.map.addOnMapLongClickListener(this) + + holder.map.removeOnMoveListener(this) + holder.map.addOnMoveListener(this) + } + + override suspend fun clearOverlays() { + markerController.clear() + polylineController.clear() + polygonController.clear() + } + + override suspend fun compositionMarkers(data: List) = markerController.add(data) + + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) + + override suspend fun compositionPolylines(data: List) = polylineController.add(data) + + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + + override suspend fun compositionCircles(data: List) = circleController.add(data) + + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } + + override fun run(cameraChanged: CameraChanged) { + cameraMoveCallback?.let { + val mapCameraPosition = + CameraState( + cameraChanged.cameraState.center, + cameraChanged.cameraState.padding, + cameraChanged.cameraState.zoom + ZOOM_ADJUST_VALUE, + cameraChanged.cameraState.bearing, + cameraChanged.cameraState.pitch, + ).toMapCameraPosition() + + it(mapCameraPosition) + } + } + + override fun moveCamera( + dstPosition: MapCameraPosition, + listener: MapViewState.MoveCameraCallback?, + ) { + val cameraOptions = + CameraOptions + .Builder() + .center(dstPosition.position.toPoint()) + .zoom(dstPosition.zoom - ZOOM_ADJUST_VALUE) + .pitch(dstPosition.tilt) + .bearing(dstPosition.bearing) + .build() + + coroutine.launch { + holder.map.setCamera(cameraOptions) + } + listener?.onComplete() + } + + override fun animateCamera( + dstPosition: MapCameraPosition, + duration: Long, + listener: MapViewState.MoveCameraCallback?, + ) { + val targetCamera = dstPosition.toCameraOptions() + val adjustCamera = + CameraOptions + .Builder() + .center(targetCamera.center) + .zoom(targetCamera.zoom!! + ZOOM_ADJUST_VALUE) + .pitch(targetCamera.pitch) + .bearing(targetCamera.pitch) + .build() + + val animationOptions = + MapAnimationOptions + .Builder() + .duration(duration) + .build() + + val animatorListener = + object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { + // Do nothing here + } + + override fun onAnimationEnd(animation: Animator) { + listener?.onComplete() + } + + override fun onAnimationCancel(animation: Animator) { + listener?.onComplete() + } + + override fun onAnimationRepeat(animation: Animator) { + // Do nothing here + } + } + + coroutine.launch { + holder.map.flyTo( + cameraOptions = adjustCamera, + animationOptions = animationOptions, + animatorListener = animatorListener, + ) + } + } + + override fun onMapLongClick(point: Point): Boolean { + val touchPosition = point.toGeoPoint() + markerController.find(touchPosition)?.let { entity -> + if (entity.state.draggable) { + markerController.selectedMarker = entity + markerController.markerManager.removeEntity(entity.state.id) + markerController.dragStartListener?.invoke(entity.state) + return true + } + } + + mapLongClickCallback?.invoke(touchPosition) + return true + } + + override fun onMapClick(point: Point): Boolean { + val touchPosition = point.toGeoPoint() + + markerController.find(touchPosition)?.let { entity -> + markerController.clickListener?.invoke(entity.state) + return true + } + + circleController.find(touchPosition)?.let { entity -> + val event = + CircleEvent( + state = entity.state, + clicked = touchPosition, + ) + circleController.clickListener?.invoke(event) + return true + } + + polygonController.find(touchPosition)?.let { polygonEntity -> + val event = + PolygonEvent( + state = polygonEntity.state, + clicked = touchPosition, + ) + polygonController.clickListener?.invoke(event) + return true + } + + mapClickCallback?.invoke(touchPosition) + return true + } + + override fun onMove(detector: MoveGestureDetector): Boolean { + markerController.renderer.dragLayer.selected?.let { entity -> + + val screenCoordinate = + Offset( + detector.focalPoint.x, + detector.focalPoint.y, + ) + + holder.fromScreenOffsetSync(screenCoordinate)?.let { + entity.state.position = it + markerController.renderer.dragLayer.updatePosition(it) + markerController.renderer.drawDragLayer() + } + + markerController.dragListener?.invoke(entity.state) + return true + } + return false + } + + override fun onMoveBegin(detector: MoveGestureDetector) { + // Do nothing here + } + + override fun onMoveEnd(detector: MoveGestureDetector) { + markerController.selectedMarker?.let { entity -> + val screenCoordinate = + ScreenCoordinate( + detector.focalPoint.x.toDouble(), + detector.focalPoint.y.toDouble(), + ) + val point = holder.map.coordinateForPixel(screenCoordinate) + markerController.renderer.dragLayer.updatePosition(point.toGeoPoint()) + markerController.selectedMarker = null + markerController.dragEndListener?.invoke(entity.state) + } + } + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + markerController.clickListener = listener + } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + polylineController.clickListener = listener + } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + polygonController.clickListener = listener + } + + private var mapDesignType: MapboxDesignType = MapboxMapDesign.Standard + + private var mapDesignTypeChangeListener: MapboxMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: MapboxDesignType) { + coroutine.launch { + holder.mapView.mapboxMap.loadStyle(value.getValue()) + } + } + + override fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) { + mapDesignTypeChangeListener = listener + listener(mapDesignType) + } + + override fun run(styleLoaded: StyleLoaded) { + holder.map.style?.toMapDesignType()?.let { mapDesignType -> + this@MapboxMapViewControllerImpl.mapDesignType = mapDesignType + mapDesignTypeChangeListener?.invoke(mapDesignType) + } + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt new file mode 100644 index 00000000..044d7fd6 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt @@ -0,0 +1,43 @@ +package com.mapconductor.mapbox + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.LineString +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.features.normalize +import com.mapconductor.core.splitByMeridian +import com.mapconductor.mapbox.polyline.MapboxPolylineLayer + +internal fun createMapboxLines( + id: String, + points: List, + geodesic: Boolean, + strokeColor: Color, + strokeWidth: Dp, +): List { + val geoPoints: List = + when (geodesic) { + true -> createInterpolatePoints(points) + false -> createLinearInterpolatePoints(points) + }.map { it.normalize() } + + return splitByMeridian(geoPoints, geodesic).mapIndexed { index, linePoints -> + val points = linePoints.map { GeoPoint.from(it).toPoint() } + val id = "polyline-$id-$index" + + return@mapIndexed Feature.fromGeometry( + LineString.fromLngLats(points), + JsonObject().apply { + addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, strokeColor.toMapboxColorString()) + addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, strokeWidth.value) + addProperty("id", id) + }, + id, + ) + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt index a8199ba7..a011c7c0 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt @@ -5,4 +5,9 @@ import com.mapbox.geojson.Feature typealias MapboxActualMarker = Feature typealias MapboxActualCircle = Feature typealias MapboxActualPolyline = List -typealias MapboxActualPolygon = Feature +typealias MapboxActualPolygon = List + +data class MapboxOutlineAndFill( + val outline: MapboxActualPolyline?, + val fill: MapboxActualPolygon?, +) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt similarity index 58% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index fc796c23..c0c3d49e 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -1,5 +1,6 @@ package com.mapconductor.mapbox +import MapboxMapViewController import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,23 +19,41 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IMapboxMapViewState : MapViewState +interface MapboxViewState : MapViewState -class MapboxMapViewState( +class MapboxViewStateImpl( + mapDesignType: MapboxDesignType, override val id: String, - override var mapDesignType: MapboxDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), - IMapboxMapViewState { - internal var controller: IMapboxMapViewController? = null + MapboxViewState { + private var controller: MapboxMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - override fun changeMapDesignType(value: MapboxDesignType) { - this.mapDesignType = value - this.controller?.changeMapDesign(value.getValue()) + private var _mapDesignType: MapboxDesignType = mapDesignType + + override var mapDesignType: MapboxDesignType + set(value) { + value?.let { + _mapDesignType = it + this.controller?.setMapDesignType(it) + } + } + get() = _mapDesignType + + internal fun setController(controller: MapboxMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: MapboxDesignType) { + _mapDesignType = value } override fun moveCameraTo( @@ -43,8 +62,11 @@ class MapboxMapViewState( listener: MapViewState.MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() return } val currentPosition = this.cameraPosition.value @@ -55,26 +77,27 @@ class MapboxMapViewState( this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): MapboxMapViewHolder? = controller?.holder as? MapboxMapViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - if (controller == null) { - listener?.onComplete(false) - return - } - if (durationMs == 0L) { - controller!!.moveCamera(dstCameraPosition, listener) - } else { - controller!!.animateCamera(dstCameraPosition, durationMs.toLong(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return + } } + _cameraPosition.value = cameraPosition + listener?.onComplete() } internal fun onCameraChange(cameraPosition: MapCameraPosition) { @@ -82,22 +105,22 @@ class MapboxMapViewState( } } -class MapboxMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: MapboxMapViewState): MapCameraPosition? = state.cameraPosition.value +class MapboxMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: MapboxViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: MapboxMapViewState, + state: MapboxViewStateImpl, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + bundle.putString("id", state.mapDesignType?.id ?: "null") } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): MapboxMapViewState = - MapboxMapViewState( + ): MapboxViewStateImpl = + MapboxViewStateImpl( id = stateId, mapDesignType = MapboxMapDesign.Create( @@ -106,14 +129,14 @@ class MapboxMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: MapboxMapViewState): String = state.id + override fun getStateId(state: MapboxViewStateImpl): String = state.id } @Composable fun rememberMapboxMapViewState( mapDesign: MapboxDesignType = Standard, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): MapboxMapViewState { +): MapboxViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -123,7 +146,7 @@ fun rememberMapboxMapViewState( stateSaver = MapboxMapViewSaver().createSaver(), ) { mutableStateOf( - MapboxMapViewState( + MapboxViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt new file mode 100644 index 00000000..a1e1fdef --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt @@ -0,0 +1,10 @@ +package com.mapconductor.mapbox.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.mapbox.MapboxActualCircle + +class MapboxCircleController( + override val renderer: MapboxCircleOverlayRenderer, + circleManager: CircleManager = renderer.circleManager, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt new file mode 100644 index 00000000..30ca64a2 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt @@ -0,0 +1,85 @@ +package com.mapconductor.mapbox.circle + +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.Point +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.mapbox.MapboxActualCircle +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.toMapboxColorString +import com.mapconductor.mapbox.toPoint +import kotlin.math.cos +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxCircleOverlayRenderer( + val layer: MapboxCircleLayer = + MapboxCircleLayer( + sourceId = "circles-source", + layerId = "circles-layer", + ), + val circleManager: CircleManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun removeCircle(entity: CircleEntity) { + val featureIds = listOf("circle-${entity.state.id}") + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun createCircle(state: CircleState): MapboxActualCircle? { + val centerPoint = GeoPoint.from(state.center).toPoint() + return Feature.fromGeometry( + Point.fromLngLat(centerPoint.longitude(), centerPoint.latitude()), + JsonObject().apply { + val latitudeCorrection = cos(Math.toRadians(centerPoint.latitude())) + addProperty(MapboxCircleLayer.Prop.LATITUDE_CORRECTION, latitudeCorrection) + addProperty(MapboxCircleLayer.Prop.RADIUS, state.radiusMeters) + addProperty(MapboxCircleLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + }, + "circle-${state.id}", + ) + } + + override suspend fun updateCircleProperties( + circle: MapboxActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): MapboxActualCircle? { + val state = current.state + val centerPoint = GeoPoint.from(state.center).toPoint() + return Feature.fromGeometry( + Point.fromLngLat(centerPoint.longitude(), centerPoint.latitude()), + JsonObject().apply { + val latitudeCorrection = cos(Math.toRadians(centerPoint.latitude())) + addProperty(MapboxCircleLayer.Prop.LATITUDE_CORRECTION, latitudeCorrection) + addProperty(MapboxCircleLayer.Prop.RADIUS, state.radiusMeters) + addProperty(MapboxCircleLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + }, + "circle-${state.id}", + ) + } + + override suspend fun onPostProcess() { + val circles = getAllCircleEntities() + coroutine.launch { + layer.draw(circles) + } + } + + private fun getAllCircleEntities(): List> { + // This would need access to the polyline manager + // For now, we'll implement a simple workaround + return circleManager.allEntities() + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt index 7a477bfb..38023744 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt @@ -7,10 +7,7 @@ import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures import com.mapconductor.core.circle.AbstractCircleRenderer import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.mapbox.MapboxActualCircle @@ -21,21 +18,6 @@ import kotlin.math.cos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultMapboxCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class MapboxCircleRenderer( override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt new file mode 100644 index 00000000..f2dc64bf --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt @@ -0,0 +1,60 @@ +package com.mapconductor.mapbox.marker + +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.mapbox.MapboxActualMarker +import com.mapconductor.settings.Settings + +class MapboxMarkerController( + override val renderer: MapboxMarkerOverlayRenderer, +) : AbstractMarkerController( + markerManager = renderer.markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: MarkerEntity? = null + + internal var selectedMarker: MarkerEntity? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + renderer.dragLayer.updatePosition(GeoPoint.from(it.state.position)) + // Restore the recomposition for the position property + setDraggingState(it.state, false) + renderer.drawDragLayer() + markerManager.registerEntity(it) + renderer.redraw() + } + return + } + internalSelectedMarker = value + markerManager.removeEntity(value.state.id) + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + renderer.dragLayer.selected = value + renderer.dragLayer.updatePosition(GeoPoint.from(value.state.position)) + renderer.redraw() + renderer.drawDragLayer() + } + get() = internalSelectedMarker + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = renderer.holder.map.cameraState.zoom + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt similarity index 68% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt index 5ac85b38..4940c9f9 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt @@ -6,50 +6,44 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerEntity import com.mapconductor.core.marker.MarkerIcon import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.mapbox.MapboxMapViewHolder import com.mapconductor.mapbox.toPoint import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class DefaultMapboxMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onRemove = onIconRemove, - onAdd = onIconAdd, - onChange = onIconChange, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - -class MapboxMarkerRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, - private val markerLayer: MarkerLayer, - private val dragLayer: MarkerDragLayer, -) : AbstractMarkerRenderer() { +class MapboxMarkerOverlayRenderer( + holder: MapboxMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), + val markerManager: MarkerManager, + val markerLayer: MarkerLayer = + MarkerLayer( + sourceId = "markers-source", + layerId = "markers-layer", + ), + val dragLayer: MarkerDragLayer = + MarkerDragLayer( + sourceId = "marker-drag-source", + layerId = "marker-drag-layer", + ), +) : AbstractMarkerOverlayRenderer< + MapboxMapViewHolder, + MapboxActualMarker, + >( + holder = holder, + coroutine = coroutine, + ) { private val iconRefCounter: MutableMap = mutableMapOf() + private val defaultIcon: BitmapIcon = DefaultIcon().toBitmapIcon() object Prop { const val ICON_ID = "icon_id" @@ -58,15 +52,14 @@ class MapboxMarkerRenderer( const val ICON_ANCHOR = "icon-offset" } - override fun init(markerOverlayManager: MarkerOverlayManager) { - super.init(markerOverlayManager) + init { holder.map.getStyle { style -> style.addImage(Prop.DEFAULT_MARKER_ID, defaultIcon.bitmap) } } fun redraw() { - val entities = markerOverlayManager.markerManager.allEntities() + val entities = markerManager.allEntities() coroutine.launch { markerLayer.draw(entities) } @@ -82,7 +75,7 @@ class MapboxMarkerRenderer( markerEntity: MarkerEntity, position: GeoPoint, ) { - val entities = markerOverlayManager.markerManager.allEntities() + val entities = markerManager.allEntities() val feature = Feature.fromGeometry( position.toPoint(), @@ -105,7 +98,7 @@ class MapboxMarkerRenderer( } } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { val style = suspendCoroutine { continuation -> holder.map.getStyle { style -> @@ -113,21 +106,24 @@ class MapboxMarkerRenderer( } } - newMarkers.forEach { (state, bitmapIcon) -> - val iconKey = state.icon.hashCode().toString() + data.forEach { + val iconKey = + it.state.icon + .hashCode() + .toString() if (!iconRefCounter.contains(iconKey)) { - style.addImage(iconKey, bitmapIcon.bitmap) + style.addImage(iconKey, it.bitmapIcon.bitmap) iconRefCounter[iconKey] = 0 } } - return newMarkers.map { (state, _) -> - val featureId = "marker-${state.id}" - val position = GeoPoint.from(state.position).toPoint() + return data.map { + val featureId = "marker-${it.state.id}" + val position = GeoPoint.from(it.state.position).toPoint() val properties = JsonObject().apply { - if (state.icon != null) { - state.icon?.let { icon -> + if (it.state.icon != null) { + it.state.icon?.let { icon -> val iconKey = icon.hashCode().toString() iconRefCounter[iconKey] = iconRefCounter.getOrDefault(iconKey, 0) + 1 addProperty(Prop.ICON_ID, iconKey) @@ -138,14 +134,14 @@ class MapboxMarkerRenderer( addProperty(Prop.ICON_ID, Prop.DEFAULT_MARKER_ID) add(Prop.ICON_ANCHOR, getDefaultIconOffsetProperty()) } - addProperty(Prop.SCALE, state.icon?.scale ?: 1.0) + addProperty(Prop.SCALE, it.state.icon?.scale ?: 1.0) } Feature.fromGeometry(position, properties, featureId) } } - override suspend fun removeIcons(removeEntities: List>) { - removeEntities.forEach { entity -> + override suspend fun onRemove(data: List>) { + data.forEach { entity -> entity.state.icon?.let { icon -> val iconKey = icon.hashCode().toString() val cnt = iconRefCounter.getOrDefault(iconKey, 1) - 1 @@ -159,17 +155,23 @@ class MapboxMarkerRenderer( } } - override suspend fun changeIcons(changes: List>): List = - changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint - val prevProperties = params.prevEntity.marker.properties() + override suspend fun onPostProcess() { + redraw() + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint + val prevProperties = params.prev.marker.properties() val properties = JsonObject().apply { addProperty( Prop.SCALE, - params.entity.state.icon + params.current.state.icon ?.scale ?: 1.0f, ) if (currFinger.icon == prevFinger.icon) { @@ -196,7 +198,7 @@ class MapboxMarkerRenderer( addProperty(Prop.ICON_ID, Prop.DEFAULT_MARKER_ID) add(Prop.ICON_ANCHOR, getDefaultIconOffsetProperty()) } else { - params.entity.state.icon?.let { icon -> + params.current.state.icon?.let { icon -> // icon id val iconKey = icon.hashCode().toString() if (iconRefCounter.contains(iconKey)) { @@ -213,8 +215,8 @@ class MapboxMarkerRenderer( } val position = - GeoPoint.from(params.entity.state.position).toPoint() - val featureId = "marker-${params.entity.state.id}" + GeoPoint.from(params.current.state.position).toPoint() + val featureId = "marker-${params.current.state.id}" Feature.fromGeometry(position, properties, featureId) } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt index 7b09cd89..5badc84b 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt @@ -4,13 +4,14 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.mapbox.toPoint class MarkerDragLayer( sourceId: String, layerId: String, ) : MarkerLayer(sourceId, layerId) { - var selected: MarkerEntity? = null + var selected: MarkerEntity? = null fun updatePosition(geoPoint: GeoPoint) { selected?.let { @@ -28,8 +29,8 @@ class MarkerDragLayer( it.state.id, ) it.marker = feature - listOf(feature) - } ?: emptyList() + listOf(feature) + } ?: emptyList() source.featureCollection( FeatureCollection.fromFeatures(features), diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt index 289a3e98..9048f3bf 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt @@ -17,16 +17,16 @@ open class MarkerLayer( ) { val layer = symbolLayer(layerId, sourceId) { - iconSize(Expression.get(MapboxMarkerRenderer.Prop.SCALE)) - iconImage(Expression.get(MapboxMarkerRenderer.Prop.ICON_ID)) +// iconSize(Expression.get(MapboxMarkerRenderer.Prop.SCALE)) + iconImage(Expression.get(MapboxMarkerOverlayRenderer.Prop.ICON_ID)) iconAllowOverlap(true) iconIgnorePlacement(true) iconAnchor(IconAnchor.TOP_LEFT) iconTranslateAnchor(IconTranslateAnchor.MAP) iconOffset( switchCase { - has(MapboxMarkerRenderer.Prop.ICON_ANCHOR) - get(MapboxMarkerRenderer.Prop.ICON_ANCHOR) + has(MapboxMarkerOverlayRenderer.Prop.ICON_ANCHOR) + get(MapboxMarkerOverlayRenderer.Prop.ICON_ANCHOR) literal(listOf(0.0, 0.0)) // center-middle }, ) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt new file mode 100644 index 00000000..8e9f9ba6 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -0,0 +1,95 @@ +package com.mapconductor.mapbox.polygon + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonEntityImpl +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineEntityImpl +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer + +class MapboxPolygonConductor( + val polygonOverlay: MapboxPolygonOverlayRenderer, + val polylineOverlay: MapboxPolylineOverlayRenderer, +) : OverlayController< + PolygonState, + PolygonEntity, + PolygonEvent, + > { + override val zIndex: Int = 2 + + override suspend fun add(data: List) { + data.forEach { polygonState -> + + polygonOverlay.createPolygon(polygonState)?.let { polygon -> + val polygonEntity = + PolygonEntityImpl( + polygon = polygon, + state = polygonState, + ) + polygonOverlay.polygonManager.registerEntity(polygonEntity) + } + + val polylineState = polygonState.toPolylineState() + polylineOverlay.createPolyline(polylineState)?.let { polyline -> + val polylineEntity = + PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) + polylineOverlay.polylineManager.registerEntity(polylineEntity) + } + } + polygonOverlay.onPostProcess() + polylineOverlay.onPostProcess() + } + + override suspend fun update(state: PolygonState) { + polygonOverlay.createPolygon(state)?.let { polygon -> + val polygonEntity = + PolygonEntityImpl( + polygon = polygon, + state = state, + ) + polygonOverlay.polygonManager.registerEntity(polygonEntity) + } + + val polylineState = state.toPolylineState() + polylineOverlay.createPolyline(polylineState)?.let { polyline -> + val polylineEntity = + PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) + polylineOverlay.polylineManager.registerEntity(polylineEntity) + } + polygonOverlay.onPostProcess() + polylineOverlay.onPostProcess() + } + + override var clickListener: ((PolygonEvent) -> Unit)? = null + + override fun find(position: IGeoPoint): PolygonEntity? = null + + override suspend fun clear() { + } +} + +private fun PolygonState.toPolylineState(): PolylineState { + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return PolylineState( + points = closedPoints, + id = "outline-${this.id}", + strokeColor = this.strokeColor, + strokeWidth = this.strokeWidth, + geodesic = this.geodesic, + extra = null, + ) +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt index b0362976..f1a5829d 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt @@ -12,8 +12,6 @@ class MapboxPolygonLayer( val layerId: String, ) { object Prop { - const val STROKE_COLOR = "strokeColor" - const val STROKE_WIDTH = "strokeWidth" const val FILL_COLOR = "fillColor" } @@ -25,17 +23,12 @@ class MapboxPolygonLayer( literal(Prop.FILL_COLOR) }, ) - fillOutlineColor( - get { - literal(Prop.STROKE_COLOR) - }, - ) } fun draw(entities: List>) { val features = entities.map { it.polygon } source.featureCollection( - FeatureCollection.fromFeatures(features), + FeatureCollection.fromFeatures(features.flatten()), ) } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt new file mode 100644 index 00000000..15f076d7 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt @@ -0,0 +1,97 @@ +package com.mapconductor.mapbox.polygon + +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.Polygon +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.mapbox.MapboxActualPolygon +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.toMapboxColorString +import com.mapconductor.mapbox.toPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxPolygonOverlayRenderer( + val layer: MapboxPolygonLayer, + val polygonManager: PolygonManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolygonOverlayRenderer() { + override suspend fun onRemove(data: List>) { +// val featureIds = data.map { entity -> +// entity.polygon.getStringProperty("id") +// } +// layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun onPostProcess() { + val polygons = getAllPolygonEntities() + coroutine.launch { + this@MapboxPolygonOverlayRenderer.layer.draw(polygons) + } + } + + override suspend fun removePolygon(entity: PolygonEntity) { +// val featureIds = +// listOf(entity.polygon.getStringProperty("id")) +// layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun createPolygon(state: PolygonState): MapboxActualPolygon? { + val points = state.points.map { GeoPoint.from(it).toPoint() } + // Close the polygon by adding the first point at the end if not already closed + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return listOf( + Feature.fromGeometry( + Polygon.fromLngLats(listOf(closedPoints)), + JsonObject().apply { + addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + }, + "polygon-${state.id}", + ), + ) + } + + override suspend fun updatePolygonProperties( + polygon: MapboxActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): MapboxActualPolygon? { +// val state = current.state +// val points = state.points.map { GeoPoint.from(it).toPoint() } +// // Close the polygon by adding the first point at the end if not already closed +// val closedPoints = +// if (points.first() != points.last()) { +// points + points.first() +// } else { +// points +// } +// val feature = Feature.fromGeometry( +// Polygon.fromLngLats(listOf(closedPoints)), +// JsonObject().apply { +// addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) +// addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) +// addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) +// }, +// "polygon-${state.id}", +// ) +// layer.source.updateGeoJSONSourceFeatures(listOf(feature)) + return prev.polygon + } + + private fun getAllPolygonEntities(): List> { + // This would need access to the polygon manager + // For now, we'll implement a simple workaround + return polygonManager.allEntities() + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt deleted file mode 100644 index b39e00d4..00000000 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.mapconductor.mapbox.polygon - -import com.google.gson.JsonObject -import com.mapbox.geojson.Feature -import com.mapbox.geojson.Polygon -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.mapbox.MapboxActualPolygon -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.toMapboxColorString -import com.mapconductor.mapbox.toPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultMapboxPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class MapboxPolygonRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, - private val layer: MapboxPolygonLayer, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { - val polygons = - newPolygons.map { state -> - val points = state.points.map { GeoPoint.from(it).toPoint() } - // Close the polygon by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) - } - return polygons - } - - override suspend fun removePolygons(removeEntities: List>) { - val featureIds = removeEntities.map { "polygon-${it.state.id}" } - layer.source.removeGeoJSONSourceFeatures(featureIds) - } - - override suspend fun changePolygon(changes: List>): List { - val features = - changes.map { params -> - val state = params.entity.state - val points = state.points.map { GeoPoint.from(it).toPoint() } - // Close the polygon by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) - } - layer.source.updateGeoJSONSourceFeatures(features) - return features - } - - fun redraw() { - val polygons = polygonOverlayManager.getAllEntities() - coroutine.launch { - layer.draw(polygons) - } - } -} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt new file mode 100644 index 00000000..e2fbd223 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -0,0 +1,10 @@ +package com.mapconductor.mapbox.polyline + +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.mapbox.MapboxActualPolyline + +class MapboxPolylineController( + override val renderer: MapboxPolylineOverlayRenderer, + polylineManager: PolylineManager = renderer.polylineManager, +) : PolylineController(polylineManager, renderer) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt new file mode 100644 index 00000000..e213b5b7 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt @@ -0,0 +1,66 @@ +package com.mapconductor.mapbox.polyline + +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.MapboxActualPolyline +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.createMapboxLines +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxPolylineOverlayRenderer( + val layer: MapboxPolylineLayer, + val polylineManager: PolylineManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = + createMapboxLines( + id = state.id, + points = state.points, + geodesic = state.geodesic, + strokeColor = state.strokeColor, + strokeWidth = state.strokeWidth, + ) + + override suspend fun updatePolylineProperties( + polyline: MapboxActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): MapboxActualPolyline? { + // For Mapbox, we need to recreate the features when properties change + return createMapboxLines( + id = current.state.id, + points = current.state.points, + geodesic = current.state.geodesic, + strokeColor = current.state.strokeColor, + strokeWidth = current.state.strokeWidth, + ) + } + + override suspend fun removePolyline(entity: PolylineEntity) { + val featureIds = + entity.polyline.map { feature -> + feature.getStringProperty("id") + } + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun onPostProcess() { + // Redraw all polylines on the layer + val polylines = getAllPolylineEntities() + coroutine.launch { + layer.draw(polylines) + } + } + + private fun getAllPolylineEntities(): List> { + // This would need access to the polyline manager + // For now, we'll implement a simple workaround + return polylineManager.allEntities() + } +} 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 deleted file mode 100644 index ada810d1..00000000 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.mapconductor.mapbox.polyline - -import com.google.gson.JsonObject -import com.mapbox.geojson.Feature -import com.mapbox.geojson.LineString -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.features.normalize -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.core.splitByMeridian -import com.mapconductor.mapbox.MapboxActualPolyline -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.toMapboxColorString -import com.mapconductor.mapbox.toPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultMapboxPolylineRenderer : 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 MapboxPolylineRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, - private val layer: MapboxPolylineLayer, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List = - newLines.map { state -> createMapboxLines(state) } - - private fun createMapboxLines(state: PolylineState): List { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - }.map { it.normalize() } - return splitByMeridian(geoPoints, state.geodesic).mapIndexed { index, linePoints -> - val points = linePoints.map { GeoPoint.from(it).toPoint() } - val id = "polyline-${state.id}-$index" - - return@mapIndexed Feature.fromGeometry( - LineString.fromLngLats(points), - JsonObject().apply { - addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty("id", id) - }, - id, - ) - } - } - - override suspend fun removePolylines(removeEntities: List>) { - val featureIds = - removeEntities.map { entity -> - entity.polyline.map { feature -> - feature.getStringProperty("id") - } - } - layer.source.removeGeoJSONSourceFeatures(featureIds.flatten()) - } - - override suspend fun changePolylines( - changes: List>, - ): List = changes.map { params -> createMapboxLines(params.entity.state) } - - fun redraw() { - val polylines = polylineOverlayManager.getAllEntities() - coroutine.launch { - layer.draw(polylines) - } - } -}