diff --git a/README.md b/README.md index bc78d6ca..e0f4433a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ KtLintに従います。ローカルで実行する場合は、下記コマン | Map | ☑ | ☑ | ☑ | ☑ | | Marker | ☑ | ☑ | ☑ | ☑ | | Circle | ☑ | ☑ | ☐ | ☐ (wip) | -| Polyline | ☐ | ☐ | ☐ | ☐ | -| Polygon | ☐ | ☐ | ☐ | ☐ | -| GroundOverlay | ☐ | ☐ | ☐ | ☐ | -| RasterTileLayer | ☐ | ☐ | ☐ | ☐ | -| VectorTileLayer | ☐ | ☐ | ☐ | ☐ | +| Polyline | ☑ | ☑ | ☑ | ☑ | +| Polygon | ☐ | ☐ | ☐ | ☐ | +| GroundOverlay | ☐ | ☐ | ☐ | ☐ | +| RasterTileLayer | ☐ | ☐ | ☐ | ☐ | +| VectorTileLayer | ☐ | ☐ | ☐ | ☐ | 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 3033ba56..0eb14b27 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -3,7 +3,9 @@ package com.mapconductor.example import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Text @@ -17,8 +19,11 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage +import com.mapconductor.example.pages.groundimage.GroundImageMapPage +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.polyline.PolylineMapPage import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar @@ -42,6 +47,14 @@ fun DemoAppScreen() { ) } + val groundImageResources = + remember { + GroundImageResources( + image = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, + clickedImage = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_1)!!, + ) + } + val sidebarItems = listOf( SidebarItem( @@ -62,12 +75,24 @@ fun DemoAppScreen() { icon = Icons.Default.CheckCircle, route = "circle", ), + SidebarItem( + id = "groundImage", + title = "GroundImage", + icon = Icons.Default.Favorite, + route = "groundImage", + ), SidebarItem( id = "polyline", title = "Polyline ", icon = Icons.Default.PlayArrow, route = "polyline", ), + SidebarItem( + id = "mapDesign", + title = "MapDesign Demo", + icon = Icons.Default.Build, + route = "mapDesign", + ), // SidebarItem( // id = "examples", // title = "Map Examples", @@ -108,6 +133,17 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "groundImage" -> { + GroundImageMapPage( + groundImageResources = groundImageResources, + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } + "mapDesign" -> { + MapDesignMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "settings" -> { // Placeholder for settings page Box( diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index a1cafa27..36b09250 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -6,6 +6,7 @@ import com.mapconductor.arcgis.ArcGISMapView import com.mapconductor.arcgis.ArcGISMapViewState 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 @@ -30,6 +31,7 @@ fun MapViewContainer( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onGroundImageClick: OnGroundImageEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { when (state) { @@ -46,6 +48,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onGroundImageClick = onGroundImageClick, content = content, ) @@ -60,6 +63,7 @@ fun MapViewContainer( onMarkerDragEnd = onMarkerDragEnd, onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, + onGroundImageClick = onGroundImageClick, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, content = content, @@ -78,6 +82,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onGroundImageClick = onGroundImageClick, content = content, ) @@ -93,6 +98,7 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onGroundImageClick = onGroundImageClick, onPolylineClick = onPolylineClick, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt index e771adcf..0ed71062 100644 --- a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel class NavigationViewModel : ViewModel() { - private val _currentPage = mutableStateOf("flyTo") + private val _currentPage = mutableStateOf("groundImage") val currentPage: State = _currentPage private val _isSidebarExpanded = mutableStateOf(false) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index 56cb301a..7d983ae6 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 @@ -31,9 +31,7 @@ fun CircleMapComponent( onMarkerDrag = onMarkerDrag, ) { // Circle - key(viewModel.circleState.id) { - Circle(viewModel.circleState) - } + Circle(viewModel.circleState) // Center marker (not draggable) key(viewModel.centerMarker.id) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt new file mode 100644 index 00000000..7d1e2620 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -0,0 +1,36 @@ +package com.mapconductor.example.pages.groundimage + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mapconductor.core.groundimage.GroundImage +import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.example.MapViewContainer + +@Composable +fun GroundImageMapComponent( + mapViewState: MapViewState<*>?, + viewModel: GroundImageMapPageViewModel, + modifier: Modifier = Modifier, + onGroundImageClick: OnGroundImageEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler = {}, +) { + mapViewState?.let { mapViewState -> + MapViewContainer( + modifier = modifier, + state = mapViewState, + onGroundImageClick = onGroundImageClick, + onMarkerDrag = onMarkerDrag, + ) { + // GroundImage + GroundImage(viewModel.groundImageState) + + // BoundsMarkers + viewModel.markers.forEach { marker -> + Marker(marker) + } + } + } +} 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 new file mode 100644 index 00000000..a6fce2c2 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -0,0 +1,95 @@ +package com.mapconductor.example.pages.groundimage + +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.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.MessageCard + +@Composable +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") + } + }, + ) + + DemoMapPageScaffold( + initCameraPosition = viewModel.initCameraPosition, + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + GroundImageMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + onGroundImageClick = viewModel::onGroundImageClick, + onMarkerDrag = viewModel::onMarkerDrag, + ) + + // 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 = "Messages", + ) { + Column { + Text("opacity: ${"%.2f".format(viewModel.opacity)}", color = Color.Black) + Slider( + value = viewModel.opacity, + onValueChange = { newValue -> + viewModel.opacity = newValue + }, + valueRange = 0.0f..1.0f, // スライダー範囲 + steps = 0, + colors = + SliderDefaults.colors( + thumbColor = Color.Black, // つまみの色 + activeTrackColor = Color.DarkGray, // 値までのトラック + inactiveTrackColor = Color.LightGray, // 残りのトラック + activeTickColor = Color.Black, // 有効ステップの目盛り + inactiveTickColor = Color.White, // 無効ステップの目盛り + ), + ) + } + } + + ToastHost( + messages = viewModel.messages.collectAsState().value, + onDismiss = { viewModel.removeToast(it) }, + ) + } +} 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/GroundImagePageViewModel.kt new file mode 100644 index 00000000..4047338c --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImagePageViewModel.kt @@ -0,0 +1,197 @@ +package com.mapconductor.example.pages.groundimage + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.GroundImageEvent +import com.mapconductor.core.groundimage.GroundImageState +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.example.toast.ToastMessage +import android.graphics.drawable.Drawable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface GroundImageMapPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val markers: List + val imageResources: GroundImageResources + val image: Drawable + var opacity: Float + val groundImageState: GroundImageState + + fun onMapViewChanged(state: MapViewState<*>) + + fun onGroundImageClick(clicked: GroundImageEvent) + + fun onMarkerDrag(dragged: MarkerState) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class GroundImageMapPageViewModelImpl( + override val imageResources: GroundImageResources, +) : ViewModel(), + GroundImageMapPageViewModel { + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 40.7430785, + longitude = -74.175995, + ), + zoom = 12.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + } + + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + private var southWestPosition by mutableStateOf( + GeoPoint( + latitude = 40.712216, + longitude = -74.22655, + ), + ) + + private var northEastPosition by mutableStateOf( + GeoPoint( + latitude = 40.773941, + longitude = -74.12544, + ), + ) + + private fun calculateMarkerLabels(): Pair { + val swLat = southWestPosition.latitude + val swLng = southWestPosition.longitude + val neLat = northEastPosition.latitude + val neLng = northEastPosition.longitude + + val southWestLabel = + when { + swLat <= neLat && swLng <= neLng -> "SW" // Normal + swLat <= neLat && swLng > neLng -> "SE" // East-West flipped + swLat > neLat && swLng <= neLng -> "NW" // North-South flipped + else -> "NE" // Both flipped + } + + val northEastLabel = + when { + neLat >= swLat && neLng >= swLng -> "NE" // Normal + neLat >= swLat && neLng < swLng -> "NW" // East-West flipped + neLat < swLat && neLng >= swLng -> "SE" // North-South flipped + else -> "SW" // Both flipped + } + + return Pair(southWestLabel, northEastLabel) + } + + override val markers: List + get() { + val (swLabel, neLabel) = calculateMarkerLabels() + return listOf( + MarkerState( + id = "south_west", + position = southWestPosition, + icon = + DefaultIcon( + fillColor = Color.Blue, + strokeColor = Color.White, + label = swLabel, + labelTextColor = Color.White, + ), + draggable = true, + ), + MarkerState( + id = "north_east", + position = northEastPosition, + icon = + DefaultIcon( + fillColor = Color.Red, + strokeColor = Color.White, + label = neLabel, + labelTextColor = Color.White, + ), + draggable = true, + ), + ) + } + + override var opacity by mutableStateOf(0.5f) + + override var image by mutableStateOf(imageResources.image) + + private var bounds by mutableStateOf( + GeoRectBounds( + southWest = GeoPoint.from(southWestPosition), + northEast = GeoPoint.from(northEastPosition), + ), + ) + + override val groundImageState + get() = + GroundImageState( + id = "groundImage", + bounds = bounds, + image = image, + opacity = opacity, + ) + + override fun onGroundImageClick(clicked: GroundImageEvent) { + if (clicked.state.image == imageResources.image) { + image = imageResources.clickedImage + } else { + image = imageResources.image + } + + showToast("Ground Image clicked.") + } + + override fun onMarkerDrag(dragged: MarkerState) { + // Update the internal position based on which marker was dragged + when (dragged.id) { + "south_west" -> southWestPosition = GeoPoint.from(dragged.position) + "north_east" -> northEastPosition = GeoPoint.from(dragged.position) + } + + // Update bounds using the new positions + bounds = + GeoRectBounds().also { + it.extend(markers[0].position) + it.extend(markers[1].position) + } + } + + override fun showToast(text: String) { + this._messages.value = this._messages.value + ToastMessage(text = text) + } + + override fun removeToast(toastMessage: ToastMessage) { + this._messages.value = this._messages.value.filter { it != toastMessage } + } + + override fun onCleared() { + super.onCleared() + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt new file mode 100644 index 00000000..6d356daf --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt @@ -0,0 +1,8 @@ +package com.mapconductor.example.pages.groundimage + +import android.graphics.drawable.Drawable + +data class GroundImageResources( + val image: Drawable, + val clickedImage: Drawable, +) 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 new file mode 100644 index 00000000..46746739 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt @@ -0,0 +1,19 @@ +package com.mapconductor.example.pages.mapDesign + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.MapViewContainer + +@Composable +fun MapDesignMapComponent( + mapViewState: MapViewState<*>?, + 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 new file mode 100644 index 00000000..472456b5 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt @@ -0,0 +1,156 @@ +package com.mapconductor.example.pages.mapDesign + +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +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 +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +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.ArcGISMapViewState +import com.mapconductor.core.map.MapDesignType +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard +import com.mapconductor.googlemaps.GoogleMapDesign +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 + +@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 optionsState = viewModel.options.collectAsState() + + DemoMapPageScaffold( + initCameraPosition = 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) } + } + } + + 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" + } + + var selectedLabel by rememberSaveable(sdkKey) { + // SDKごとに独立して保存 + mutableStateOf(items.firstOrNull()?.label ?: "") + } + + // ★ options(候補リスト)が変わったら選択と展開状態をリセット + LaunchedEffect(items) { + selectedLabel = items.firstOrNull()?.label ?: "" + expanded = false + } + + val enabled = mapViewState.value != null && items.isNotEmpty() + + 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") }, + ) + 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 new file mode 100644 index 00000000..a0f5ed07 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt @@ -0,0 +1,205 @@ +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 +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.mapbox.MapboxMapDesign +import com.mapconductor.mapbox.MapboxMapViewState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class MapDesignOptions( + val label: String, + val design: MapDesignType<*>, +) + +interface MapDesignPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val options: StateFlow> + + fun onMapViewChanged(state: MapViewState<*>) + + fun onMapClick(clicked: GeoPoint) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class MapDesignPageViewModelImpl : + ViewModel(), + MapDesignPageViewModel { + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 12.0, + bearing = 0.0, + tilt = 0.0, + 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() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + when (state) { + is GoogleMapViewState -> { + _options.value = googleMapDesigns + } + is HereMapViewState -> { + _options.value = hereMapDesigns + } + is MapboxMapViewState -> { + _options.value = mapboxMapDesigns + } + is ArcGISMapViewState -> { + _options.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), + ) + + 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), + ) + + 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), + ) + + 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(label = "HumanGeographyDarkDetail", design = + ArcGISDesign.Companion.HumanGeographyDarkDetail), + MapDesignOptions(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), + ) +} 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 0e48e220..ce59dcbd 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 @@ -154,21 +154,6 @@ fun DemoMapPageScaffold( ) } } - -// // 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 = "Messages", -// ) { -// MapViewStatePanel(selectedMapViewState.mapCameraPosition.collectAsState().value) -// } } } } diff --git a/example-app/src/main/res/drawable/newark_nj_1922_0.jpg b/example-app/src/main/res/drawable/newark_nj_1922_0.jpg new file mode 100644 index 00000000..1f4ae595 Binary files /dev/null and b/example-app/src/main/res/drawable/newark_nj_1922_0.jpg differ diff --git a/example-app/src/main/res/drawable/newark_nj_1922_1.jpg b/example-app/src/main/res/drawable/newark_nj_1922_1.jpg new file mode 100644 index 00000000..c025cf69 Binary files /dev/null and b/example-app/src/main/res/drawable/newark_nj_1922_1.jpg differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b82ae1aa..c675bf42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,6 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 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" } -ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } [plugins] 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 f21deb94..dc351e37 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.MapViewControllerAlias +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 @@ -64,6 +66,7 @@ open class MapViewScope { val polylineFlow = MutableStateFlow>(emptyList()) val circleFlow = MutableStateFlow>(emptyList()) val polygonFlow = MutableStateFlow>(emptyList()) + val groundImageFlow = MutableStateFlow>(emptyList()) fun buildRegistry(): MapOverlayRegistry { val registry = MapOverlayRegistry() @@ -71,6 +74,7 @@ open class MapViewScope { registry.register(CircleOverlay(circleFlow)) registry.register(PolylineOverlay(polylineFlow)) // registry.register(PolygonOverlay(polygonFlow)) // TODO: Implement addPolygons in MapViewController + registry.register(GroundImageOverlay(groundImageFlow)) return registry } } @@ -140,6 +144,11 @@ val LocalCircleCollector = // error("Polygon must be under the ") // } +val LocalGroundImageCollector = + compositionLocalOf>> { + error("GroundImage must be under the ") + } + @Composable fun CollectAndRenderOverlays( registry: MapOverlayRegistry, @@ -151,7 +160,9 @@ fun CollectAndRenderOverlays( LaunchedEffect(Unit) { typedOverlay.flow.collect { - typedOverlay.render(it, controller) + if (it.isNotEmpty()) { + typedOverlay.render(it, controller) + } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt index 30ed1ce4..ec5c710d 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt @@ -1,8 +1,7 @@ package com.mapconductor.core.circle import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -12,9 +11,8 @@ import android.os.Parcelable @Composable fun MapViewScope.Circle(state: CircleState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - circleFlow.value = circleFlow.value.filter { it.id != rememberState.id } + rememberState + LaunchedEffect(state.fingerPrint()) { + circleFlow.value = circleFlow.value.filter { it.id != state.id } + state } } 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 b018c723..6fccbc27 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 @@ -4,6 +4,7 @@ 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.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler @@ -67,6 +68,52 @@ 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 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 4be64fab..74b61ca6 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 @@ -1,8 +1,5 @@ package com.mapconductor.core.features -import kotlin.math.max -import kotlin.math.min - data class GeoRectBounds( var southWest: GeoPoint? = null, var northEast: GeoPoint? = null, @@ -11,38 +8,68 @@ data class GeoRectBounds( get() = southWest == null || northEast == null fun extend(point: IGeoPoint) { - val target = GeoPoint.from(point) - - if (isEmpty) { - southWest = target - northEast = target - return - } - - val sw = southWest!! - val ne = northEast!! - - val south = min(sw.latitude, target.latitude) - val north = max(ne.latitude, target.latitude) - - var west = sw.longitude - var east = ne.longitude + val position = GeoPoint.from(point) + + when { + // まだ何もない:両方に同じ点を入れて初期化 + southWest == null && northEast == null -> { + southWest = position + northEast = position + return + } - val lon = target.longitude + // southWest だけある:既存点と position の2点から SW/NE を決める + southWest != null && northEast == null -> { + val sw = southWest!! + val south = minOf(sw.latitude, position.latitude) + val north = maxOf(sw.latitude, position.latitude) + // 1点ずつなので子午線跨ぎの判定は不要。単純に min/max でOK + val west = minOf(sw.longitude, position.longitude) + val east = maxOf(sw.longitude, position.longitude) + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) + return + } - if (!containsLongitude(lon, west, east)) { - val distToEast = distanceEast(lon, east) - val distToWest = distanceWest(west, lon) + // northEast だけある:既存点と position の2点から SW/NE を決める + southWest == null && northEast != null -> { + val ne = northEast!! + val south = minOf(ne.latitude, position.latitude) + val north = maxOf(ne.latitude, position.latitude) + val west = minOf(ne.longitude, position.longitude) + val east = maxOf(ne.longitude, position.longitude) + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) + return + } - if (distToEast < distToWest) { - east = lon - } else { - west = lon + else -> { + // どちらもある:従来ロジック(子午線跨ぎ考慮)+ 緯度/経度の参照を修正 + val south = minOf(position.latitude, southWest!!.latitude) + val north = maxOf(position.latitude, northEast!!.latitude) + + var west = southWest!!.longitude + var east = northEast!!.longitude + + if (west > 0 && east < 0) { + // すでに経度が + と - に分かれている=日付変更線跨ぎの矩形 + if (position.longitude > 0) { + west = minOf(position.longitude, west) + } else { + east = maxOf(position.longitude, east) + } + } else { + // 通常ケース:単純に min/max + west = minOf(position.longitude, southWest!!.longitude) + east = maxOf(position.longitude, northEast!!.longitude) + } + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) } } - - southWest = GeoPoint(south, west) - northEast = GeoPoint(north, east) } private fun distanceEast( 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 new file mode 100644 index 00000000..0f7068f3 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -0,0 +1,74 @@ +package com.mapconductor.core.groundimage + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import android.graphics.drawable.Drawable +import android.os.Parcelable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +class GroundImageState( + bounds: GeoRectBounds, + image: Drawable, + opacity: Float = 1.0f, + id: String? = null, + extra: Parcelable? = null, +) { + val id = (id ?: generateId(bounds, image, opacity, extra)).toString() + +// var bounds by StateFlowDelegate(bounds) + var bounds by mutableStateOf(bounds) + var image by mutableStateOf(image) + var opacity by mutableStateOf(opacity) + var extra by mutableStateOf(extra) + + fun fingerPrint(): GroundImageFingerPrint = + GroundImageFingerPrint( + id = id.hashCode(), + bounds = bounds.hashCode(), + image = image.hashCode(), + opacity = opacity.hashCode(), + extra = extra?.hashCode() ?: 0, + ) + + fun asFlow(): Flow = + snapshotFlow { + fingerPrint() + }.distinctUntilChanged() + + private fun generateId( + bounds: GeoRectBounds, + image: Drawable, + opacity: Float, + extra: Parcelable?, + ): Int { + var result = bounds.hashCode() + result = 31 * result + image.hashCode() + result = 31 * result + opacity.hashCode() + result = 31 * result + (extra?.hashCode() ?: 0) + return result + } + + override fun equals(other: Any?): Boolean = (other as? GroundImageState)?.hashCode() == this.hashCode() + + override fun hashCode(): Int = fingerPrint().hashCode() +} + +data class GroundImageFingerPrint( + val id: Int, + val bounds: Int, + val image: Int, + val opacity: Int, + val extra: Int, +) + +data class GroundImageEvent( + val state: GroundImageState, + val position: GeoPoint?, +) + +typealias OnGroundImageEventHandler = (GroundImageEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt new file mode 100644 index 00000000..06c84bb6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -0,0 +1,34 @@ +package com.mapconductor.core.groundimage + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.mapconductor.core.MapViewScope +import com.mapconductor.core.features.GeoRectBounds +import android.graphics.drawable.Drawable +import android.os.Parcelable + +@Composable +fun MapViewScope.GroundImage(state: GroundImageState) { + LaunchedEffect(state.fingerPrint()) { + groundImageFlow.value = groundImageFlow.value.filter { it.id != state.id } + state + } +} + +@Composable +fun MapViewScope.GroundImage( + bounds: GeoRectBounds, + image: Drawable, + opacity: Float = 0.5f, + id: String? = null, + extra: Parcelable? = null, +) { + val state = + GroundImageState( + bounds = bounds, + image = image, + opacity = opacity, + id = id, + extra = extra, + ) + GroundImage(state) +} 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 new file mode 100644 index 00000000..0a9d34b1 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -0,0 +1,141 @@ +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>, + 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 removed = mutableListOf>() + + data.forEach { + if (previous.contains(it.id)) { + val prevEntity = entities[it.id]!! + updated.add( + object : OverlayRenderer.Changes> { + override val current = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = it, + ) + override val prev = prevEntity + }, + ) + previous.remove(it.id) + } else { + added.add(it) + previous.remove(it.id) + } + } + + previous.forEach { remainId -> + entities.remove(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + 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, + ) + entities[state.id] = entity + } + } + } + + if (updated.isNotEmpty()) { + val actualOverlays = renderer.onChange(updated.toList()) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = updated[index].current.state + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + entities[state.id] = 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 actualOverlays: List = renderer.onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + entities[state.id] = entity + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + renderer.onRemove(entities.values.toList()) + entities.clear() + } + } + + override fun find(position: IGeoPoint): GroundImageEntity? { + // TODO: Improve this implementation later + return entities.values.find { entity -> + entity.state.bounds.contains(position) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt new file mode 100644 index 00000000..f8b6e8fd --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt @@ -0,0 +1,14 @@ +package com.mapconductor.core.groundimage + +interface GroundImageEntity { + val groundImage: ActualGroundImage + val state: GroundImageState + val fingerPrint: GroundImageFingerPrint +} + +data class GroundImageEntityImpl( + override val groundImage: ActualGroundImage, + override val state: GroundImageState, +) : GroundImageEntity { + override val fingerPrint: GroundImageFingerPrint = state.fingerPrint() +} 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 new file mode 100644 index 00000000..5ed59d24 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.StateFlow + +class GroundImageOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + (controller as? GroundImageCapable<*>)?.compositionGroundImages(data) + } +} 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 85e26f1c..17e312aa 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 @@ -23,12 +23,14 @@ 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.features.GeoPoint +import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon @@ -86,6 +88,7 @@ fun < LocalInfoBubbleCollector provides scope.bubbleFlow, LocalCircleCollector provides scope.circleFlow, LocalPolylineCollector provides scope.polylineFlow, + LocalGroundImageCollector provides scope.groundImageFlow, ) { with(scope) { content?.invoke(this) @@ -115,6 +118,16 @@ fun < } } } + (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) + } + } + } + } } Box( 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 0bcf093b..e22ccd79 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 @@ -17,7 +17,7 @@ enum class InitState { Failed, } -interface MapViewState { +interface MapViewState { interface MoveCameraCallback { fun onComplete(result: Boolean) } @@ -26,12 +26,16 @@ interface MapViewState { val initCameraPosition: MapCameraPosition val isInitialized: StateFlow val cameraPosition: StateFlow - val mapDesignType: MapDesignType + var mapDesignType: ActualMapDesignType fun initAsync(init: suspend () -> Boolean) fun resetInitState() + fun changeMapDesignType( + value: ActualMapDesignType + ) + fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long = 0, 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 948ff660..ddc22005 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 @@ -1,17 +1,15 @@ package com.mapconductor.core.marker import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.LaunchedEffect import com.mapconductor.core.MapViewScope import com.mapconductor.core.features.IGeoPoint import android.os.Parcelable @Composable fun MapViewScope.Marker(state: MarkerState) { - val rememberMarker = remember(state.fingerPrint()) { state } - SideEffect { - markerFlow.value = markerFlow.value.filter { it.id != rememberMarker.id } + rememberMarker + LaunchedEffect(state.fingerPrint()) { + markerFlow.value = markerFlow.value.filter { it.id != state.id } + state } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt index 9fcecf80..d3e8bef5 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt @@ -1,8 +1,7 @@ package com.mapconductor.core.polyline import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -12,9 +11,8 @@ import android.os.Parcelable @Composable fun MapViewScope.Polyline(state: PolylineState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - polylineFlow.value = polylineFlow.value.filter { it.id != rememberState.id } + rememberState + LaunchedEffect(state.fingerPrint()) { + polylineFlow.value = polylineFlow.value.filter { it.id != state.id } + state } } 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 8909c017..a5b079eb 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 @@ -30,7 +30,7 @@ data class ArcGISDesign( val Navigation = ArcGISDesign("arc_gis_navigation") val NavigationNight = ArcGISDesign("arc_gis_navigation_night") val StreetsNight = ArcGISDesign("arc_gis_streets_night") - val StreetsRelief = ArcGISDesign("arc_gis_Streets_relief") + val StreetsRelief = ArcGISDesign("arc_gis_streets_relief") val Topographic = ArcGISDesign("arc_gis_topographic") val Oceans = ArcGISDesign("arc_gis_oceans") val OceansBase = ArcGISDesign("arc_gis_oceans_base") @@ -53,7 +53,7 @@ data class ArcGISDesign( val ModernAntiqueBase = ArcGISDesign("arc_gis_modern_antique_base") val HumanGeography = ArcGISDesign("arc_gis_human_geography") val HumanGeographyBase = ArcGISDesign("arc_gis_human_geography_base") - val HumanGeographyDetail = ArcGISDesign("arc_gis_humanGeography_detail") + val HumanGeographyDetail = ArcGISDesign("arc_gis_human_geography_detail") val HumanGeographyLabels = ArcGISDesign("arc_gis_human_geography_labels") val HumanGeographyDark = ArcGISDesign("arc_gis_human_geography_dark") val HumanGeographyDarkBase = ArcGISDesign("arc_gis_human_geography_dark_base") 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 dc8c31c7..9add681d 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,6 +7,7 @@ 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 @@ -25,6 +26,7 @@ fun ArcGISMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { 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 ec34c923..f982460d 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 @@ -44,6 +44,7 @@ import android.view.MotionEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import com.arcgismaps.mapping.Basemap interface IArcGISMapViewController : MapViewController< @@ -52,6 +53,10 @@ interface IArcGISMapViewController : ArcGISActualPolyline, ArcGISActualPolygon, > { + fun changeMapDesign( + value: String + ) + fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -339,6 +344,20 @@ class ArcGISMapViewController( 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?, diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt index 92633b09..63e58f6e 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt @@ -9,6 +9,7 @@ import com.mapconductor.core.map.BaseMapViewSaver import com.mapconductor.core.map.IMapCameraPosition import com.mapconductor.core.map.InitState import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapDesignType import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapPaddingsImpl import com.mapconductor.core.map.MapViewState @@ -22,14 +23,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IArcGISMapViewState : MapViewState - class ArcGISMapViewState( override val id: String, override val initCameraPosition: MapCameraPosition, - override val mapDesignType: ArcGISDesign, -) : MapViewStateImpl(), - IArcGISMapViewState { + override var mapDesignType: ArcGISDesign, +) : MapViewStateImpl() + { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() @@ -40,6 +39,11 @@ class ArcGISMapViewState( private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + override fun changeMapDesignType(value: ArcGISDesign) { + this.mapDesignType = value + this.controller?.changeMapDesign(value.getValue()) + } + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt new file mode 100644 index 00000000..408e7492 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt @@ -0,0 +1,22 @@ +package com.mapconductor.googlemaps + +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds + +fun GeoRectBounds.toLatLngBounds(): LatLngBounds? { + val sw = southWest ?: return null + val ne = northEast ?: return null + + return LatLngBounds( + LatLng(sw.latitude, sw.longitude), + LatLng(ne.latitude, ne.longitude), + ) +} + +fun LatLngBounds.toGeoRectBounds(): GeoRectBounds = + GeoRectBounds( + southWest = GeoPoint(southwest.latitude, southwest.longitude), + northEast = GeoPoint(northeast.latitude, northeast.longitude), + ) 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 new file mode 100644 index 00000000..68769060 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt @@ -0,0 +1,13 @@ +package com.mapconductor.googlemaps + +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.GroundOverlay +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 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 c960ad95..3dc97f44 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 @@ -13,6 +13,7 @@ import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.model.CameraPosition import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -32,6 +33,7 @@ fun GoogleMapsView( onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, onPolylineClick: OnPolylineEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = {}, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -52,7 +54,7 @@ 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 { CameraPosition .Builder() .apply { @@ -87,6 +89,7 @@ fun GoogleMapsView( controller.setPolylineClickListener(onPolylineClick) controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnGroundImageClickListener(onGroundImageClick) 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 1b07699d..c103e27c 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 @@ -22,6 +22,11 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController 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 @@ -48,7 +53,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IGoogleMapViewController : MapViewController { +interface IGoogleMapViewController : + MapViewController, + GroundImageCapable { + fun changeMapDesign( + value: Int + ) + fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -73,7 +84,13 @@ class GoogleMapViewController( private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), -) : BaseMapViewController(), + private val groundImageController: GroundImageController, +) : BaseMapViewController< + ActualGoogleMapMarker, + ActualGoogleMapCircle, + ActualGoogleMapPolyline, + ActualGoogleMapPolygon, + >(), IGoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, @@ -163,6 +180,14 @@ class GoogleMapViewController( holder.map.setOnMarkerDragListener(this) } + override fun changeMapDesign( + value: Int + ){ + coroutine.launch { + holder.map.mapType = value + } + } + override fun moveCamera( position: MapCameraPosition, listener: MapViewState.MoveCameraCallback?, @@ -269,6 +294,18 @@ class GoogleMapViewController( 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()) } } @@ -307,4 +344,12 @@ class GoogleMapViewController( 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 + } } 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 6253853f..ced55963 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,8 +3,10 @@ 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.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -28,8 +30,19 @@ object GoogleMapViewControllerStore : StaticHolder() { options = options, ) + val groundImageRenderer = + GoogleMapGroundImageRenderer( + holder = holder, + ) + + val groundImageController = + GroundImageController( + renderer = groundImageRenderer, + ) + val controller = GoogleMapViewController( + groundImageController = groundImageController, holder = holder, ) this.set(id, controller) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt index 5b94238c..ebd9fb1b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt @@ -12,19 +12,20 @@ import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapPaddingsImpl import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.MapViewStateImpl +import com.mapconductor.core.state.StateOrValue import java.util.UUID import android.os.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IGoogleMapViewState : MapViewState +interface IGoogleMapViewState : MapViewState class GoogleMapViewState( override val id: String, - override val mapDesignType: GoogleMapDesignType, + override var mapDesignType: GoogleMapDesign, override val initCameraPosition: MapCameraPosition, -) : MapViewStateImpl(), +) : MapViewStateImpl(), IGoogleMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) @@ -36,6 +37,13 @@ class GoogleMapViewState( internal var controller: IGoogleMapViewController? = null + override fun changeMapDesignType( + value: GoogleMapDesign, + ){ + this.mapDesignType = value + this.controller?.changeMapDesign(value.getValue()) + } + override fun moveCameraTo( position: GeoPoint, durationMs: Long, @@ -130,7 +138,7 @@ class GoogleMapViewSaver : BaseMapViewSaver() { @Composable fun rememberGoogleMapViewState( - mapDesign: GoogleMapDesignType = GoogleMapDesign.Normal, + mapDesign: GoogleMapDesign = GoogleMapDesign.Normal, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, ): GoogleMapViewState { val stateId by rememberSaveable { 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 new file mode 100644 index 00000000..4d831a7b --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -0,0 +1,75 @@ +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-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index c0d0eb0e..7cdf8e8d 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,6 +10,7 @@ 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 @@ -32,6 +33,7 @@ fun HereMapView( onMarkerDragEnd: OnMarkerEventHandler? = {}, onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, onCircleClick: OnCircleEventHandler? = {}, onPolylineClick: OnPolylineEventHandler? = {}, content: (@Composable HereMapViewScope.() -> Unit)? = null, @@ -56,7 +58,7 @@ fun HereMapView( val mapInitOptions = HereMapViewInitOptions( - scheme = state.mapDesignType.id, + scheme = state.mapDesignType.getValue(), ) val controller = @@ -84,7 +86,7 @@ fun HereMapView( (state as? HereMapViewState)?.controller = controller - controller.holder.mapView.mapScene.loadScene(state.mapDesignType.id) { mapError -> + controller.holder.mapView.mapScene.loadScene(state.mapDesignType.getValue()) { mapError -> if (mapError != null) { throw Throwable("Loading map failed: mapError: " + mapError.name) } 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 537936e3..14fe1520 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,349 +1,362 @@ -package com.mapconductor.here - -import com.here.sdk.animation.AnimationState -import com.here.sdk.core.GeoOrientation -import com.here.sdk.core.Point2D -import com.here.sdk.gestures.GestureState -import com.here.sdk.gestures.LongPressListener -import com.here.sdk.gestures.TapListener -import com.here.sdk.mapview.MapCamera -import com.here.sdk.mapview.MapCameraAnimationFactory -import com.here.sdk.mapview.MapCameraListener -import com.here.sdk.mapview.MapCameraUpdateFactory -import com.here.sdk.mapview.MapMarker -import com.here.sdk.mapview.MapMeasure -import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.MapScene -import com.here.sdk.mapview.MapView -import com.here.time.Duration -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapViewHolder -import com.mapconductor.core.map.MapViewState.MoveCameraCallback -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator -import com.mapconductor.here.circle.DefaultHereMapCircleRenderer -import com.mapconductor.here.circle.HereMapCircleRenderer -import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer -import com.mapconductor.here.marker.HereMapMarkerRenderer -import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer -import com.mapconductor.here.polygon.HereMapPolygonRenderer -import com.mapconductor.here.polyline.DefaultHereMapPolylineRenderer -import com.mapconductor.here.polyline.HereMapPolylineRenderer -import com.mapconductor.settings.Settings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -interface IHereMapViewController : MapViewController { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - durationMs: Long, - listener: MoveCameraCallback? = null, - ) -} - -class HereMapViewController( - override val holder: MapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultHereMapPolylineRenderer(), - private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), - private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), -) : BaseMapViewController< - HereMapActualMarker, - HereMapActualCircle, - HereMapActualPolyline, - HereMapActualPolygon, - >(), - IHereMapViewController, - MapCameraListener, - TapListener, - LongPressListener { - companion object { - private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 - } - - private var selectedMarker: MarkerEntity? = null - - override val markerRenderer: MarkerRenderer = - HereMapMarkerRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - - override val polylineRenderer: PolylineRenderer = - HereMapPolylineRenderer( - holder = holder, - coroutine = coroutine, - ) - - override val polygonRenderer = - HereMapPolygonRenderer( - holder = holder, - coroutine = coroutine, - ) - - override val circleRenderer = - HereMapCircleRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - - override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() - polylineOverlayManager.clearOverlays() - polygonOverlayManager.clearOverlays() - circleOverlayManager.clearOverlays() - } - - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) - - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) - - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) - - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) - - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - - init { - setupListeners() - markerRenderer.init(markerOverlayManager) - polygonRenderer.init(polygonOverlayManager) - circleRenderer.init(circleOverlayManager) - } - - override fun setupListeners() { - holder.mapView.camera.removeListener(this) - holder.mapView.camera.addListener(this) - holder.mapView.gestures.tapListener = this - holder.mapView.gestures.longPressListener = this - } - - override fun moveCamera( - dstPosition: MapCameraPosition, - listener: MoveCameraCallback?, - ) { - val camera = this.holder.mapView.camera - val adjustCameraUpdate = - MapCameraUpdateFactory.lookAt( - GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), - GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), - MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), - ) - - camera.applyUpdate(adjustCameraUpdate) - listener?.onComplete(true) - } - - override fun animateCamera( - dstPosition: MapCameraPosition, - durationMs: Long, - listener: MoveCameraCallback?, - ) { - val camera = this.holder.mapView.camera - -// bowFactor > 0: 最初にズームアウト → 到達時にズームイン -// bowFactor < 0: 最初にズームイン → 到達時にズームアウト(ややレア) -// bowFactor = 0: 常に同じズーム(直線的) - val bowFactor = 1.0 - val animation = - MapCameraAnimationFactory.flyTo( - GeoPoint.from(dstPosition.position).toGeoCoordinates().toUpdate(), - GeoOrientation(dstPosition.bearing, dstPosition.tilt).toUpdate(), - MapMeasure(MapMeasure.Kind.ZOOM_LEVEL, dstPosition.zoom + ZOOM_ADJUST_VALUE), - bowFactor, - Duration.ofMillis(durationMs), - ) - coroutine.launch { - camera.startAnimation(animation) { animState -> - when (animState) { - // Do nothing here - AnimationState.STARTED -> Unit - AnimationState.COMPLETED -> listener?.onComplete(true) - AnimationState.CANCELLED -> listener?.onComplete(false) - } - } - } - } - - override fun onMapCameraUpdated(cameraState: MapCamera.State) { - val correctCameraState = - MapCamera.State( - cameraState.targetCoordinates, - GeoOrientation(cameraState.orientationAtTarget.bearing, cameraState.orientationAtTarget.tilt), - 0.0, - cameraState.zoomLevel - ZOOM_ADJUST_VALUE, - ) - - 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?.let { it(touchPosition) } - } - - override fun onLongPress( - gesture: GestureState, - point: Point2D, - ) { - val position = this.getGeoPointFromPoint(point) ?: return - - when (gesture.value) { - GestureState.BEGIN.value -> { - val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE - val tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity() - - val entity = - markerRenderer.findNearestMarker( - position = position, - tolerance = tolerance, - zoom = zoom, - ) ?: return - - entity.state.position = position - selectedMarker = entity - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(entity.state, true) - - 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() -} +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.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() +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt index 1600d14e..dab6aed3 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt @@ -24,13 +24,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IHereMapViewState : MapViewState +interface IHereMapViewState : MapViewState class HereMapViewState( override val id: String, - override val mapDesignType: HereMapDesignType, + override var mapDesignType: HereMapDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, -) : MapViewStateImpl(), +) : MapViewStateImpl(), IHereMapViewState { internal var controller: IHereMapViewController? = null @@ -38,6 +38,13 @@ class HereMapViewState( private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + override fun changeMapDesignType( + value: HereMapDesignType + ) { + this.mapDesignType = value + this.controller?.changeMapDesign(value.getValue()) + } + override fun moveCameraTo( position: GeoPoint, durationMs: Long, 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 48d1cd96..43721569 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 @@ -7,6 +7,7 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -28,6 +29,7 @@ fun MapboxMapView( onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, onPolylineClick: OnPolylineEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } 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 bae037c1..6c61525f 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 @@ -66,6 +66,11 @@ interface IMapboxMapViewController : MapboxActualPolyline, MapboxActualPolygon, > { + + fun changeMapDesign( + value: String + ) + fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -260,6 +265,14 @@ internal class MapboxMapViewController( } } + override fun changeMapDesign( + value: String + ){ + coroutine.launch { + holder.mapView.mapboxMap.loadStyle(value) {} + } + } + override fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback?, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt index 4a893721..fc796c23 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt @@ -18,13 +18,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IMapboxMapViewState : MapViewState +interface IMapboxMapViewState : MapViewState class MapboxMapViewState( override val id: String, - override val mapDesignType: MapboxDesignType, + override var mapDesignType: MapboxDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, -) : MapViewStateImpl(), +) : MapViewStateImpl(), IMapboxMapViewState { internal var controller: IMapboxMapViewController? = null @@ -32,6 +32,11 @@ class MapboxMapViewState( private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + override fun changeMapDesignType(value: MapboxDesignType) { + this.mapDesignType = value + this.controller?.changeMapDesign(value.getValue()) + } + override fun moveCameraTo( position: GeoPoint, durationMs: Long,