Amenities
This commit is contained in:
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 9
|
versionCode = 10
|
||||||
versionName = "0.1.3.9"
|
versionName = "0.1.3.10"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,23 +72,17 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
val routeData = MutableLiveData("")
|
val routeData = MutableLiveData("")
|
||||||
|
|
||||||
val viewModel = ViewModel(NavigationRepository())
|
val viewModel = ViewModel(NavigationRepository())
|
||||||
val routeModel = RouteModel()
|
val routeModel = RouteModel()
|
||||||
|
|
||||||
var tilt = 50.0
|
var tilt = 50.0
|
||||||
|
|
||||||
val useMock = true
|
val useMock = true
|
||||||
val stepData: MutableLiveData<StepData> by lazy {
|
val stepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData<StepData>()
|
MutableLiveData<StepData>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val nextStepData: MutableLiveData<StepData> by lazy {
|
val nextStepData: MutableLiveData<StepData> by lazy {
|
||||||
MutableLiveData<StepData>()
|
MutableLiveData<StepData>()
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastLocation = location(0.0, 0.0)
|
var lastLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
val observer = Observer<String> { newRoute ->
|
val observer = Observer<String> { newRoute ->
|
||||||
if (newRoute.isNotEmpty()) {
|
if (newRoute.isNotEmpty()) {
|
||||||
routeModel.startNavigation(newRoute)
|
routeModel.startNavigation(newRoute)
|
||||||
@@ -188,7 +182,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
sheetPeekHeightState.value = 128.dp
|
sheetPeekHeightState.value = 128.dp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationTheme() {
|
NavigationTheme {
|
||||||
BottomSheetScaffold(
|
BottomSheetScaffold(
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(hostState = snackbarHostState)
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -28,16 +29,34 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.window.layout.WindowMetricsCalculator
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
|
import com.kouros.navigation.car.ViewStyle
|
||||||
|
import com.kouros.navigation.car.map.BuildingLayer
|
||||||
import com.kouros.navigation.car.map.DarkMode
|
import com.kouros.navigation.car.map.DarkMode
|
||||||
import com.kouros.navigation.car.map.MapLibre
|
import com.kouros.navigation.car.map.MapLibre
|
||||||
import com.kouros.navigation.car.map.NavigationImage
|
import com.kouros.navigation.car.map.NavigationImage
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
||||||
|
import com.kouros.navigation.data.RouteColor
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
import org.maplibre.compose.camera.CameraPosition
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
|
import org.maplibre.compose.expressions.dsl.const
|
||||||
|
import org.maplibre.compose.expressions.dsl.exponential
|
||||||
|
import org.maplibre.compose.expressions.dsl.interpolate
|
||||||
|
import org.maplibre.compose.expressions.dsl.zoom
|
||||||
|
import org.maplibre.compose.layers.CircleLayer
|
||||||
|
import org.maplibre.compose.layers.LineLayer
|
||||||
import org.maplibre.compose.location.LocationTrackingEffect
|
import org.maplibre.compose.location.LocationTrackingEffect
|
||||||
import org.maplibre.compose.location.UserLocationState
|
import org.maplibre.compose.location.UserLocationState
|
||||||
|
import org.maplibre.compose.map.MapOptions
|
||||||
|
import org.maplibre.compose.map.MaplibreMap
|
||||||
|
import org.maplibre.compose.map.OrnamentOptions
|
||||||
|
import org.maplibre.compose.sources.GeoJsonData
|
||||||
|
import org.maplibre.compose.sources.GeoJsonSource
|
||||||
|
import org.maplibre.compose.sources.getBaseSource
|
||||||
|
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||||
import org.maplibre.compose.style.BaseStyle
|
import org.maplibre.compose.style.BaseStyle
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -77,7 +96,7 @@ fun MapView(
|
|||||||
Column {
|
Column {
|
||||||
NavigationInfo(step)
|
NavigationInfo(step)
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
MapLibre(applicationContext, cameraState, baseStyle, route, "", position)
|
MapLibre(applicationContext, cameraState, baseStyle, route, ViewStyle.VIEW)
|
||||||
LocationTrackingEffect(
|
LocationTrackingEffect(
|
||||||
locationState = userLocationState,
|
locationState = userLocationState,
|
||||||
) {
|
) {
|
||||||
@@ -97,3 +116,5 @@ fun MapView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ fun SearchSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (recentPlaces.value != null) {
|
if (recentPlaces.value != null) {
|
||||||
println("Recent Places ${recentPlaces.value}")
|
|
||||||
val textFieldState = rememberTextFieldState()
|
val textFieldState = rememberTextFieldState()
|
||||||
val items = listOf(recentPlaces)
|
val items = listOf(recentPlaces)
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
@@ -108,7 +107,7 @@ fun Home(
|
|||||||
closeSheet()
|
closeSheet()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = com.google.android.gms.base.R.drawable.common_full_open_on_phone),
|
painter = painterResource(id = R.drawable.ic_place_white_24dp),
|
||||||
"Home",
|
"Home",
|
||||||
modifier = Modifier.size(24.dp, 24.dp),
|
modifier = Modifier.size(24.dp, 24.dp),
|
||||||
)
|
)
|
||||||
@@ -222,7 +221,6 @@ private fun SearchPlaces(
|
|||||||
street = place.address.road
|
street = place.address.road
|
||||||
)
|
)
|
||||||
viewModel.saveRecent(pl)
|
viewModel.saveRecent(pl)
|
||||||
println("Save $pl")
|
|
||||||
val toLocation =
|
val toLocation =
|
||||||
location(place.lon.toDouble(), place.lat.toDouble())
|
location(place.lon.toDouble(), place.lat.toDouble())
|
||||||
viewModel.loadRoute(context, location, toLocation)
|
viewModel.loadRoute(context, location, toLocation)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -63,7 +62,6 @@ class SurfaceRenderer(
|
|||||||
private var visibleArea = MutableLiveData(
|
private var visibleArea = MutableLiveData(
|
||||||
Rect(0, 0, 0, 0)
|
Rect(0, 0, 0, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
var stableArea = Rect()
|
var stableArea = Rect()
|
||||||
var width = 0
|
var width = 0
|
||||||
var height = 0
|
var height = 0
|
||||||
@@ -71,11 +69,9 @@ class SurfaceRenderer(
|
|||||||
val routeData = MutableLiveData("")
|
val routeData = MutableLiveData("")
|
||||||
val speed = MutableLiveData(0F)
|
val speed = MutableLiveData(0F)
|
||||||
lateinit var centerLocation: Location
|
lateinit var centerLocation: Location
|
||||||
var preview = false
|
var viewStyle = ViewStyle.VIEW
|
||||||
var previewDistance = 0.0
|
var previewDistance = 0.0
|
||||||
val previewRouteData = MutableLiveData("")
|
|
||||||
lateinit var mapView: ComposeView
|
lateinit var mapView: ComposeView
|
||||||
var panView = false
|
|
||||||
var tilt = 55.0
|
var tilt = 55.0
|
||||||
var countDownTimerActive = false
|
var countDownTimerActive = false
|
||||||
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
||||||
@@ -171,18 +167,16 @@ class SurfaceRenderer(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MapView() {
|
fun MapView() {
|
||||||
val stateWidth = visibleArea.observeAsState()
|
|
||||||
val position: CameraPosition? by cameraPosition.observeAsState()
|
val position: CameraPosition? by cameraPosition.observeAsState()
|
||||||
val route: String? by routeData.observeAsState()
|
val route: String? by routeData.observeAsState()
|
||||||
val previewRoute: String? by previewRouteData.observeAsState()
|
val paddingValues = getPaddingValues(height, viewStyle)
|
||||||
val paddingValues = getPaddingValues(width - stateWidth.value!!.width(), height, preview)
|
|
||||||
val cameraState = cameraState(paddingValues, position, tilt)
|
val cameraState = cameraState(paddingValues, position, tilt)
|
||||||
|
|
||||||
val baseStyle = remember {
|
val baseStyle = remember {
|
||||||
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
|
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
|
||||||
}
|
}
|
||||||
DarkMode(carContext, baseStyle)
|
DarkMode(carContext, baseStyle)
|
||||||
MapLibre(carContext, cameraState, baseStyle, route, previewRoute, position)
|
MapLibre(carContext, cameraState, baseStyle, route, viewStyle)
|
||||||
ShowPosition(cameraState, position, paddingValues)
|
ShowPosition(cameraState, position, paddingValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,25 +186,32 @@ class SurfaceRenderer(
|
|||||||
position: CameraPosition?,
|
position: CameraPosition?,
|
||||||
paddingValues: PaddingValues
|
paddingValues: PaddingValues
|
||||||
) {
|
) {
|
||||||
val cameraDuration = duration(preview, position!!.bearing, lastBearing)
|
val cameraDuration =
|
||||||
|
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
|
||||||
var bearing = position.bearing
|
var bearing = position.bearing
|
||||||
var zoom = position.zoom
|
var zoom = position.zoom
|
||||||
var target = position.target
|
var target = position.target
|
||||||
var localTilt = tilt
|
var localTilt = tilt
|
||||||
val currentSpeed: Float? by speed.observeAsState()
|
val currentSpeed: Float? by speed.observeAsState()
|
||||||
if (!preview) {
|
when (viewStyle) {
|
||||||
if (routeModel.isNavigating()) {
|
ViewStyle.VIEW -> {
|
||||||
DrawImage(paddingValues, currentSpeed, width, height)
|
|
||||||
} else {
|
|
||||||
DrawImage(paddingValues, currentSpeed, width, height)
|
DrawImage(paddingValues, currentSpeed, width, height)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
ViewStyle.PREVIEW -> {
|
||||||
bearing = 0.0
|
bearing = 0.0
|
||||||
zoom = previewZoom(previewDistance)
|
zoom = previewZoom(previewDistance)
|
||||||
target = Position(centerLocation.longitude, centerLocation.latitude)
|
target = Position(centerLocation.longitude, centerLocation.latitude)
|
||||||
localTilt = 0.0
|
localTilt = 0.0
|
||||||
}
|
}
|
||||||
LaunchedEffect(position) {
|
|
||||||
|
else -> {
|
||||||
|
bearing = 0.0
|
||||||
|
localTilt = 0.0
|
||||||
|
zoom = 12.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(position, viewStyle) {
|
||||||
cameraState.animateTo(
|
cameraState.animateTo(
|
||||||
finalPosition = CameraPosition(
|
finalPosition = CameraPosition(
|
||||||
bearing = bearing,
|
bearing = bearing,
|
||||||
@@ -234,7 +235,7 @@ class SurfaceRenderer(
|
|||||||
/** Handles the map zoom-in and zoom-out events. */
|
/** Handles the map zoom-in and zoom-out events. */
|
||||||
fun handleScale(zoomSign: Int) {
|
fun handleScale(zoomSign: Int) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
panView = true
|
viewStyle = ViewStyle.PAN_VIEW
|
||||||
val newZoom = if (zoomSign < 0) {
|
val newZoom = if (zoomSign < 0) {
|
||||||
cameraPosition.value!!.zoom - 1.0
|
cameraPosition.value!!.zoom - 1.0
|
||||||
} else {
|
} else {
|
||||||
@@ -259,9 +260,9 @@ class SurfaceRenderer(
|
|||||||
|
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (!preview) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
val bearing = bearing(lastLocation, location, cameraPosition.value!!.bearing)
|
val bearing = bearing(lastLocation, location, cameraPosition.value!!.bearing)
|
||||||
val zoom = if (!panView) {
|
val zoom = if (viewStyle == ViewStyle.VIEW) {
|
||||||
calculateZoom(location.speed.toDouble())
|
calculateZoom(location.speed.toDouble())
|
||||||
} else {
|
} else {
|
||||||
cameraPosition.value!!.zoom
|
cameraPosition.value!!.zoom
|
||||||
@@ -280,12 +281,6 @@ class SurfaceRenderer(
|
|||||||
val lastLocationTimer = lastLocation
|
val lastLocationTimer = lastLocation
|
||||||
checkUpdate(mainThreadHandler, lastLocationTimer)
|
checkUpdate(mainThreadHandler, lastLocationTimer)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
updateCameraPosition(
|
|
||||||
0.0,
|
|
||||||
previewZoom(previewDistance),
|
|
||||||
Position(centerLocation.longitude, centerLocation.latitude)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +290,7 @@ class SurfaceRenderer(
|
|||||||
lastLocationTimer: Location
|
lastLocationTimer: Location
|
||||||
) {
|
) {
|
||||||
mainThreadHandler.post {
|
mainThreadHandler.post {
|
||||||
object : CountDownTimer(5000, 1000) {
|
object : CountDownTimer(3000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {}
|
override fun onTick(millisUntilFinished: Long) {}
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
countDownTimerActive = false
|
countDownTimerActive = false
|
||||||
@@ -313,7 +308,7 @@ class SurfaceRenderer(
|
|||||||
bearing = bearing,
|
bearing = bearing,
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
tilt = tilt,
|
tilt = tilt,
|
||||||
padding = getPaddingValues(width - visibleArea.value!!.width(), height, preview),
|
padding = getPaddingValues(height, viewStyle),
|
||||||
target = target
|
target = target
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -321,28 +316,41 @@ class SurfaceRenderer(
|
|||||||
|
|
||||||
fun setRouteData() {
|
fun setRouteData() {
|
||||||
routeData.value = routeModel.route.routeGeoJson
|
routeData.value = routeModel.route.routeGeoJson
|
||||||
previewRouteData.value = ""
|
viewStyle = ViewStyle.VIEW
|
||||||
preview = false
|
|
||||||
panView = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewRouteData(routeModel: RouteModel) {
|
fun setPreviewRouteData(routeModel: RouteModel) {
|
||||||
previewRouteData.value = routeModel.route.routeGeoJson
|
viewStyle = ViewStyle.PREVIEW
|
||||||
centerLocation = routeModel.route.centerLocation
|
with(routeModel) {
|
||||||
preview = true
|
routeData.value = route.routeGeoJson
|
||||||
previewDistance = routeModel.route.distance
|
centerLocation = route.centerLocation
|
||||||
|
previewDistance = route.distance
|
||||||
|
}
|
||||||
|
updateCameraPosition(
|
||||||
|
0.0,
|
||||||
|
previewZoom(previewDistance),
|
||||||
|
Position(centerLocation.longitude, centerLocation.latitude)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLocation(location: Location) {
|
fun setCategories(location: Location, route: String) {
|
||||||
|
viewStyle = ViewStyle.SEARCH_VIEW
|
||||||
|
routeData.value = route
|
||||||
|
updateCameraPosition(
|
||||||
|
0.0,
|
||||||
|
12.0,
|
||||||
|
target = Position(location.longitude, location.latitude)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCategoryLocation(location: Location) {
|
||||||
|
viewStyle = ViewStyle.SEARCH_VIEW
|
||||||
cameraPosition.postValue(
|
cameraPosition.postValue(
|
||||||
cameraPosition.value!!.copy(
|
cameraPosition.value!!.copy(
|
||||||
bearing = 0.0,
|
|
||||||
zoom = 15.0,
|
|
||||||
tilt = 0.0,
|
|
||||||
padding = PaddingValues(start = 350.dp, bottom = 0.dp),
|
|
||||||
target = Position(location.longitude, location.latitude)
|
target = Position(location.longitude, location.latitude)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion
|
companion
|
||||||
@@ -352,3 +360,7 @@ class SurfaceRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ViewStyle {
|
||||||
|
VIEW, PREVIEW, PAN_VIEW, SEARCH_VIEW
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,10 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Badge
|
|
||||||
import androidx.compose.material3.BadgedBox
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -28,6 +25,7 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
|
import com.kouros.navigation.car.ViewStyle
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
||||||
import com.kouros.navigation.data.NavigationColor
|
import com.kouros.navigation.data.NavigationColor
|
||||||
@@ -39,9 +37,15 @@ import org.maplibre.compose.camera.CameraPosition
|
|||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
import org.maplibre.compose.expressions.dsl.const
|
import org.maplibre.compose.expressions.dsl.const
|
||||||
|
import org.maplibre.compose.expressions.dsl.exponential
|
||||||
|
import org.maplibre.compose.expressions.dsl.image
|
||||||
|
import org.maplibre.compose.expressions.dsl.interpolate
|
||||||
|
import org.maplibre.compose.expressions.dsl.zoom
|
||||||
import org.maplibre.compose.layers.Anchor
|
import org.maplibre.compose.layers.Anchor
|
||||||
|
import org.maplibre.compose.layers.CircleLayer
|
||||||
import org.maplibre.compose.layers.FillLayer
|
import org.maplibre.compose.layers.FillLayer
|
||||||
import org.maplibre.compose.layers.LineLayer
|
import org.maplibre.compose.layers.LineLayer
|
||||||
|
import org.maplibre.compose.layers.SymbolLayer
|
||||||
import org.maplibre.compose.location.LocationPuck
|
import org.maplibre.compose.location.LocationPuck
|
||||||
import org.maplibre.compose.location.LocationPuckColors
|
import org.maplibre.compose.location.LocationPuckColors
|
||||||
import org.maplibre.compose.location.LocationPuckSizes
|
import org.maplibre.compose.location.LocationPuckSizes
|
||||||
@@ -54,6 +58,8 @@ import org.maplibre.compose.sources.Source
|
|||||||
import org.maplibre.compose.sources.getBaseSource
|
import org.maplibre.compose.sources.getBaseSource
|
||||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||||
import org.maplibre.compose.style.BaseStyle
|
import org.maplibre.compose.style.BaseStyle
|
||||||
|
import org.maplibre.spatialk.geojson.Feature
|
||||||
|
import org.maplibre.spatialk.geojson.FeatureCollection
|
||||||
import org.maplibre.spatialk.geojson.Position
|
import org.maplibre.spatialk.geojson.Position
|
||||||
|
|
||||||
|
|
||||||
@@ -83,8 +89,7 @@ fun MapLibre(
|
|||||||
cameraState: CameraState,
|
cameraState: CameraState,
|
||||||
baseStyle: MutableState<BaseStyle.Uri>,
|
baseStyle: MutableState<BaseStyle.Uri>,
|
||||||
route: String?,
|
route: String?,
|
||||||
previewRoute: String?,
|
viewStyle: ViewStyle
|
||||||
position: CameraPosition?
|
|
||||||
) {
|
) {
|
||||||
MaplibreMap(
|
MaplibreMap(
|
||||||
options = MapOptions(
|
options = MapOptions(
|
||||||
@@ -98,45 +103,58 @@ fun MapLibre(
|
|||||||
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
|
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
|
||||||
BuildingLayer(tiles)
|
BuildingLayer(tiles)
|
||||||
}
|
}
|
||||||
RouteLayer(route, previewRoute, position!!.zoom)
|
RouteLayer(route, viewStyle)
|
||||||
}
|
}
|
||||||
//Puck(cameraState, lastLocation)
|
//Puck(cameraState, lastLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) {
|
fun RouteLayer(routeData: String?, viewStyle: ViewStyle) {
|
||||||
val width = zoom - 2
|
if (routeData != null && routeData.isNotEmpty()) {
|
||||||
if (routeData!!.isNotEmpty()) {
|
println(routeData)
|
||||||
val routes =
|
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||||
rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
if (viewStyle == ViewStyle.VIEW) {
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "routes-casing",
|
id = "routes-casing$viewStyle",
|
||||||
source = routes,
|
source = routes,
|
||||||
color = const(Color.White),
|
color = const(Color.White),
|
||||||
width = const((width+2).dp),
|
width =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.4.dp),
|
||||||
|
6 to const(0.8.dp),
|
||||||
|
7 to const(2.0.dp),
|
||||||
|
20 to const(24.dp),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
LineLayer(
|
LineLayer(
|
||||||
id = "routes",
|
id = "routes$viewStyle",
|
||||||
source = routes,
|
source = routes,
|
||||||
color = const(RouteColor),
|
color = const(RouteColor),
|
||||||
width = const(width.dp),
|
width =
|
||||||
|
interpolate(
|
||||||
|
type = exponential(1.2f),
|
||||||
|
input = zoom(),
|
||||||
|
5 to const(0.4.dp),
|
||||||
|
6 to const(0.7.dp),
|
||||||
|
7 to const(1.75.dp),
|
||||||
|
20 to const(22.dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SymbolLayer(
|
||||||
|
id = "my-symbol-layer",
|
||||||
|
source = routes,
|
||||||
|
// Convert a drawable resource to a MapLibre image
|
||||||
|
// drawAsSdf = true allows us to tint the image programmatically
|
||||||
|
iconImage = image(painterResource(R.drawable.ic_place_white_24dp), drawAsSdf = true),
|
||||||
|
// Now we can apply any color we want!
|
||||||
|
iconColor = const(Color.Blue),
|
||||||
|
iconSize = const(1.5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (previewRoute!!.isNotEmpty()) {
|
|
||||||
val routes =
|
|
||||||
rememberGeoJsonSource(GeoJsonData.JsonString(previewRoute))
|
|
||||||
LineLayer(
|
|
||||||
id = "routes-casing-pre",
|
|
||||||
source = routes,
|
|
||||||
color = const(Color.White),
|
|
||||||
width = const(6.dp),
|
|
||||||
)
|
|
||||||
LineLayer(
|
|
||||||
id = "routes-pre",
|
|
||||||
source = routes,
|
|
||||||
color = const(RouteColor),
|
|
||||||
width = const(4.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +181,10 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
|
|||||||
val imageSize = (height / 6)
|
val imageSize = (height / 6)
|
||||||
val color = remember { NavigationColor }
|
val color = remember { NavigationColor }
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
||||||
Canvas(modifier =Modifier
|
Canvas(
|
||||||
.size(imageSize.dp, imageSize.dp)) {
|
modifier = Modifier
|
||||||
|
.size(imageSize.dp, imageSize.dp)
|
||||||
|
) {
|
||||||
scale(scaleX = 1f, scaleY = 0.7f) {
|
scale(scaleX = 1f, scaleY = 0.7f) {
|
||||||
drawCircle(Color.DarkGray.copy(alpha = 0.2f))
|
drawCircle(Color.DarkGray.copy(alpha = 0.2f))
|
||||||
}
|
}
|
||||||
@@ -259,11 +279,11 @@ fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues {
|
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
|
||||||
return if (preView) {
|
return when (viewStyle) {
|
||||||
PaddingValues(start = 150.dp, bottom = 0.dp)
|
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp)
|
||||||
} else {
|
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
|
||||||
PaddingValues(start = 50.dp, top = distanceFromTop(height).dp)
|
else -> PaddingValues(start = 450.dp, bottom = 0.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import android.location.Location
|
|||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.Header
|
import androidx.car.app.model.Header
|
||||||
import androidx.car.app.model.ItemList
|
import androidx.car.app.model.ItemList
|
||||||
import androidx.car.app.model.ListTemplate
|
import androidx.car.app.model.ListTemplate
|
||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
|
import com.kouros.navigation.car.ViewStyle
|
||||||
import com.kouros.navigation.data.Category
|
import com.kouros.navigation.data.Category
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ class CategoriesScreen(
|
|||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var categories: List<Category> = listOf(
|
var categories: List<Category> = listOf(
|
||||||
Category(id = Constants.GAS_STATION, name = carContext.getString(R.string.gas_station)),
|
Category(id = Constants.FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
||||||
Category(id = Constants.PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
Category(id = Constants.PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
||||||
Category(id = Constants.CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
Category(id = Constants.CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
||||||
)
|
)
|
||||||
@@ -29,12 +32,17 @@ class CategoriesScreen(
|
|||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val itemListBuilder = ItemList.Builder()
|
val itemListBuilder = ItemList.Builder()
|
||||||
.setNoItemsMessage("No categories to show")
|
.setNoItemsMessage("No categories to show")
|
||||||
|
|
||||||
categories.forEach {
|
categories.forEach {
|
||||||
it.name
|
|
||||||
itemListBuilder.addItem(
|
itemListBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle(it.name)
|
.setTitle(it.name)
|
||||||
|
.setImage(CarIcon.Builder(
|
||||||
|
IconCompat.createWithResource(
|
||||||
|
carContext,
|
||||||
|
com.kouros.android.cars.carappservice.R.drawable.ev_station_24px
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build())
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
screenManager
|
screenManager
|
||||||
.pushForResult(
|
.pushForResult(
|
||||||
@@ -56,6 +64,8 @@ class CategoriesScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surfaceRenderer.viewStyle = ViewStyle.SEARCH_VIEW
|
||||||
|
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle("title")
|
.setTitle("title")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.kouros.navigation.car.screen
|
package com.kouros.navigation.car.screen
|
||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
@@ -14,15 +15,19 @@ import androidx.car.app.model.ItemList
|
|||||||
import androidx.car.app.model.ListTemplate
|
import androidx.car.app.model.ListTemplate
|
||||||
import androidx.car.app.model.Row
|
import androidx.car.app.model.Row
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.car.app.navigation.model.MapController
|
||||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||||
import androidx.car.app.versioning.CarAppApiLevels
|
import androidx.car.app.versioning.CarAppApiLevels
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
|
import com.kouros.navigation.car.navigation.NavigationMessage
|
||||||
|
import com.kouros.navigation.data.Constants.homeLocation
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.createGeoJson
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -38,35 +43,50 @@ class CategoryScreen(
|
|||||||
|
|
||||||
val observer = Observer<List<Elements>> { newElements ->
|
val observer = Observer<List<Elements>> { newElements ->
|
||||||
elements = newElements
|
elements = newElements
|
||||||
|
val coordinates = mutableListOf<List<Double>>()
|
||||||
|
val loc = location(0.0, 0.0)
|
||||||
|
elements.forEach {
|
||||||
|
if (loc.latitude == 0.0) {
|
||||||
|
loc.longitude = it.lon!!
|
||||||
|
loc.latitude = it.lat!!
|
||||||
|
}
|
||||||
|
if (coordinates.isEmpty())
|
||||||
|
coordinates.add(listOf(location.longitude, location.latitude))
|
||||||
|
}
|
||||||
|
if (elements.isNotEmpty()) {
|
||||||
|
val route = createGeoJson("Point", coordinates)
|
||||||
|
surfaceRenderer.setCategories(loc, route)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModel.elements.observe(this, observer)
|
viewModel.elements.observe(this, observer)
|
||||||
viewModel.getAmenities(category, location)
|
viewModel.getAmenities(category, location)
|
||||||
invalidate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
|
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||||
|
var index = 0
|
||||||
val listLimit = min(
|
val listLimit = min(
|
||||||
10,
|
100,
|
||||||
carContext.getCarService(ConstraintManager::class.java)
|
carContext.getCarService(ConstraintManager::class.java)
|
||||||
.getContentLimit(
|
.getContentLimit(
|
||||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elements.forEach {
|
elements.forEach {
|
||||||
//listBuilder.addItem(createRow(it.tags!!.operator.toString(), it.tags?.capacity.toString()))
|
if (index++ < listLimit) {
|
||||||
listBuilder.addItem(
|
listBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setOnClickListener({
|
.setOnClickListener {
|
||||||
val location = location(it.lon!!, it.lat!!)
|
val location = location(it.lon!!, it.lat!!)
|
||||||
surfaceRenderer.setLocation(location)
|
surfaceRenderer.setCategoryLocation(location)
|
||||||
})
|
}
|
||||||
.setTitle(secondText(it.tags?.capacity.toString()))
|
.setTitle(it.tags.operator.toString())
|
||||||
.setImage(
|
.setImage(
|
||||||
CarIcon.Builder(
|
CarIcon.Builder(
|
||||||
IconCompat.createWithResource(
|
IconCompat.createWithResource(
|
||||||
@@ -76,44 +96,17 @@ class CategoryScreen(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.addText(it.tags!!.operator.toString())
|
.addText(it.tags.network.toString())
|
||||||
.addText(secondText(it.tags?.capacity.toString()))
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val header = Header.Builder()
|
val header = Header.Builder()
|
||||||
.setStartHeaderAction(Action.BACK)
|
.setStartHeaderAction(Action.BACK)
|
||||||
.setTitle(carContext.getString(R.string.charging_station))
|
.setTitle(carContext.getString(R.string.charging_station))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val actionStrip = ActionStrip.Builder()
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setOnClickListener {
|
|
||||||
CarToast.makeText(
|
|
||||||
carContext,
|
|
||||||
carContext.getString(
|
|
||||||
R.string.notification_demo
|
|
||||||
),
|
|
||||||
CarToast.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_place_white_24dp
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
.setFlags(Action.FLAG_IS_PERSISTENT)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val builder = MapWithContentTemplate.Builder()
|
val builder = MapWithContentTemplate.Builder()
|
||||||
.setContentTemplate(
|
.setContentTemplate(
|
||||||
ListTemplate.Builder()
|
ListTemplate.Builder()
|
||||||
@@ -121,7 +114,11 @@ class CategoryScreen(
|
|||||||
.setSingleList(listBuilder.build())
|
.setSingleList(listBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setActionStrip(actionStrip)
|
.setMapController(
|
||||||
|
MapController.Builder().setMapActionStrip(
|
||||||
|
getMapActionStrip()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
@@ -140,4 +137,27 @@ class CategoryScreen(
|
|||||||
|
|
||||||
return secondText
|
return secondText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMapActionStrip(): ActionStrip {
|
||||||
|
return ActionStrip.Builder()
|
||||||
|
.addAction(
|
||||||
|
createAction(R.drawable.ic_zoom_in_24, 1)
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
createAction(R.drawable.ic_zoom_out_24, -1)
|
||||||
|
)
|
||||||
|
.addAction(Action.PAN)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAction(
|
||||||
|
@DrawableRes iconRes: Int,
|
||||||
|
scale: Int
|
||||||
|
): Action {
|
||||||
|
val navigationMessage = NavigationMessage(carContext)
|
||||||
|
return Action.Builder()
|
||||||
|
.setOnClickListener { surfaceRenderer.handleScale(scale) }
|
||||||
|
.setIcon(navigationMessage.createCarIcon(iconRes))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.lifecycle.Observer
|
|||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.NavigationCarAppService
|
import com.kouros.navigation.car.NavigationCarAppService
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
|
import com.kouros.navigation.car.ViewStyle
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
@@ -237,7 +238,7 @@ class NavigationScreen(
|
|||||||
val actionStripBuilder = ActionStrip.Builder()
|
val actionStripBuilder = ActionStrip.Builder()
|
||||||
.addAction(zoomPlus())
|
.addAction(zoomPlus())
|
||||||
.addAction(zoomMinus())
|
.addAction(zoomMinus())
|
||||||
if (surfaceRenderer.panView) {
|
if (surfaceRenderer.viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
actionStripBuilder
|
actionStripBuilder
|
||||||
.addAction(
|
.addAction(
|
||||||
panAction()
|
panAction()
|
||||||
@@ -362,7 +363,7 @@ class NavigationScreen(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
).setOnClickListener {
|
).setOnClickListener {
|
||||||
surfaceRenderer.panView = false
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -410,7 +411,7 @@ class NavigationScreen(
|
|||||||
|
|
||||||
fun stopNavigation() {
|
fun stopNavigation() {
|
||||||
listener.stopNavigation()
|
listener.stopNavigation()
|
||||||
surfaceRenderer.routeData.postValue("")
|
surfaceRenderer.routeData.value = ""
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,19 +105,21 @@ class PlaceListScreen(
|
|||||||
it.street,
|
it.street,
|
||||||
avatar = null
|
avatar = null
|
||||||
)
|
)
|
||||||
screenManager
|
setResult(place)
|
||||||
.pushForResult(
|
|
||||||
RoutePreviewScreen(
|
|
||||||
carContext,
|
|
||||||
surfaceRenderer,
|
|
||||||
place
|
|
||||||
)
|
|
||||||
) { obj: Any? ->
|
|
||||||
if (obj != null) {
|
|
||||||
setResult(obj)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
// screenManager
|
||||||
}
|
// .pushForResult(
|
||||||
|
// RoutePreviewScreen(
|
||||||
|
// carContext,
|
||||||
|
// surfaceRenderer,
|
||||||
|
// place
|
||||||
|
// )
|
||||||
|
// ) { obj: Any? ->
|
||||||
|
// if (obj != null) {
|
||||||
|
// setResult(obj)
|
||||||
|
// finish()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,16 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kouros.navigation.car.screen
|
package com.kouros.navigation.car.screen
|
||||||
|
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import android.location.Location
|
|
||||||
import android.location.LocationManager
|
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.Action
|
||||||
import androidx.car.app.model.Action.FLAG_PRIMARY
|
import androidx.car.app.model.Action.FLAG_PRIMARY
|
||||||
import androidx.car.app.model.ActionStrip
|
import androidx.car.app.model.ActionStrip
|
||||||
@@ -47,6 +43,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
|
|||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.model.ViewModel
|
import com.kouros.navigation.model.ViewModel
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
|
||||||
@@ -57,9 +54,7 @@ class RoutePreviewScreen(
|
|||||||
private var destination: Place
|
private var destination: Place
|
||||||
) :
|
) :
|
||||||
Screen(carContext) {
|
Screen(carContext) {
|
||||||
private var mIsFavorite = false
|
private var isFavorite = false
|
||||||
|
|
||||||
private var mItemLimit = 0
|
|
||||||
|
|
||||||
val vieModel = ViewModel(NavigationRepository())
|
val vieModel = ViewModel(NavigationRepository())
|
||||||
|
|
||||||
@@ -76,19 +71,11 @@ class RoutePreviewScreen(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
vieModel.previewRoute.observe(this, observer)
|
vieModel.previewRoute.observe(this, observer)
|
||||||
val location = Location(LocationManager.GPS_PROVIDER)
|
val location = location(destination.longitude, destination.latitude)
|
||||||
location.latitude = destination.latitude
|
|
||||||
location.longitude = destination.longitude
|
|
||||||
vieModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
|
vieModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
mItemLimit =
|
|
||||||
carContext.getCarService(ConstraintManager::class.java)
|
|
||||||
.getContentLimit(
|
|
||||||
ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST
|
|
||||||
)
|
|
||||||
|
|
||||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||||
IconCompat.createWithResource(
|
IconCompat.createWithResource(
|
||||||
carContext, R.drawable.baseline_assistant_navigation_24
|
carContext, R.drawable.baseline_assistant_navigation_24
|
||||||
@@ -145,7 +132,7 @@ class RoutePreviewScreen(
|
|||||||
CarIcon.Builder(
|
CarIcon.Builder(
|
||||||
IconCompat.createWithResource(
|
IconCompat.createWithResource(
|
||||||
carContext,
|
carContext,
|
||||||
if (mIsFavorite)
|
if (isFavorite)
|
||||||
R.drawable.ic_favorite_filled_white_24dp
|
R.drawable.ic_favorite_filled_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_favorite_white_24dp
|
R.drawable.ic_favorite_white_24dp
|
||||||
@@ -154,10 +141,10 @@ class RoutePreviewScreen(
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
mIsFavorite = !mIsFavorite
|
isFavorite = !isFavorite
|
||||||
CarToast.makeText(
|
CarToast.makeText(
|
||||||
carContext,
|
carContext,
|
||||||
if (mIsFavorite)
|
if (isFavorite)
|
||||||
carContext
|
carContext
|
||||||
.getString(R.string.favorites)
|
.getString(R.string.favorites)
|
||||||
else
|
else
|
||||||
@@ -174,10 +161,10 @@ class RoutePreviewScreen(
|
|||||||
|
|
||||||
private fun deleteFavoriteAction(): Action = Action.Builder()
|
private fun deleteFavoriteAction(): Action = Action.Builder()
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
if (mIsFavorite) {
|
if (isFavorite) {
|
||||||
vieModel.deleteFavorite(destination)
|
vieModel.deleteFavorite(destination)
|
||||||
}
|
}
|
||||||
mIsFavorite = !mIsFavorite
|
isFavorite = !isFavorite
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setIcon(
|
.setIcon(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.core.graphics.drawable.IconCompat
|
|||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
|
import com.kouros.navigation.car.ViewStyle
|
||||||
import com.kouros.navigation.data.Category
|
import com.kouros.navigation.data.Category
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
@@ -76,6 +77,7 @@ class SearchScreen(
|
|||||||
location,
|
location,
|
||||||
)
|
)
|
||||||
) { obj: Any? ->
|
) { obj: Any? ->
|
||||||
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
if (obj != null) {
|
if (obj != null) {
|
||||||
setResult(obj)
|
setResult(obj)
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
implementation("hu.supercluster:overpasser:0.2.2")
|
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ data class StepData (
|
|||||||
|
|
||||||
|
|
||||||
// GeoJSON data classes
|
// GeoJSON data classes
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GeoJsonLineString(
|
data class GeoJsonType(
|
||||||
val type: String,
|
val type: String,
|
||||||
val coordinates: List<List<Double>>
|
val coordinates: List<List<Double>>
|
||||||
)
|
)
|
||||||
@@ -77,7 +78,7 @@ data class GeoJsonLineString(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class GeoJsonFeature(
|
data class GeoJsonFeature(
|
||||||
val type: String,
|
val type: String,
|
||||||
val geometry: GeoJsonLineString
|
val geometry: GeoJsonType,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -156,7 +157,7 @@ object Constants {
|
|||||||
|
|
||||||
const val FAVORITES: String = "Favorites"
|
const val FAVORITES: String = "Favorites"
|
||||||
|
|
||||||
const val GAS_STATION: String ="GasStation"
|
const val FUEL_STATION: String ="fuel"
|
||||||
|
|
||||||
const val PHARMACY: String ="pharmacy"
|
const val PHARMACY: String ="pharmacy"
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ data class Route(
|
|||||||
points.add(point)
|
points.add(point)
|
||||||
}
|
}
|
||||||
pointLocations = points
|
pointLocations = points
|
||||||
routeGeoJson = createGeoJson(waypoints)
|
routeGeoJson = createGeoJson("LineString", waypoints)
|
||||||
centerLocation = createCenterLocation(routeGeoJson)
|
centerLocation = createCenterLocation(routeGeoJson)
|
||||||
return Route(
|
return Route(
|
||||||
maneuvers,
|
maneuvers,
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ data class Elements (
|
|||||||
@SerializedName("id" ) var id : Long? = null,
|
@SerializedName("id" ) var id : Long? = null,
|
||||||
@SerializedName("lat" ) var lat : Double? = null,
|
@SerializedName("lat" ) var lat : Double? = null,
|
||||||
@SerializedName("lon" ) var lon : Double? = null,
|
@SerializedName("lon" ) var lon : Double? = null,
|
||||||
@SerializedName("tags" ) var tags : Tags? = Tags()
|
@SerializedName("tags" ) var tags : Tags = Tags()
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -1,45 +1,25 @@
|
|||||||
package com.kouros.navigation.data
|
package com.kouros.navigation.data.overpass
|
||||||
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.data.overpass.Amenity
|
import com.kouros.navigation.utils.NavigationUtils
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getBoundingBox2
|
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getOverpassBbox
|
|
||||||
import hu.supercluster.overpasser.library.output.OutputFormat
|
|
||||||
import hu.supercluster.overpasser.library.output.OutputModificator
|
|
||||||
import hu.supercluster.overpasser.library.output.OutputOrder
|
|
||||||
import hu.supercluster.overpasser.library.output.OutputVerbosity
|
|
||||||
import hu.supercluster.overpasser.library.query.OverpassQuery
|
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
|
||||||
class Overpass {
|
class Overpass {
|
||||||
|
|
||||||
|
val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
|
||||||
fun getAmenities(category: String, location: Location) : List<Elements> {
|
fun getAmenities(category: String, location: Location) : List<Elements> {
|
||||||
val boundingBox = getOverpassBbox(location, 2.0)
|
val boundingBox = NavigationUtils.getOverpassBbox(location, 2.0)
|
||||||
val bb = getBoundingBox2(location, 2.0)
|
val bb = NavigationUtils.getBoundingBox2(location, 2.0)
|
||||||
val url = "https://overpass.kumi.systems/api/interpreter"
|
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
||||||
val httpURLConnection = URL(url).openConnection() as HttpURLConnection
|
|
||||||
httpURLConnection.requestMethod = "POST"
|
httpURLConnection.requestMethod = "POST"
|
||||||
httpURLConnection.setRequestProperty(
|
httpURLConnection.setRequestProperty(
|
||||||
"Accept",
|
"Accept",
|
||||||
"application/json"
|
"application/json"
|
||||||
)
|
)
|
||||||
httpURLConnection.setDoOutput(true);
|
httpURLConnection.setDoOutput(true);
|
||||||
val query = OverpassQuery()
|
|
||||||
.format(OutputFormat.JSON)
|
|
||||||
.timeout(30)
|
|
||||||
.filterQuery()
|
|
||||||
.node()
|
|
||||||
.amenity("charging_station")
|
|
||||||
.tagNot("access", "private")
|
|
||||||
.boundingBox(bb.southernLat, bb.westernLon, bb.northerLat, bb.easternLon)
|
|
||||||
.end()
|
|
||||||
.output(OutputVerbosity.BODY, OutputModificator.CENTER, OutputOrder.QT, 100)
|
|
||||||
.build()
|
|
||||||
// define a query
|
// define a query
|
||||||
val test = """
|
val test = """
|
||||||
|[out:json];
|
|[out:json];
|
||||||
@@ -56,12 +36,9 @@ class Overpass {
|
|||||||
outputStreamWriter.flush()
|
outputStreamWriter.flush()
|
||||||
// Check if the connection is successful
|
// Check if the connection is successful
|
||||||
val responseCode = httpURLConnection.responseCode
|
val responseCode = httpURLConnection.responseCode
|
||||||
println("Overpass: $responseCode")
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
val response = httpURLConnection.inputStream.bufferedReader()
|
val response = httpURLConnection.inputStream.bufferedReader()
|
||||||
.use { it.readText() } // defaults to UTF-8
|
.use { it.readText() } // defaults to UTF-8
|
||||||
|
|
||||||
|
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
val overpass = gson.fromJson(response, Amenity::class.java)
|
||||||
println("Overpass: $response")
|
println("Overpass: $response")
|
||||||
@@ -17,6 +17,6 @@ data class Tags (
|
|||||||
@SerializedName("operator:wikipedia" ) var operatorWikipedia : String? = null,
|
@SerializedName("operator:wikipedia" ) var operatorWikipedia : String? = null,
|
||||||
@SerializedName("ref" ) var ref : String? = null,
|
@SerializedName("ref" ) var ref : String? = null,
|
||||||
@SerializedName("socket:type2" ) var socketType2 : String? = null,
|
@SerializedName("socket:type2" ) var socketType2 : String? = null,
|
||||||
@SerializedName("socket:type2:output" ) var socketType2Ooutput : String? = null
|
@SerializedName("socket:type2:output" ) var socketType2Output : String? = null
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -52,7 +52,6 @@ open class RouteModel() {
|
|||||||
this.routeState = routeState.copy(
|
this.routeState = routeState.copy(
|
||||||
route = null,
|
route = null,
|
||||||
isNavigating = false,
|
isNavigating = false,
|
||||||
// destination = Place(),
|
|
||||||
arrived = false,
|
arrived = false,
|
||||||
maneuverType = 0,
|
maneuverType = 0,
|
||||||
currentShapeIndex = 0,
|
currentShapeIndex = 0,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import com.kouros.navigation.data.Constants
|
|||||||
import com.kouros.navigation.data.Locations
|
import com.kouros.navigation.data.Locations
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.ObjectBox.boxStore
|
import com.kouros.navigation.data.ObjectBox.boxStore
|
||||||
import com.kouros.navigation.data.Overpass
|
import com.kouros.navigation.data.overpass.Overpass
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.Place_
|
import com.kouros.navigation.data.Place_
|
||||||
import com.kouros.navigation.data.SearchFilter
|
import com.kouros.navigation.data.SearchFilter
|
||||||
@@ -118,7 +118,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
.orderDesc(Place_.lastDate)
|
.orderDesc(Place_.lastDate)
|
||||||
.build()
|
.build()
|
||||||
val results = query.find()
|
val results = query.find()
|
||||||
println("Favorites $results")
|
|
||||||
query.close()
|
query.close()
|
||||||
for (place in results) {
|
for (place in results) {
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
@@ -233,7 +232,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
val address = repository.reverseAddress(location)
|
val address = repository.reverseAddress(location)
|
||||||
val gson = GsonBuilder().serializeNulls().create()
|
val gson = GsonBuilder().serializeNulls().create()
|
||||||
val place = gson.fromJson(address, SearchResult::class.java)
|
val place = gson.fromJson(address, SearchResult::class.java)
|
||||||
println(place.address.road)
|
|
||||||
return place.address.road
|
return place.address.road
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +269,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
|||||||
val current = LocalDateTime.now(ZoneOffset.UTC)
|
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
||||||
placeBox.put(place)
|
placeBox.put(place)
|
||||||
println("Save Place $place")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.kouros.navigation.data.BoundingBox
|
|||||||
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
|
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
|
||||||
import com.kouros.navigation.data.GeoJsonFeature
|
import com.kouros.navigation.data.GeoJsonFeature
|
||||||
import com.kouros.navigation.data.GeoJsonFeatureCollection
|
import com.kouros.navigation.data.GeoJsonFeatureCollection
|
||||||
import com.kouros.navigation.data.GeoJsonLineString
|
import com.kouros.navigation.data.GeoJsonType
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.maplibre.geojson.FeatureCollection
|
import org.maplibre.geojson.FeatureCollection
|
||||||
import org.maplibre.geojson.Point
|
import org.maplibre.geojson.Point
|
||||||
@@ -23,12 +23,9 @@ import java.time.ZonedDateTime
|
|||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.asin
|
|
||||||
import kotlin.math.atan2
|
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sin
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -149,9 +146,8 @@ object NavigationUtils {
|
|||||||
// 4. Create and return the Location object.
|
// 4. Create and return the Location object.
|
||||||
return location(centerPoint.longitude(), centerPoint.latitude())
|
return location(centerPoint.longitude(), centerPoint.latitude())
|
||||||
}
|
}
|
||||||
fun createGeoJson(lineCoordinates: List<List<Double>>): String {
|
fun createGeoJson(type : String, lineCoordinates: List<List<Double>>): String {
|
||||||
|
val lineString = GeoJsonType(type = type, coordinates = lineCoordinates)
|
||||||
val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates)
|
|
||||||
val feature = GeoJsonFeature(type = "Feature", geometry = lineString)
|
val feature = GeoJsonFeature(type = "Feature", geometry = lineString)
|
||||||
val featureCollection =
|
val featureCollection =
|
||||||
GeoJsonFeatureCollection(type = "FeatureCollection", features = listOf(feature))
|
GeoJsonFeatureCollection(type = "FeatureCollection", features = listOf(feature))
|
||||||
@@ -159,6 +155,8 @@ object NavigationUtils {
|
|||||||
return jsonString
|
return jsonString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun getOverpassBbox(location: Location, radius: Double): String {
|
fun getOverpassBbox(location: Location, radius: Double): String {
|
||||||
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
||||||
val neLon = bbox["ne"]?.get("lon")
|
val neLon = bbox["ne"]?.get("lon")
|
||||||
|
|||||||
Reference in New Issue
Block a user