feat: 添加当前位置Marker,显示实时设计高度,完善地形图代码
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Include this permission to grab user's general location -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- Include only if your app benefits from precise location access. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- Include only if your app benefits from precise location access. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -11,6 +11,15 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Orx">
|
||||
<activity
|
||||
android:name=".MapActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import ColorBrewer2Type
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import colorBrewer2Palettes
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.icegps.geotools.catmullrom.CatmullRomChain2
|
||||
import com.icegps.geotools.ktx.area
|
||||
import com.icegps.geotools.ktx.toColorInt
|
||||
import com.icegps.geotools.ktx.optimalHeight
|
||||
import com.icegps.geotools.ktx.toMapboxPoint
|
||||
import com.icegps.geotools.ktx.toast
|
||||
import com.icegps.geotools.marchingsquares.ShapeContour
|
||||
import com.icegps.geotools.marchingsquares.findContours
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.shared.ktx.TAG
|
||||
import com.icegps.triangulation.DelaunayTriangulation
|
||||
import com.icegps.triangulation.Triangle
|
||||
@@ -38,6 +37,7 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
@@ -54,17 +54,18 @@ class ContoursManager(
|
||||
private val gridSourceId: String = "grid-polygon-source-id"
|
||||
private val gridLayerId: String = "grid-polygon-layer-id"
|
||||
|
||||
private var contourSize: Int = 6
|
||||
private var contourSize: Int = 13
|
||||
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
|
||||
private var cellSize: Double? = 10.0
|
||||
val simplePalette = SimplePalette(
|
||||
range = 0.0..100.0
|
||||
)
|
||||
val mySimplePalette = SimplePalette(0.0..13.0)
|
||||
|
||||
private var colors = colorBrewer2Palettes(
|
||||
numberOfColors = contourSize,
|
||||
paletteType = ColorBrewer2Type.Any
|
||||
).first().colors.reversed()
|
||||
val CONTOUR_COLORS = arrayOf(
|
||||
"#CC0000", "#FF0000", "#FF3333", "#FF6666", "#FF9999", "#FFCCCC",
|
||||
"#8CED8C", "#B3D9FF", "#80BFFF", "#4D94FF", "#2673FF", "#004CFF", "#1517EC"
|
||||
).reversed()
|
||||
|
||||
private var points: List<Vector3D> = emptyList()
|
||||
|
||||
@@ -72,10 +73,6 @@ class ContoursManager(
|
||||
|
||||
fun updateContourSize(contourSize: Int) {
|
||||
this.contourSize = contourSize
|
||||
colors = colorBrewer2Palettes(
|
||||
numberOfColors = contourSize,
|
||||
paletteType = ColorBrewer2Type.Any
|
||||
).first().colors.reversed()
|
||||
}
|
||||
|
||||
fun updateCellSize(value: Double) {
|
||||
@@ -142,21 +139,72 @@ class ContoursManager(
|
||||
fun setTriangleVisible(visible: Boolean) {
|
||||
if (visible != isTriangleVisible) {
|
||||
isTriangleVisible = visible
|
||||
val sourceId = "triangle-source-id"
|
||||
val layerId = "triangle-layer-id"
|
||||
if (visible) {
|
||||
polylineManager.update(
|
||||
/*polylineManager.update(
|
||||
triangles.map {
|
||||
listOf(it.x1, it.x2, it.x3)
|
||||
.map { Vector3D(it.x, it.y, it.z) }
|
||||
}
|
||||
)*/
|
||||
mapView.displayTriangle(
|
||||
triangles = triangles,
|
||||
sourceId = sourceId,
|
||||
layerId = layerId
|
||||
)
|
||||
} else {
|
||||
polylineManager.clearContours()
|
||||
// polylineManager.clearContours()
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private val _slopeData = MutableStateFlow(SlopeData())
|
||||
val slopeData = _slopeData.asStateFlow()
|
||||
|
||||
data class SlopeData(
|
||||
val slopeDirection: Double = 0.0,
|
||||
val slopePercentage: Double = 0.0,
|
||||
val targetHeight: Double = 0.0,
|
||||
val optimalHeight: Double = 0.0
|
||||
)
|
||||
|
||||
fun updateSlopeData(block: (SlopeData) -> SlopeData) {
|
||||
_slopeData.update(block)
|
||||
}
|
||||
|
||||
fun updateTargetHeight(
|
||||
block: (ClosedFloatingPointRange<Double>) -> Double
|
||||
) {
|
||||
updateSlopeData {
|
||||
it.copy(targetHeight = block(heightRange))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContours(
|
||||
heightRange: ClosedFloatingPointRange<Double>,
|
||||
targetHeightRange: ClosedFloatingPointRange<Double>
|
||||
): List<ClosedFloatingPointRange<Double>> {
|
||||
val frontHalfHeight = targetHeightRange.start - heightRange.start
|
||||
val backHalfHeight = heightRange.endInclusive - targetHeightRange.endInclusive
|
||||
val frontHalfStep = frontHalfHeight / 6
|
||||
val backHalfStep = backHalfHeight / 6
|
||||
val frontHalfZip = (0..6).map { index ->
|
||||
heightRange.start + index * frontHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val backHalfZip = (0..6).map { index ->
|
||||
targetHeightRange.endInclusive + index * backHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
|
||||
return frontHalfZip + listOf(targetHeightRange) + backHalfZip
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
val points = points
|
||||
if (points.size <= 3) {
|
||||
@@ -166,24 +214,34 @@ class ContoursManager(
|
||||
job?.cancel()
|
||||
scope.launch {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
val step = heightRange.endInclusive / contourSize
|
||||
val zip = (0..contourSize).map { index ->
|
||||
heightRange.start + index * step
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val area = points.area
|
||||
val triangulation = DelaunayTriangulation(points)
|
||||
val triangles = triangulation.triangles()
|
||||
triangles = triangulation.triangles()
|
||||
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
|
||||
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
|
||||
} else {
|
||||
cellSize!!
|
||||
}
|
||||
// 生成栅格网
|
||||
val gridModel = triangulationToGrid(
|
||||
area = area,
|
||||
triangles = triangles,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
this@ContoursManager._gridModel.value = gridModel
|
||||
val optimalHeight = gridModel.optimalHeight()
|
||||
|
||||
// 根据目标高度生成坡面
|
||||
val (slopeDirection, slopePercentage, targetHeight, _) = _slopeData.value
|
||||
val height = if (targetHeight <= 0.0) optimalHeight else targetHeight
|
||||
val slopeResult = SlopeCalculator.calculateSlopeByTargetHeight(
|
||||
grid = gridModel,
|
||||
slopeDirection = slopeDirection,
|
||||
slopePercentage = slopePercentage,
|
||||
targetHeight = height
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
val gridModel = triangulationToGrid(
|
||||
delaunator = triangulation,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
this@ContoursManager._gridModel.value = gridModel
|
||||
if (isGridVisible) mapView.displayGridModel(
|
||||
grid = gridModel,
|
||||
sourceId = gridSourceId,
|
||||
@@ -192,6 +250,19 @@ class ContoursManager(
|
||||
)
|
||||
}
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
// 由目标高度计算等级
|
||||
fun simple(): List<ClosedFloatingPointRange<Double>> {
|
||||
val step = (heightRange.endInclusive - heightRange.start) / contourSize
|
||||
val zip = (0..contourSize).map { index ->
|
||||
heightRange.start + index * step
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
return zip
|
||||
}
|
||||
// val zip = simple()
|
||||
val zip = getContours(
|
||||
heightRange = heightRange,
|
||||
height..height
|
||||
)
|
||||
val lineFeatures = mutableListOf<List<Feature>>()
|
||||
val features = zip.mapIndexed { index, range ->
|
||||
async {
|
||||
@@ -201,13 +272,15 @@ class ContoursManager(
|
||||
area = area,
|
||||
cellSize = cellSize
|
||||
)
|
||||
val color = colors[index].toColorInt()
|
||||
// val color = colors[index].toColorInt()
|
||||
// val color = mySimplePalette.palette(index.toDouble()).toColorInt()
|
||||
val color = CONTOUR_COLORS[index].toColorInt()
|
||||
lineFeatures.add(contoursToLineFeatures(contours, color).flatten())
|
||||
contoursToPolygonFeatures(contours, color)
|
||||
}
|
||||
}.awaitAll()
|
||||
withContext(Dispatchers.Main) {
|
||||
if (false) setupLineLayer(
|
||||
if (true) setupLineLayer(
|
||||
style = style,
|
||||
sourceId = sourceId,
|
||||
layerId = layerId,
|
||||
@@ -296,8 +369,8 @@ class ContoursManager(
|
||||
style.addSource(source)
|
||||
|
||||
val layer = fillLayer(layerId, sourceId) {
|
||||
fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||
fillOpacity(0.5)
|
||||
fillColor(Expression.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||
fillOpacity(1.0)
|
||||
fillAntialias(true)
|
||||
}
|
||||
style.addLayer(layer)
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.icegps.geotools
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.util.Log
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.geotools.ktx.toVector2D
|
||||
import com.icegps.math.geometry.Angle
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.icegps.math.geometry.degrees
|
||||
@@ -40,7 +40,7 @@ object SlopeCalculator {
|
||||
val elevations = grid.cells.filterNotNull()
|
||||
val baseElevation = elevations.average() + baseHeightOffset
|
||||
|
||||
val basePoint = Triple(centerX, centerY, baseElevation)
|
||||
val basePoint: Triple<Double, Double, Double> = Triple(centerX, centerY, baseElevation)
|
||||
|
||||
val earthworkResult = EarthworkCalculator.calculateForSlopeDesign(
|
||||
grid = grid,
|
||||
@@ -64,6 +64,56 @@ object SlopeCalculator {
|
||||
)
|
||||
}
|
||||
|
||||
fun calculateSlope(
|
||||
grid: GridModel,
|
||||
slopeDirection: Double,
|
||||
slopePercentage: Double,
|
||||
basePoint: Triple<Double, Double, Double>
|
||||
): SlopeResult {
|
||||
val elevations = grid.cells.filterNotNull()
|
||||
val baseHeightOffset = basePoint.third - elevations.average()
|
||||
|
||||
val earthworkResult = EarthworkCalculator.calculateForSlopeDesign(
|
||||
grid = grid,
|
||||
basePoint = basePoint,
|
||||
slope = slopePercentage,
|
||||
aspect = slopeDirection
|
||||
)
|
||||
|
||||
return SlopeResult(
|
||||
slopeDirection = slopeDirection,
|
||||
slopePercentage = slopePercentage,
|
||||
baseHeightOffset = baseHeightOffset,
|
||||
baseElevation = basePoint.third,
|
||||
earthworkResult = earthworkResult,
|
||||
designSurface = generateSlopeDesignGrid(
|
||||
grid = grid,
|
||||
basePoint = basePoint,
|
||||
slopePercentage = slopePercentage,
|
||||
slopeDirection = slopeDirection
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun calculateSlopeByTargetHeight(
|
||||
grid: GridModel,
|
||||
slopeDirection: Double,
|
||||
slopePercentage: Double,
|
||||
targetHeight: Double = 0.0
|
||||
): SlopeResult {
|
||||
val centerX = (grid.minX + grid.maxX) / 2
|
||||
val centerY = (grid.minY + grid.maxY) / 2
|
||||
|
||||
val basePoint = Triple(centerX, centerY, targetHeight)
|
||||
|
||||
return calculateSlope(
|
||||
grid = grid,
|
||||
slopeDirection = slopeDirection,
|
||||
slopePercentage = slopePercentage,
|
||||
basePoint = basePoint
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成斜坡设计面网格(用于可视化)
|
||||
@@ -282,6 +332,17 @@ data class EarthworkResult(
|
||||
appendLine("总面积:${"%.1f".format(totalArea)} m²")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Empty = EarthworkResult(
|
||||
cutVolume = 0.0,
|
||||
fillVolume = 0.0,
|
||||
netVolume = 0.0,
|
||||
cutArea = 0.0,
|
||||
fillArea = 0.0,
|
||||
totalArea = 0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EarthworkManager(
|
||||
@@ -347,12 +408,6 @@ class EarthworkManager(
|
||||
)
|
||||
}
|
||||
|
||||
fun Point.toVector2D(): Vector2D {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
val enu = geoHelper.wgs84ToENU(lon = longitude(), lat = latitude(), hgt = 0.0)
|
||||
return Vector2D(enu.x, enu.y)
|
||||
}
|
||||
|
||||
fun removeOnMoveListener() {
|
||||
listener?.let(mapView.mapboxMap::removeOnMoveListener)
|
||||
listener = null
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.triangulation.DelaunayTriangulation
|
||||
import com.icegps.triangulation.Triangle
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.ceil
|
||||
|
||||
@@ -28,8 +29,21 @@ data class GridModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun GridModel.getHeightAt(point: Vector2D): Double? {
|
||||
if (point.x !in minX..<maxX || point.y < minY || point.y >= maxY) {
|
||||
return null
|
||||
}
|
||||
|
||||
val col = ((point.x - minX) / cellSize).toInt()
|
||||
val row = ((maxY - point.y) / cellSize).toInt()
|
||||
|
||||
val index = row * cols + col
|
||||
return cells.getOrNull(index)
|
||||
}
|
||||
|
||||
fun triangulationToGrid(
|
||||
delaunator: DelaunayTriangulation,
|
||||
area: Rectangle,
|
||||
triangles: List<Triangle>,
|
||||
cellSize: Double = 50.0,
|
||||
maxSidePixels: Int = 5000
|
||||
): GridModel {
|
||||
@@ -70,19 +84,13 @@ fun triangulationToGrid(
|
||||
return values[0] * wA + values[1] * wB + values[2] * wC
|
||||
}
|
||||
|
||||
val minX = area.x
|
||||
val maxX = area.x + area.width
|
||||
val minY = area.y
|
||||
val maxY = area.y + area.height
|
||||
|
||||
val pts = delaunator.points
|
||||
require(pts.isNotEmpty()) { "points empty" }
|
||||
|
||||
val x = pts.map { it.x }
|
||||
val y = pts.map { it.y }
|
||||
val minX = x.min()
|
||||
val maxX = x.max()
|
||||
val minY = y.min()
|
||||
val maxY = y.max()
|
||||
|
||||
val width = maxX - minX
|
||||
val height = maxY - minY
|
||||
val width = area.width
|
||||
val height = area.height
|
||||
|
||||
var cols = ceil(width / cellSize).toInt()
|
||||
var rows = ceil(height / cellSize).toInt()
|
||||
@@ -93,9 +101,6 @@ fun triangulationToGrid(
|
||||
|
||||
val cells = Array<Double?>(rows * cols) { null }
|
||||
|
||||
|
||||
val triangles = delaunator.triangles()
|
||||
|
||||
for (ti in 0 until triangles.size) {
|
||||
val (a, b, c) = triangles[ti]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
@@ -20,6 +21,7 @@ import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -36,6 +38,10 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var contoursManager: ContoursManager
|
||||
private lateinit var earthworkManager: EarthworkManager
|
||||
|
||||
private val geoHelper = GeoHelper.getSharedInstance()
|
||||
private lateinit var terrainManager: TerrainManager
|
||||
private lateinit var terrainUiController: TerrainUiController
|
||||
|
||||
init {
|
||||
initGeoHelper()
|
||||
}
|
||||
@@ -45,6 +51,13 @@ class MainActivity : AppCompatActivity() {
|
||||
enableEdgeToEdge()
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
mapView = binding.mapView
|
||||
terrainManager = TerrainManagerImpl(scope = lifecycleScope)
|
||||
terrainUiController = TerrainUiControllerImpl(
|
||||
context = this,
|
||||
mapView = mapView,
|
||||
scope = lifecycleScope,
|
||||
terrainManager = terrainManager,
|
||||
)
|
||||
earthworkManager = EarthworkManager(mapView, lifecycleScope)
|
||||
setContentView(binding.root)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
@@ -62,7 +75,18 @@ class MainActivity : AppCompatActivity() {
|
||||
.build()
|
||||
)
|
||||
|
||||
val points = coordinateGenerate()
|
||||
// val points = coordinateGenerate()
|
||||
|
||||
val pathGenerator = PathGenerator(
|
||||
centerLon = home.longitude,
|
||||
centerLat = home.latitude,
|
||||
widthMeters = 10 * 10.0,
|
||||
heightMeters = 20 * 10.0,
|
||||
minHeight = 30.0,
|
||||
maxHeight = 80.0,
|
||||
)
|
||||
val originPoints = pathGenerator.generatePath()
|
||||
viewModel.addPoints(originPoints)
|
||||
|
||||
// divider
|
||||
contoursManager = ContoursManager(
|
||||
@@ -70,16 +94,7 @@ class MainActivity : AppCompatActivity() {
|
||||
mapView = mapView,
|
||||
scope = lifecycleScope
|
||||
)
|
||||
contoursManager.updateContourSize(6)
|
||||
contoursManager.updatePoints(points)
|
||||
val height = points.map { it.z }
|
||||
val min = height.min()
|
||||
val max = height.max()
|
||||
contoursManager.updateHeightRange((min / 2)..max)
|
||||
binding.heightRange.values = listOf(min.toFloat() / 2, max.toFloat())
|
||||
binding.heightRange.valueFrom = min.toFloat()
|
||||
binding.heightRange.valueTo = max.toFloat()
|
||||
contoursManager.refresh()
|
||||
contoursManager.updateContourSize(13)
|
||||
|
||||
binding.sliderTargetHeight.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
@@ -142,7 +157,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateSlopeDirection(slider.value.degrees)
|
||||
// earthworkManager.updateSlopeDirection(slider.value.degrees)
|
||||
terrainManager.updateTerrainData { it.copy(slopeDirection = slider.value.degrees) }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -152,38 +168,67 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateSlopePercentage(slider.value.toDouble())
|
||||
// earthworkManager.updateSlopePercentage(slider.value.toDouble())
|
||||
terrainManager.updateTerrainData { it.copy(slopePercentage = slider.value.toDouble()) }
|
||||
}
|
||||
}
|
||||
)
|
||||
fun Slider.getRatioValue(): Float {
|
||||
val length = valueTo - valueFrom
|
||||
val position = value - valueFrom
|
||||
return position / length
|
||||
}
|
||||
binding.designHeight.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateDesignHeight(slider.value.toDouble())
|
||||
// earthworkManager.updateDesignHeight(slider.value.toDouble())
|
||||
/*contoursManager.updateTargetHeight {
|
||||
(it.endInclusive - it.start) * slider.getRatioValue()
|
||||
}
|
||||
contoursManager.refresh()*/
|
||||
terrainManager.updateTargetHeight {
|
||||
(it.endInclusive - it.start) * slider.getRatioValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
binding.switchDesignSurface.setOnCheckedChangeListener { button, isChecked ->
|
||||
binding.switchDesignSurface.setOnCheckedChangeListener { _, isChecked ->
|
||||
showDesignHeight.value = isChecked
|
||||
terrainUiController
|
||||
}
|
||||
earthworkManager.setupOnMoveListener()
|
||||
binding.switchSlopeResult.setOnCheckedChangeListener { _, isChecked ->
|
||||
slopeResultVisible.value = isChecked
|
||||
}
|
||||
binding.switchCatmullRom.setOnCheckedChangeListener { _, isChecked ->
|
||||
contoursManager.setCatmullRom(isChecked)
|
||||
contoursManager.refresh()
|
||||
terrainUiController.enabledCatmullRom(isChecked)
|
||||
}
|
||||
binding.targetHeight.doAfterTextChanged {
|
||||
|
||||
}
|
||||
initData()
|
||||
}
|
||||
|
||||
private val showDesignHeight = MutableStateFlow(false)
|
||||
private val slopeResultVisible = MutableStateFlow(false)
|
||||
private val targetHeight = MutableStateFlow(0.0)
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
private fun initData() {
|
||||
viewModel.points.onEach {
|
||||
viewModel.points.filter { it.size > 3 }.onEach {
|
||||
contoursManager.updatePoints(it)
|
||||
val height = it.map { it.z }
|
||||
val min = height.min()
|
||||
val max = height.max()
|
||||
contoursManager.updateHeightRange()
|
||||
binding.heightRange.values = listOf(min.toFloat(), max.toFloat())
|
||||
binding.heightRange.valueFrom = min.toFloat()
|
||||
binding.heightRange.valueTo = max.toFloat()
|
||||
contoursManager.refresh()
|
||||
}.launchIn(lifecycleScope)
|
||||
contoursManager.gridModel.filterNotNull().onEach {
|
||||
@@ -197,7 +242,7 @@ class MainActivity : AppCompatActivity() {
|
||||
combine(
|
||||
earthworkManager.slopeDirection,
|
||||
earthworkManager.slopePercentage,
|
||||
earthworkManager.baseHeightOffset,
|
||||
targetHeight,
|
||||
contoursManager.gridModel,
|
||||
showDesignHeight,
|
||||
slopeResultVisible
|
||||
@@ -210,7 +255,7 @@ class MainActivity : AppCompatActivity() {
|
||||
p5 = it[4] as Boolean,
|
||||
p6 = it[5] as Boolean
|
||||
)
|
||||
}.map { (slopeDirection, slopePercentage, baseHeightOffset, gridModel, showDesignHeight, slopeResultVisible) ->
|
||||
}.map { (slopeDirection, slopePercentage, targetHeight, gridModel, showDesignHeight, slopeResultVisible) ->
|
||||
if (!slopeResultVisible) {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
style.removeStyleLayer(slopeResultLayerId)
|
||||
@@ -218,12 +263,19 @@ class MainActivity : AppCompatActivity() {
|
||||
style.removeStyleSource(slopeResultSourceId)
|
||||
}
|
||||
} else gridModel?.let { gridModel ->
|
||||
val slopeResult: SlopeResult = SlopeCalculator.calculateSlope(
|
||||
/*val slopeResult: SlopeResult = SlopeCalculator.calculateSlope(
|
||||
grid = gridModel,
|
||||
slopeDirection = slopeDirection.degrees,
|
||||
slopePercentage = slopePercentage,
|
||||
baseHeightOffset = baseHeightOffset
|
||||
)*/
|
||||
val slopeResult = SlopeCalculator.calculateSlopeByTargetHeight(
|
||||
grid = gridModel,
|
||||
slopeDirection = slopeDirection.degrees,
|
||||
slopePercentage = slopePercentage,
|
||||
targetHeight = targetHeight
|
||||
)
|
||||
slopeResult.earthworkResult.cutArea
|
||||
mapView.displaySlopeResult(
|
||||
originalGrid = gridModel,
|
||||
slopeResult = slopeResult,
|
||||
@@ -253,7 +305,7 @@ data class Params6<
|
||||
val p6: P6,
|
||||
)
|
||||
|
||||
val home = GeoPoint(114.476060, 22.771073, 30.897)
|
||||
val home = GeoPoint(longitude = 114.476060, latitude = 22.771073, altitude = 30.897)
|
||||
|
||||
fun initGeoHelper(base: GeoPoint = home) {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
|
||||
@@ -5,14 +5,15 @@ import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.geotools.ktx.toast
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.shared.SharedHttpClient
|
||||
import com.icegps.shared.SharedJson
|
||||
import com.icegps.shared.api.OpenElevation
|
||||
import com.icegps.shared.api.OpenElevationApi
|
||||
import com.icegps.shared.ktx.TAG
|
||||
import com.icegps.shared.model.GeoPoint
|
||||
import com.icegps.shared.model.IGeoPoint
|
||||
import com.mapbox.geojson.Point
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -24,12 +25,28 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class MainViewModel(private val context: Application) : AndroidViewModel(context) {
|
||||
private val geoHelper = GeoHelper.Companion.getSharedInstance()
|
||||
private val geoHelper = GeoHelper.getSharedInstance()
|
||||
private val openElevation: OpenElevationApi = OpenElevation(SharedHttpClient(SharedJson()))
|
||||
|
||||
private val _points = MutableStateFlow<List<Point>>(emptyList())
|
||||
val points = _points.filter { it.size > 3 }.debounce(1000).map {
|
||||
openElevation.lookup(it.map { GeoPoint(it.longitude(), it.latitude(), it.altitude()) })
|
||||
it.filter { it.altitude().isNaN() }.let {
|
||||
if (it.isEmpty()) it.map {
|
||||
GeoPoint(
|
||||
longitude = it.longitude(),
|
||||
latitude = it.latitude(),
|
||||
altitude = it.altitude()
|
||||
)
|
||||
} else {
|
||||
openElevation.lookup(it.map { GeoPoint(longitude = it.longitude(), latitude = it.latitude(), altitude = it.altitude()) })
|
||||
}
|
||||
} + it.filter { !it.altitude().isNaN() }.map {
|
||||
GeoPoint(
|
||||
longitude = it.longitude(),
|
||||
latitude = it.latitude(),
|
||||
altitude = it.altitude()
|
||||
)
|
||||
}
|
||||
}.catch {
|
||||
Log.e(TAG, "高程请求失败", it)
|
||||
context.toast("高程请求失败")
|
||||
@@ -40,7 +57,7 @@ class MainViewModel(private val context: Application) : AndroidViewModel(context
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
@@ -53,6 +70,12 @@ class MainViewModel(private val context: Application) : AndroidViewModel(context
|
||||
}
|
||||
}
|
||||
|
||||
fun addPoints(points: List<IGeoPoint>) {
|
||||
_points.value = points.map {
|
||||
Point.fromLngLat(it.longitude, it.latitude, it.altitude)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPoints() {
|
||||
_points.value = emptyList()
|
||||
}
|
||||
|
||||
228
android/src/main/java/com/icegps/geotools/MapActivity.kt
Normal file
228
android/src/main/java/com/icegps/geotools/MapActivity.kt
Normal file
@@ -0,0 +1,228 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.geotools.databinding.ActivityMapBinding
|
||||
import com.icegps.math.geometry.degrees
|
||||
import com.icegps.shared.ktx.TAG
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.maps.CameraOptions
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class MapActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMapBinding
|
||||
private lateinit var mapView: MapView
|
||||
private lateinit var terrainManager: TerrainManager
|
||||
private lateinit var terrainUiController: TerrainUiController
|
||||
|
||||
private val pathGenerator = PathGenerator(home)
|
||||
private val geoHelper = GeoHelper.getSharedInstance()
|
||||
|
||||
init {
|
||||
initGeoHelper(home)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = ActivityMapBinding.inflate(layoutInflater)
|
||||
mapView = binding.mapView
|
||||
terrainManager = TerrainManagerImpl(scope = lifecycleScope)
|
||||
terrainUiController = TerrainUiControllerImpl(
|
||||
context = this,
|
||||
mapView = mapView,
|
||||
scope = lifecycleScope,
|
||||
terrainManager = terrainManager,
|
||||
)
|
||||
setContentView(binding.root)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
initView()
|
||||
initData()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
setupMap()
|
||||
setupCheckbox()
|
||||
setupSlider()
|
||||
binding.spinnerGridType.apply {
|
||||
adapter = ArrayAdapter(
|
||||
this@MapActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
GridType.entries.map { it.text }
|
||||
).apply {
|
||||
this.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
terrainUiController.updateGridType(GridType.entries[position])
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Slider.onValueChange(block: Slider.(Double) -> Unit) {
|
||||
addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
block(slider, slider.value.toDouble())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun RangeSlider.onValueChange(block: RangeSlider.(ClosedFloatingPointRange<Double>) -> Unit) {
|
||||
addOnSliderTouchListener(
|
||||
object : RangeSlider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: RangeSlider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: RangeSlider) {
|
||||
block(slider, slider.values.run { min().toDouble()..max().toDouble() })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setupSlider() {
|
||||
binding.sliderCellSize.onValueChange { value ->
|
||||
terrainManager.updateTerrainData { it.copy(cellSize = value) }
|
||||
terrainManager.update()
|
||||
}
|
||||
binding.sliderSlopeDirection.onValueChange { value ->
|
||||
terrainManager.updateTerrainData { it.copy(slopeDirection = value.degrees) }
|
||||
}
|
||||
binding.sliderSlopePercentage.onValueChange { value ->
|
||||
terrainManager.updateTerrainData { it.copy(slopePercentage = value) }
|
||||
}
|
||||
binding.sliderTargetHeight.onValueChange { value ->
|
||||
terrainManager.updateTerrainData { it.copy(targetHeight = value) }
|
||||
terrainManager.update()
|
||||
}
|
||||
binding.sliderHeightRange.onValueChange { range ->
|
||||
terrainManager.updateTerrainData { it.copy(heightRange = range) }
|
||||
terrainManager.update()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCheckbox() {
|
||||
with(terrainUiController.terrainUiState.value) {
|
||||
binding.checkboxTriangle.isChecked = isTriangleVisible
|
||||
binding.checkboxGrid.isChecked = isGridVisible
|
||||
binding.checkboxContourLine.isChecked = isContourVisible
|
||||
binding.checkboxContourLineLabel.isChecked = isContourLabelingVisible
|
||||
binding.checkboxGridHeightDiff.isChecked = isGridHeightDiffVisible
|
||||
binding.checkboxControllableArrow.isChecked = isControllableArrowVisible
|
||||
}
|
||||
binding.checkboxTriangle.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setTriangleVisible(isChecked)
|
||||
}
|
||||
binding.checkboxGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setGridVisible(isChecked)
|
||||
}
|
||||
binding.checkboxContourLine.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setContourVisible(isChecked)
|
||||
}
|
||||
binding.checkboxContourLineLabel.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setContourLabelingVisible(isChecked)
|
||||
}
|
||||
binding.checkboxGridHeightDiff.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setGridHeightDiffVisible(isChecked)
|
||||
}
|
||||
binding.checkboxControllableArrow.setOnCheckedChangeListener { _, isChecked ->
|
||||
terrainUiController.setControllableArrowVisible(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMap() {
|
||||
mapView.mapboxMap.setCamera(
|
||||
CameraOptions.Builder()
|
||||
.center(Point.fromLngLat(home.longitude, home.latitude))
|
||||
.pitch(0.0)
|
||||
.zoom(18.0)
|
||||
.bearing(0.0)
|
||||
.build()
|
||||
)
|
||||
mapView.mapboxMap.addOnMapClickListener { point ->
|
||||
// point.toVector2D()
|
||||
// terrainManager.addPoint()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
setupMapData()
|
||||
terrainManager.points.filter { it.size > 3 }.map {
|
||||
val heightList = it.map { it.z }
|
||||
val min = heightList.min().toFloat()
|
||||
val max = heightList.max().toFloat()
|
||||
Log.d(TAG, "initData: $min, $max")
|
||||
binding.sliderHeightRange.apply {
|
||||
values = listOf(min, max)
|
||||
valueFrom = min
|
||||
valueTo = max
|
||||
}
|
||||
binding.sliderTargetHeight.apply {
|
||||
if (value !in min..max) {
|
||||
value = heightList.average().toFloat()
|
||||
}
|
||||
valueFrom = min
|
||||
valueTo = max
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
terrainManager.terrainData.onEach {
|
||||
binding.tvSlopePercentage.text = "坡度:%.2f".format(it.slopePercentage)
|
||||
binding.tvCurrentHeight.text = "当前高度:%.2f m".format(it.currentHeight)
|
||||
binding.tvTargetHeight.text = "目标高度:%.2f m".format(it.targetHeight ?: it.optimalHeight)
|
||||
binding.tvOptimalHeight.text = "最佳高度:%.2f m".format(it.optimalHeight)
|
||||
}.launchIn(lifecycleScope)
|
||||
terrainManager.earthworkResult.onEach {
|
||||
binding.tvArea.text = "包围面积:%.2f m²".format(it.totalArea)
|
||||
binding.tvCutVolume.text = "挖土方量:%.2f m³".format(it.cutVolume)
|
||||
binding.tvFillVolume.text = "填土方量:%.2f m³".format(it.fillVolume)
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun setupMapData() {
|
||||
val points = pathGenerator.generatePath().map {
|
||||
geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude)
|
||||
}.map {
|
||||
Vector3D(it.x, it.y, it.z)
|
||||
}
|
||||
terrainManager.setPoints(points)
|
||||
terrainManager.updateHeightRange()
|
||||
terrainManager.update()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum class GridType(
|
||||
val text: String
|
||||
) {
|
||||
Origin("原始栅格"), Design("目标栅格")
|
||||
}
|
||||
214
android/src/main/java/com/icegps/geotools/PathGenerator.kt
Normal file
214
android/src/main/java/com/icegps/geotools/PathGenerator.kt
Normal file
@@ -0,0 +1,214 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.shared.model.GeoPoint
|
||||
import com.icegps.shared.model.GeodeticCoordinate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* 路径生成器
|
||||
*
|
||||
* @param centerLon 区域中心经度
|
||||
* @param centerLat 区域中心纬度
|
||||
* @param widthMeters 区域宽度(米)
|
||||
* @param heightMeters 区域高度(米)
|
||||
* @param minHeight 最低高度(米)
|
||||
* @param maxHeight 最高高度(米)
|
||||
* @param numZigzagLines Zigzag 路径的线条数
|
||||
* @param cornerRadiusRatio 圆角半径与较短边长的比例
|
||||
* @param pointSpacingMeters 路径点之间的间隔(米)
|
||||
*/
|
||||
class PathGenerator(
|
||||
private val centerLon: Double,
|
||||
private val centerLat: Double,
|
||||
private val widthMeters: Double,
|
||||
private val heightMeters: Double,
|
||||
private val minHeight: Double,
|
||||
private val maxHeight: Double,
|
||||
private val numZigzagLines: Int = 10,
|
||||
private val cornerRadiusRatio: Double = 0.1,
|
||||
private val pointSpacingMeters: Double = 5.0
|
||||
) {
|
||||
private val metersPerDegreeLat = 111000.0
|
||||
private val metersPerDegreeLon = cos(centerLat.toRadians()) * 111000.0
|
||||
private val width = widthMeters / metersPerDegreeLon
|
||||
private val height = heightMeters / metersPerDegreeLat
|
||||
private val cornerRadius = minOf(width, height) * cornerRadiusRatio
|
||||
private val pointSpacing = pointSpacingMeters / metersPerDegreeLat
|
||||
|
||||
/**
|
||||
* 创建带圆角的矩形边界
|
||||
*/
|
||||
private fun createRoundedRectangle(numPoints: Int = 100): Pair<List<Double>, List<Double>> {
|
||||
val t = (0 until numPoints).map { it * 2 * PI / numPoints }
|
||||
|
||||
val corners = listOf(
|
||||
Pair(centerLon + width / 2 - cornerRadius, centerLat + height / 2 - cornerRadius),
|
||||
Pair(centerLon - width / 2 + cornerRadius, centerLat + height / 2 - cornerRadius),
|
||||
Pair(centerLon - width / 2 + cornerRadius, centerLat - height / 2 + cornerRadius),
|
||||
Pair(centerLon + width / 2 - cornerRadius, centerLat - height / 2 + cornerRadius)
|
||||
)
|
||||
|
||||
val lon = mutableListOf<Double>()
|
||||
val lat = mutableListOf<Double>()
|
||||
|
||||
corners.forEachIndexed { i, (x, y) ->
|
||||
val quarter = numPoints / 4
|
||||
lon.addAll((i * quarter until (i + 1) * quarter).map { x + cornerRadius * cos(t[it]) })
|
||||
lat.addAll((i * quarter until (i + 1) * quarter).map { y + cornerRadius * sin(t[it]) })
|
||||
}
|
||||
|
||||
lon.add(lon[0])
|
||||
lat.add(lat[0])
|
||||
|
||||
return Pair(lon, lat)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对角线 zigzag 路径
|
||||
*/
|
||||
private fun createDiagonalZigzagPath(startLon: Double, startLat: Double): Pair<List<Double>, List<Double>> {
|
||||
val width = this.width
|
||||
val height = this.height
|
||||
val numLines = this.numZigzagLines
|
||||
|
||||
val lonStart = startLon
|
||||
val lonEnd = if (startLon > centerLon) startLon - width else startLon + width
|
||||
val latStart = startLat
|
||||
val latEnd = if (startLat < centerLat) startLat + height else startLat - height
|
||||
|
||||
val latStep = abs(latEnd - latStart) / (numLines)
|
||||
|
||||
val pathLon = mutableListOf<Double>()
|
||||
val pathLat = mutableListOf<Double>()
|
||||
|
||||
for (i in 0 until numLines) {
|
||||
if (i % 2 == 0) {
|
||||
pathLon.add(lonStart)
|
||||
pathLon.add(lonEnd)
|
||||
} else {
|
||||
pathLon.add(lonEnd)
|
||||
pathLon.add(lonStart)
|
||||
}
|
||||
val currentLat = if (latStart < latEnd) latStart + i * latStep else latStart - i * latStep
|
||||
val nextLat = if (latStart < latEnd) latStart + (i + 1) * latStep else latStart - (i + 1) * latStep
|
||||
pathLat.add(currentLat)
|
||||
pathLat.add(nextLat)
|
||||
}
|
||||
|
||||
val minLon = centerLon - width / 2
|
||||
val maxLon = centerLon + width / 2
|
||||
val minLat = centerLat - height / 2
|
||||
val maxLat = centerLat + height / 2
|
||||
|
||||
pathLon.replaceAll { it.coerceIn(minLon, maxLon) }
|
||||
pathLat.replaceAll { it.coerceIn(minLat, maxLat) }
|
||||
|
||||
return Pair(pathLon, pathLat)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据经纬度位置计算高度
|
||||
*/
|
||||
private fun calculateHeight(lon: Double, lat: Double): Double {
|
||||
val normalizedPosition = (lon - (centerLon - width / 2)) / width
|
||||
return minHeight + (maxHeight - minHeight) * normalizedPosition
|
||||
}
|
||||
|
||||
/**
|
||||
* 对给定的点进行插值,使路径更平滑
|
||||
*/
|
||||
private fun interpolatePoints(lon: List<Double>, lat: List<Double>): Pair<List<Double>, List<Double>> {
|
||||
val dist = mutableListOf(0.0)
|
||||
for (i in 1 until lon.size) {
|
||||
dist.add(dist.last() + sqrt((lon[i] - lon[i - 1]).pow(2) + (lat[i] - lat[i - 1]).pow(2)))
|
||||
}
|
||||
|
||||
val numPoints = (dist.last() / pointSpacing).toInt()
|
||||
val newDist = (0 until numPoints).map { it * dist.last() / (numPoints - 1) }
|
||||
|
||||
val newLon = newDist.map { d -> lon.interpolate(dist, d) }
|
||||
val newLat = newDist.map { d -> lat.interpolate(dist, d) }
|
||||
|
||||
return Pair(newLon, newLat)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的路径,返回 GeodeticCoordinate 列表
|
||||
*/
|
||||
fun generatePath(): List<GeodeticCoordinate> {
|
||||
val (boundaryLon, boundaryLat) = createRoundedRectangle()
|
||||
val (interpolatedBoundaryLon, interpolatedBoundaryLat) = interpolatePoints(boundaryLon, boundaryLat)
|
||||
|
||||
val startLon = interpolatedBoundaryLon.first()
|
||||
val startLat = interpolatedBoundaryLat.first()
|
||||
val (pathLon, pathLat) = createDiagonalZigzagPath(startLon, startLat)
|
||||
val (interpolatedPathLon, interpolatedPathLat) = interpolatePoints(pathLon, pathLat)
|
||||
|
||||
val allLon = interpolatedBoundaryLon + interpolatedPathLon
|
||||
val allLat = interpolatedBoundaryLat + interpolatedPathLat
|
||||
|
||||
return allLon.zip(allLat).map { (lon, lat) ->
|
||||
GeodeticCoordinate(lat, lon, calculateHeight(lon, lat))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成路径并以 Flow 形式发送 GeodeticCoordinate
|
||||
*/
|
||||
fun generatePathFlow(): Flow<GeodeticCoordinate> = flow {
|
||||
val (boundaryLon, boundaryLat) = createRoundedRectangle()
|
||||
val (interpolatedBoundaryLon, interpolatedBoundaryLat) = interpolatePoints(boundaryLon, boundaryLat)
|
||||
val random = Random(1)
|
||||
|
||||
// 发送边界点
|
||||
interpolatedBoundaryLon.zip(interpolatedBoundaryLat).forEach { (lon, lat) ->
|
||||
emit(GeodeticCoordinate(lat, lon, calculateHeight(lon, lat)))
|
||||
// emit(GeodeticCoordinate(lat, lon, 0.0))
|
||||
// emit(GeodeticCoordinate(lat, lon, random.nextDouble(0.001, 0.002)))
|
||||
}
|
||||
|
||||
val startLon = interpolatedBoundaryLon.first()
|
||||
val startLat = interpolatedBoundaryLat.first()
|
||||
val (pathLon, pathLat) = createDiagonalZigzagPath(startLon, startLat)
|
||||
val (interpolatedPathLon, interpolatedPathLat) = interpolatePoints(pathLon, pathLat)
|
||||
|
||||
// 发送内部路径点
|
||||
interpolatedPathLon.zip(interpolatedPathLat).forEach { (lon, lat) ->
|
||||
emit(GeodeticCoordinate(lat, lon, calculateHeight(lon, lat)))
|
||||
// emit(GeodeticCoordinate(lat, lon, 0.0))
|
||||
// emit(GeodeticCoordinate(lat, lon, random.nextDouble(0.001, 0.002)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Double>.interpolate(x: List<Double>, xi: Double): Double {
|
||||
val i = x.binarySearch(xi).let { if (it < 0) -(it + 1) - 1 else it }
|
||||
if (i == x.lastIndex) return this.last()
|
||||
val t = (xi - x[i]) / (x[i + 1] - x[i])
|
||||
return this[i] * (1 - t) + this[i + 1] * t
|
||||
}
|
||||
|
||||
private fun Double.toRadians() = this * PI / 180
|
||||
}
|
||||
|
||||
fun PathGenerator(
|
||||
center: GeoPoint = GeoPoint(longitude = 114.24357285, latitude = 22.70470464, altitude = 0.0)
|
||||
): PathGenerator {
|
||||
return PathGenerator(
|
||||
centerLon = center.longitude,
|
||||
centerLat = center.latitude,
|
||||
widthMeters = 50.0,
|
||||
heightMeters = 100.0,
|
||||
minHeight = 100.0,
|
||||
maxHeight = 102.0,
|
||||
numZigzagLines = 10,
|
||||
pointSpacingMeters = 5.0,
|
||||
)
|
||||
}
|
||||
994
android/src/main/java/com/icegps/geotools/TerrainManager.kt
Normal file
994
android/src/main/java/com/icegps/geotools/TerrainManager.kt
Normal file
@@ -0,0 +1,994 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import com.icegps.geotools.catmullrom.CatmullRomChain2
|
||||
import com.icegps.geotools.ktx.area
|
||||
import com.icegps.geotools.ktx.optimalHeight
|
||||
import com.icegps.geotools.ktx.toMapboxPoint
|
||||
import com.icegps.geotools.ktx.toVector2D
|
||||
import com.icegps.geotools.ktx.toast
|
||||
import com.icegps.geotools.marchingsquares.ShapeContour
|
||||
import com.icegps.geotools.marchingsquares.findContours
|
||||
import com.icegps.math.geometry.Angle
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.degrees
|
||||
import com.icegps.shared.ktx.TAG
|
||||
import com.icegps.shared.model.GeoPoint
|
||||
import com.icegps.triangulation.DelaunayTriangulation
|
||||
import com.icegps.triangulation.Triangle
|
||||
import com.mapbox.android.gestures.MoveGestureDetector
|
||||
import com.mapbox.geojson.Feature
|
||||
import com.mapbox.geojson.FeatureCollection
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.geojson.Polygon
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.ScreenCoordinate
|
||||
import com.mapbox.maps.Style
|
||||
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
||||
import com.mapbox.maps.extension.style.layers.addLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.fillLayer
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.TextAnchor
|
||||
import com.mapbox.maps.extension.style.sources.addSource
|
||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||
import com.mapbox.maps.plugin.annotation.Annotation
|
||||
import com.mapbox.maps.plugin.annotation.annotations
|
||||
import com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener
|
||||
import com.mapbox.maps.plugin.annotation.generated.PointAnnotation
|
||||
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager
|
||||
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions
|
||||
import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager
|
||||
import com.mapbox.maps.plugin.gestures.OnMoveListener
|
||||
import com.mapbox.maps.plugin.gestures.addOnMoveListener
|
||||
import com.mapbox.maps.plugin.gestures.removeOnMoveListener
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// typealias GeoPoint = com.mapbox.geojson.Point
|
||||
typealias GeoPoint = GeoPoint
|
||||
typealias Vector2D = com.icegps.math.geometry.Vector2D
|
||||
typealias Vector3D = com.icegps.math.geometry.Vector3D
|
||||
|
||||
/**
|
||||
* 地形图数据管理
|
||||
*
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/27
|
||||
*/
|
||||
interface TerrainManager {
|
||||
/**
|
||||
* 地形图数据
|
||||
*/
|
||||
val terrainData: StateFlow<TerrainData>
|
||||
/**
|
||||
* 土方量数据
|
||||
*/
|
||||
val earthworkResult: StateFlow<EarthworkResult>
|
||||
/**
|
||||
* 坐标点集合
|
||||
*/
|
||||
val points: StateFlow<List<Vector3D>>
|
||||
/**
|
||||
* 等高线
|
||||
*/
|
||||
val contoursLines: StateFlow<List<List<ShapeContour>>>
|
||||
fun updateTerrainData(block: (TerrainData) -> TerrainData)
|
||||
fun updateTargetHeight(block: (ClosedFloatingPointRange<Double>) -> Double)
|
||||
fun addPoint(point: Vector3D)
|
||||
fun setPoints(points: List<Vector3D>)
|
||||
fun clearPoints()
|
||||
fun updateHeightRange(range: ClosedFloatingPointRange<Double>? = null)
|
||||
fun update()
|
||||
}
|
||||
|
||||
class TerrainManagerImpl(
|
||||
private val scope: CoroutineScope
|
||||
) : TerrainManager {
|
||||
private val _terrainData = MutableStateFlow(TerrainData.Empty)
|
||||
override val terrainData: StateFlow<TerrainData> = _terrainData.asStateFlow()
|
||||
private val _earthworkResult = MutableStateFlow(EarthworkResult.Empty)
|
||||
override val earthworkResult: StateFlow<EarthworkResult> = _earthworkResult.asStateFlow()
|
||||
private val _points = MutableStateFlow<List<Vector3D>>(emptyList())
|
||||
override val points: StateFlow<List<Vector3D>> = _points.asStateFlow()
|
||||
private val _contoursLines = MutableStateFlow<List<List<ShapeContour>>>(emptyList())
|
||||
override val contoursLines: StateFlow<List<List<ShapeContour>>> = _contoursLines.asStateFlow()
|
||||
|
||||
data class Params4<
|
||||
out P1,
|
||||
out P2,
|
||||
out P3,
|
||||
out P4,
|
||||
>(
|
||||
val p1: P1,
|
||||
val p2: P2,
|
||||
val p3: P3,
|
||||
val p4: P4,
|
||||
)
|
||||
|
||||
init {
|
||||
terrainData.map {
|
||||
val gridModel = it.gridModel
|
||||
val slopeDirection = it.slopeDirection
|
||||
val slopePercentage = it.slopePercentage
|
||||
val targetHeight = it.targetHeight ?: it.optimalHeight
|
||||
gridModel?.let {
|
||||
Params4(
|
||||
p1 = gridModel,
|
||||
p2 = slopeDirection,
|
||||
p3 = slopePercentage,
|
||||
p4 = targetHeight,
|
||||
)
|
||||
}
|
||||
}.filterNotNull().distinctUntilChanged().map { (gridModel, slopeDirection, slopePercentage, targetHeight) ->
|
||||
SlopeCalculator.calculateSlopeByTargetHeight(
|
||||
grid = gridModel,
|
||||
slopeDirection = slopeDirection.degrees,
|
||||
slopePercentage = slopePercentage,
|
||||
targetHeight = targetHeight
|
||||
)
|
||||
}.onEach { result ->
|
||||
_terrainData.update { it.copy(designGrid = result.designSurface) }
|
||||
_earthworkResult.value = result.earthworkResult
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
override fun updateTerrainData(block: (TerrainData) -> TerrainData) {
|
||||
_terrainData.update(block)
|
||||
}
|
||||
|
||||
private var updateTargetHeightJob: Job? = null
|
||||
|
||||
override fun updateTargetHeight(block: (ClosedFloatingPointRange<Double>) -> Double) {
|
||||
updateTargetHeightJob?.cancel()
|
||||
updateTargetHeightJob = scope.launch(Dispatchers.IO) {
|
||||
val heightRange = terrainData.value.heightRange
|
||||
val targetHeight = block(heightRange)
|
||||
updateTerrainData { it.copy(targetHeight = targetHeight) }
|
||||
val area = points.value.area
|
||||
val cellSize = terrainData.value.cellSize
|
||||
val triangles = terrainData.value.triangles
|
||||
updateContoursLines(
|
||||
heightRange = heightRange,
|
||||
area = area,
|
||||
cellSize = cellSize,
|
||||
triangles = triangles,
|
||||
targetHeightRange = targetHeight..targetHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addPoint(point: Vector3D) {
|
||||
_points.update {
|
||||
it.toMutableList().apply { add(point) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPoints(points: List<Vector3D>) {
|
||||
_points.value = points
|
||||
}
|
||||
|
||||
override fun clearPoints() {
|
||||
_points.value = emptyList()
|
||||
}
|
||||
|
||||
override fun updateHeightRange(range: ClosedFloatingPointRange<Double>?) {
|
||||
val points = _points.value
|
||||
if (range == null && points.isNotEmpty()) {
|
||||
val heightList = points.map { it.z }
|
||||
updateTerrainData { it.copy(heightRange = heightList.min()..heightList.max()) }
|
||||
}
|
||||
if (range != null) {
|
||||
updateTerrainData { it.copy(heightRange = range) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*private val _gridModel = MutableStateFlow<GridModel?>(null)
|
||||
val triangles = _points.map {
|
||||
DelaunayTriangulation(it).triangles()
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList()
|
||||
)
|
||||
val gridModel = combine(
|
||||
triangles,
|
||||
_terrainData.map { it.cellSize }.distinctUntilChanged()
|
||||
) { triangles, cellSize ->
|
||||
|
||||
}*/
|
||||
private var updateJob: Job? = null
|
||||
|
||||
// points, cellSize, heightRange, targetHeight
|
||||
override fun update() {
|
||||
updateJob?.cancel()
|
||||
updateJob = scope.launch(Dispatchers.IO) {
|
||||
val points = _points.value
|
||||
val area = points.area
|
||||
val triangles = DelaunayTriangulation(points).triangles()
|
||||
val cellSize = terrainData.value.cellSize
|
||||
val gridModel = triangulationToGrid(
|
||||
area = area,
|
||||
triangles = triangles,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
val optimalHeight = gridModel.optimalHeight()
|
||||
val heightRange = terrainData.value.heightRange
|
||||
val targetHeight = terrainData.value.targetHeight ?: optimalHeight
|
||||
Log.d(
|
||||
TAG,
|
||||
buildString {
|
||||
appendLine("update: ")
|
||||
appendLine("points size = ${points.size}")
|
||||
appendLine("area = $area")
|
||||
appendLine("triangles size = ${triangles.size}")
|
||||
appendLine("cellSize = $cellSize")
|
||||
appendLine("gridModel = $gridModel")
|
||||
appendLine("optimalHeight = $optimalHeight")
|
||||
appendLine("heightRange = $heightRange")
|
||||
appendLine("targetHeight = $targetHeight")
|
||||
}
|
||||
)
|
||||
updateContoursLines(
|
||||
heightRange = heightRange,
|
||||
area = area,
|
||||
cellSize = cellSize,
|
||||
triangles = triangles,
|
||||
targetHeightRange = targetHeight..targetHeight
|
||||
)
|
||||
updateTerrainData {
|
||||
it.copy(triangles = triangles, gridModel = gridModel, optimalHeight = optimalHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateContoursLines(
|
||||
heightRange: ClosedFloatingPointRange<Double>,
|
||||
area: Rectangle,
|
||||
cellSize: Double,
|
||||
triangles: List<Triangle>,
|
||||
targetHeightRange: ClosedFloatingPointRange<Double>
|
||||
) = coroutineScope {
|
||||
val contoursLines: List<List<ShapeContour>> = getContoursLines(
|
||||
heightRange = heightRange,
|
||||
targetHeightRange = targetHeightRange
|
||||
).map { range ->
|
||||
async {
|
||||
findContours(
|
||||
triangles = triangles,
|
||||
range = range,
|
||||
area = area,
|
||||
cellSize = cellSize
|
||||
)
|
||||
}
|
||||
}.awaitAll()
|
||||
_contoursLines.value = contoursLines
|
||||
}
|
||||
|
||||
private fun findContours(
|
||||
triangles: List<Triangle>,
|
||||
range: ClosedFloatingPointRange<Double>,
|
||||
area: Rectangle,
|
||||
cellSize: Double
|
||||
): List<ShapeContour> {
|
||||
return findContours(
|
||||
f = { v ->
|
||||
val triangle = triangles.firstOrNull { triangle ->
|
||||
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||
}
|
||||
(triangle?.let { triangle ->
|
||||
val interpolate = interpolateHeight(
|
||||
point = v,
|
||||
triangle = listOf(
|
||||
triangle.x1,
|
||||
triangle.x2,
|
||||
triangle.x3,
|
||||
)
|
||||
)
|
||||
if (interpolate.z in range) -1.0
|
||||
else 1.0
|
||||
} ?: 1.0).also {
|
||||
if (false) Log.d(TAG, "findContours: ${v} -> ${it}")
|
||||
}
|
||||
},
|
||||
area = area,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getContoursLines(
|
||||
heightRange: ClosedFloatingPointRange<Double>,
|
||||
targetHeightRange: ClosedFloatingPointRange<Double>
|
||||
): List<ClosedFloatingPointRange<Double>> {
|
||||
val frontHalfHeight = targetHeightRange.start - heightRange.start
|
||||
val backHalfHeight = heightRange.endInclusive - targetHeightRange.endInclusive
|
||||
val frontHalfStep = frontHalfHeight / 6
|
||||
val backHalfStep = backHalfHeight / 6
|
||||
val frontHalfZip = (0..6).map { index ->
|
||||
heightRange.start + index * frontHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val backHalfZip = (0..6).map { index ->
|
||||
targetHeightRange.endInclusive + index * backHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
|
||||
return frontHalfZip + listOf(targetHeightRange) + backHalfZip
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property slopeDirection 坡向
|
||||
* @property slopePercentage 坡度(0~100)
|
||||
* @property currentHeight 当前高度
|
||||
* @property targetHeight 目标高度
|
||||
* @property cellSize 栅格大小
|
||||
* @property heightRange 高度范围
|
||||
* @property gridModel 原始栅格数据
|
||||
* @property designGrid 目标栅格数据
|
||||
* @property triangles 三角网数据
|
||||
* @property optimalHeight 最佳高度
|
||||
*/
|
||||
data class TerrainData(
|
||||
val slopeDirection: Angle,
|
||||
val slopePercentage: Double,
|
||||
val currentHeight: Double?,
|
||||
val targetHeight: Double?,
|
||||
val cellSize: Double,
|
||||
val heightRange: ClosedFloatingPointRange<Double>,
|
||||
val gridModel: GridModel?,
|
||||
val designGrid: GridModel?,
|
||||
val triangles: List<Triangle>,
|
||||
val optimalHeight: Double,
|
||||
) {
|
||||
companion object {
|
||||
val Empty = TerrainData(
|
||||
slopeDirection = 0.degrees,
|
||||
slopePercentage = 0.0,
|
||||
currentHeight = null,
|
||||
targetHeight = null,
|
||||
cellSize = 10.0,
|
||||
heightRange = 0.0..100.0,
|
||||
gridModel = null,
|
||||
designGrid = null,
|
||||
triangles = emptyList(),
|
||||
optimalHeight = 0.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property isTriangleVisible 三角网图层
|
||||
* @property isGridVisible 栅格图层
|
||||
* @property isContourVisible 等高线图层
|
||||
* @property isContourLabelingVisible 等高线标注图层
|
||||
* @property isGridHeightDiffVisible 高差数字图层
|
||||
* @property isControllableArrowVisible 地形趋势箭头图层
|
||||
* @property useCatmullRom 是否启用 CatmullRom
|
||||
* @property gridType 栅格类型
|
||||
*/
|
||||
data class TerrainUiState(
|
||||
val isTriangleVisible: Boolean,
|
||||
val isGridVisible: Boolean,
|
||||
val isContourVisible: Boolean,
|
||||
val isContourLabelingVisible: Boolean,
|
||||
val isGridHeightDiffVisible: Boolean,
|
||||
val isControllableArrowVisible: Boolean,
|
||||
val useCatmullRom: Boolean,
|
||||
val gridType: GridType
|
||||
) {
|
||||
companion object {
|
||||
val Empty = TerrainUiState(
|
||||
isTriangleVisible = true,
|
||||
isGridVisible = true,
|
||||
isContourVisible = true,
|
||||
isContourLabelingVisible = true,
|
||||
isGridHeightDiffVisible = true,
|
||||
isControllableArrowVisible = true,
|
||||
useCatmullRom = true,
|
||||
gridType = GridType.Origin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 地形图 UI 控制器
|
||||
*
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/27
|
||||
*/
|
||||
interface TerrainUiController {
|
||||
/**
|
||||
* 地形图 UI 状态
|
||||
*/
|
||||
val terrainUiState: StateFlow<TerrainUiState>
|
||||
/**
|
||||
* 等高线颜色
|
||||
*/
|
||||
val colors: StateFlow<List<String>>
|
||||
|
||||
fun setTriangleVisible(visible: Boolean)
|
||||
fun setGridVisible(visible: Boolean)
|
||||
fun setContourVisible(visible: Boolean)
|
||||
fun setContourLabelingVisible(visible: Boolean)
|
||||
fun setGridHeightDiffVisible(visible: Boolean)
|
||||
fun setControllableArrowVisible(visible: Boolean)
|
||||
fun enabledCatmullRom(enabled: Boolean)
|
||||
|
||||
fun updateTriangle(triangles: List<Triangle>)
|
||||
fun updateGrid(gridModel: GridModel)
|
||||
fun updateContour(contoursLines: List<List<ShapeContour>>, useCatmullRom: Boolean)
|
||||
fun updateGridHeightDiff()
|
||||
fun updateControllableArrow()
|
||||
fun updateGridType(type: GridType)
|
||||
}
|
||||
|
||||
interface TerrainPalette {
|
||||
fun getColor(index: Int): String
|
||||
}
|
||||
|
||||
class ContourPalette() : TerrainPalette {
|
||||
override fun getColor(index: Int): String {
|
||||
return CONTOUR_COLORS[index]
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CONTOUR_COLORS = arrayOf(
|
||||
"#CC0000", "#FF0000", "#FF3333", "#FF6666", "#FF9999", "#FFCCCC",
|
||||
"#8CED8C", "#B3D9FF", "#80BFFF", "#4D94FF", "#2673FF", "#004CFF", "#1517EC"
|
||||
).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
class DraggableMarker(
|
||||
private val context: Context,
|
||||
private val mapView: MapView,
|
||||
private val onPositionChange: (Point) -> Unit,
|
||||
private val setHeight: DraggableMarker.(PointAnnotation) -> Unit
|
||||
) {
|
||||
private var marker: PointAnnotation? = null
|
||||
private var annotationManager: PointAnnotationManager? = null
|
||||
private fun drawableToBitmap(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun addMarker(point: Point) {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
// 从资源加载图标
|
||||
val drawable = AppCompatResources.getDrawable(mapView.context, R.drawable.ic_marker_red)
|
||||
|
||||
style.addImage("marker-icon", drawableToBitmap(drawable!!))
|
||||
|
||||
// 创建标记管理器
|
||||
annotationManager = mapView.annotations.createPointAnnotationManager()
|
||||
|
||||
// 创建可拖动标记
|
||||
marker = annotationManager?.create(
|
||||
PointAnnotationOptions()
|
||||
.withPoint(point)
|
||||
.withIconImage("marker-icon")
|
||||
.withIconAnchor(IconAnchor.BOTTOM) // 图标底部对准位置
|
||||
.withDraggable(true) // 启用拖动
|
||||
.withTextField("") // 设置坐标文本
|
||||
.withTextColor("#FFFFFF") // 白色文字
|
||||
.withTextHaloColor("#000000") // 黑色文字背景
|
||||
.withTextHaloWidth(1.0)
|
||||
.withTextSize(12.0)
|
||||
.withTextAnchor(TextAnchor.TOP) // 文本在标记上方
|
||||
)
|
||||
|
||||
// 设置拖动监听
|
||||
annotationManager?.addDragListener(
|
||||
object : OnPointAnnotationDragListener {
|
||||
override fun onAnnotationDrag(annotation: Annotation<*>) {
|
||||
//marker?.point?.let { onPositionChange(it) }
|
||||
if (annotation is PointAnnotation) {
|
||||
setHeight(this@DraggableMarker, annotation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnotationDragFinished(annotation: Annotation<*>) {
|
||||
if (annotation is PointAnnotation) {
|
||||
onPositionChange(annotation.point)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnotationDragStarted(annotation: Annotation<*>) {
|
||||
//bringMarkerToTop()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMarkerText(annotation: PointAnnotation) {
|
||||
annotation.textField = getCoordinateText(annotation.point)
|
||||
annotationManager?.update(annotation)
|
||||
}
|
||||
|
||||
fun updateMarkerText(annotation: PointAnnotation, text: String) {
|
||||
annotation.textField = text
|
||||
annotationManager?.update(annotation)
|
||||
}
|
||||
|
||||
private fun getCoordinateText(point: Point): String {
|
||||
return String.format("(%.4f, %.4f)", point.longitude(), point.latitude())
|
||||
}
|
||||
|
||||
private fun bringMarkerToTop() {
|
||||
val currentPosition = getMarkerPosition()
|
||||
if (currentPosition != null) {
|
||||
// 移除并重新添加标记,确保在最上层
|
||||
removeMarker()
|
||||
addMarker(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMarkerPosition(): Point? {
|
||||
return marker?.point
|
||||
}
|
||||
|
||||
fun removeMarker() {
|
||||
marker?.let {
|
||||
annotationManager?.let {
|
||||
mapView.annotations.removeAnnotationManager(annotationManager = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TerrainUiControllerImpl(
|
||||
private val context: Context,
|
||||
private val mapView: MapView,
|
||||
private val scope: CoroutineScope,
|
||||
private val terrainManager: TerrainManager,
|
||||
) : TerrainUiController {
|
||||
|
||||
private val _terrainUiState = MutableStateFlow(TerrainUiState.Empty)
|
||||
override val terrainUiState: StateFlow<TerrainUiState> = _terrainUiState.asStateFlow()
|
||||
private val _colors = MutableStateFlow(ContourPalette.CONTOUR_COLORS)
|
||||
override val colors: StateFlow<List<String>> = _colors.asStateFlow()
|
||||
private val markerPosition = MutableStateFlow<Vector2D>(Vector2D.ZERO)
|
||||
private val arrowHead = MutableStateFlow(emptyList<Vector2D>())
|
||||
private val arrowCenter = MutableStateFlow(Vector2D(0.0, 0.0))
|
||||
private val arrowEnd = MutableStateFlow(Vector2D(0.0, 1.0))
|
||||
private val gridPalette = SimplePalette(0.0..100.0)
|
||||
|
||||
private var listener: OnMoveListener? = null
|
||||
|
||||
private val draggableMarker = DraggableMarker(
|
||||
context, mapView,
|
||||
onPositionChange = {
|
||||
Log.d(TAG, "draggableMarker: ${it.longitude()}, ${it.latitude()}")
|
||||
markerPosition.value = it.toVector2D()
|
||||
},
|
||||
setHeight = { annotation ->
|
||||
scope.launch {
|
||||
val heightAt = terrainManager.terrainData.value.designGrid?.getHeightAt(annotation.point.toVector2D())
|
||||
heightAt?.let {
|
||||
this@DraggableMarker.updateMarkerText(annotation, "%.2f m".format(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
init {
|
||||
setupControllableArrow()
|
||||
setupOnMoveListener()
|
||||
terrainManager.terrainData.onEach {
|
||||
it.slopeDirection
|
||||
it.slopePercentage
|
||||
|
||||
}.launchIn(scope)
|
||||
terrainManager.terrainData.onEach {
|
||||
it.gridModel
|
||||
}.launchIn(scope)
|
||||
combine(
|
||||
terrainManager.terrainData.map { it.designGrid }.distinctUntilChanged(),
|
||||
markerPosition
|
||||
) { designGrid, markerPosition ->
|
||||
// [0][2][4][6]
|
||||
// cellSize = 2
|
||||
//
|
||||
designGrid?.let { designGrid ->
|
||||
val heightAt = designGrid.getHeightAt(markerPosition)
|
||||
terrainManager.updateTerrainData { it.copy(currentHeight = heightAt) }
|
||||
}
|
||||
}.launchIn(scope)
|
||||
combine(
|
||||
terrainManager.terrainData.map { data ->
|
||||
data.gridModel?.let { origin ->
|
||||
data.designGrid?.let { design ->
|
||||
origin to design
|
||||
}
|
||||
}
|
||||
}.filterNotNull().distinctUntilChanged(),
|
||||
terrainUiState.map { it.isGridVisible to it.gridType }.distinctUntilChanged()
|
||||
) { a, b ->
|
||||
val (origin, design) = a
|
||||
val (isGridVisible, gridType) = b
|
||||
if (isGridVisible) {
|
||||
when (gridType) {
|
||||
GridType.Origin -> updateGrid(origin)
|
||||
GridType.Design -> updateGrid(design)
|
||||
}
|
||||
} else {
|
||||
removeLayer(GRID_LAYER_ID, GRID_SOURCE_ID)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
combine(
|
||||
terrainManager.contoursLines,
|
||||
terrainUiState.map {
|
||||
it.isContourVisible to it.useCatmullRom
|
||||
}.distinctUntilChanged()
|
||||
) { contoursLines, pair ->
|
||||
val (isContourVisible, useCatmullRom) = pair
|
||||
if (isContourVisible) {
|
||||
updateContour(contoursLines, useCatmullRom)
|
||||
} else {
|
||||
removeLayer(layerId = CONTOUR_LAYER_ID, sourceId = CONTOUR_SOURCE_ID)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
terrainManager.terrainData.map { it.heightRange }.distinctUntilChanged().onEach {
|
||||
gridPalette.setRange(it)
|
||||
}.launchIn(scope)
|
||||
combine(
|
||||
terrainManager.terrainData.map { it.triangles }.distinctUntilChanged(),
|
||||
terrainUiState.map { it.isTriangleVisible }.distinctUntilChanged()
|
||||
) { triangles, isTriangleVisible ->
|
||||
if (isTriangleVisible) {
|
||||
updateTriangle(triangles)
|
||||
} else {
|
||||
removeLayer(layerId = TRIANGLE_LAYER_ID, sourceId = TRIANGLE_SOURCE_ID)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
terrainManager.points.filter { it.isNotEmpty() }.take(1).onEach {
|
||||
delay(3000)
|
||||
draggableMarker.addMarker(it.first().toMapboxPoint())
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
object BitmapUtils {
|
||||
/**
|
||||
* 从资源获取 Bitmap
|
||||
*/
|
||||
fun getBitmapFromResource(resources: Resources, @DrawableRes resId: Int): Bitmap {
|
||||
return BitmapFactory.decodeResource(resources, resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整 Bitmap 大小
|
||||
*/
|
||||
fun resizeBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap {
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCustomIcons(style: Style) {
|
||||
// 方法1:从资源加载
|
||||
val resourceIcon = BitmapUtils.getBitmapFromResource(mapView.context.resources, R.drawable.ic_marker_red)
|
||||
style.addImage("custom-marker", resourceIcon)
|
||||
}
|
||||
|
||||
fun setupControllableArrow() {
|
||||
combine(
|
||||
arrowCenter,
|
||||
arrowEnd,
|
||||
terrainManager.terrainData.map { it.gridModel }.filterNotNull().distinctUntilChanged(),
|
||||
terrainUiState.map { it.isControllableArrowVisible }.distinctUntilChanged()
|
||||
) { center, arrow, gridModel, isControllableArrowVisible ->
|
||||
if (isControllableArrowVisible) {
|
||||
displayControllableArrow(gridModel, getSlopeDirection(arrow, center))
|
||||
}
|
||||
}.launchIn(scope)
|
||||
val isControllableArrowVisibleFlow = terrainUiState.map { it.isControllableArrowVisible }.distinctUntilChanged()
|
||||
terrainManager.terrainData.map {
|
||||
it.gridModel?.let { gridModel ->
|
||||
gridModel to it.slopeDirection
|
||||
}
|
||||
}.filterNotNull().distinctUntilChanged().combine(isControllableArrowVisibleFlow) { pair, isControllableArrowVisible ->
|
||||
val (gridModel, slopeDirection) = pair
|
||||
if (isControllableArrowVisible) {
|
||||
displayControllableArrow(
|
||||
gridModel = gridModel,
|
||||
slopeDirection = slopeDirection
|
||||
)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun removeOnMoveListener() {
|
||||
listener?.let(mapView.mapboxMap::removeOnMoveListener)
|
||||
listener = null
|
||||
}
|
||||
|
||||
fun setupOnMoveListener() {
|
||||
removeOnMoveListener()
|
||||
listener = object : OnMoveListener {
|
||||
private var beginning: Boolean = false
|
||||
private var isArrowDragging: Boolean = false
|
||||
private var isMarkerDragging: Boolean = false
|
||||
private val tolerance = 1.0
|
||||
private fun getCoordinate(focalPoint: PointF): Point {
|
||||
return mapView.mapboxMap.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||
}
|
||||
|
||||
override fun onMove(detector: MoveGestureDetector): Boolean {
|
||||
val focalPoint = detector.focalPoint
|
||||
val point = mapView.mapboxMap
|
||||
.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||
.toVector2D()
|
||||
|
||||
val isPointInPolygon = RayCastingAlgorithm.isPointInPolygon(
|
||||
point = point,
|
||||
polygon = arrowHead.value
|
||||
)
|
||||
if (isPointInPolygon) {
|
||||
isArrowDragging = true
|
||||
}
|
||||
if (isArrowDragging) {
|
||||
arrowEnd.value = point
|
||||
}
|
||||
|
||||
if (point.distanceTo(markerPosition.value) < tolerance) {
|
||||
isMarkerDragging = true
|
||||
}
|
||||
if (isMarkerDragging) {
|
||||
markerPosition.value = point
|
||||
}
|
||||
|
||||
return isArrowDragging || isMarkerDragging
|
||||
}
|
||||
|
||||
override fun onMoveBegin(detector: MoveGestureDetector) {
|
||||
Log.d(TAG, "onMoveBegin: $detector")
|
||||
beginning = true
|
||||
}
|
||||
|
||||
override fun onMoveEnd(detector: MoveGestureDetector) {
|
||||
Log.d(TAG, "onMoveEnd: $detector")
|
||||
|
||||
if (beginning && isArrowDragging) {
|
||||
val arrow = getCoordinate(detector.focalPoint).toVector2D()
|
||||
arrowEnd.value = arrow
|
||||
val center = arrowCenter.value
|
||||
terrainManager.updateTerrainData { it.copy(slopeDirection = getSlopeDirection(arrow, center)) }
|
||||
}
|
||||
|
||||
if (beginning && isMarkerDragging) {
|
||||
val point = getCoordinate(detector.focalPoint).toVector2D()
|
||||
markerPosition.value = point
|
||||
}
|
||||
|
||||
beginning = false
|
||||
isArrowDragging = false
|
||||
isMarkerDragging = false
|
||||
}
|
||||
}.also(mapView.mapboxMap::addOnMoveListener)
|
||||
}
|
||||
|
||||
private fun displayControllableArrow(gridModel: GridModel, slopeDirection: Angle) {
|
||||
val arrowData = calculateArrowData(
|
||||
grid = gridModel,
|
||||
angle = slopeDirection,
|
||||
)
|
||||
arrowHead.value = arrowData.headRing
|
||||
mapView.displayControllableArrow(
|
||||
sourceId = CONTROLLABLE_ARROW_SOURCE_ID,
|
||||
layerId = CONTROLLABLE_ARROW_LAYER_ID,
|
||||
arrowData = arrowData,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSlopeDirection(
|
||||
arrow: Vector2D,
|
||||
center: Vector2D
|
||||
): Angle {
|
||||
val direction = (arrow - center)
|
||||
val atan2 = Angle.atan2(direction.x, direction.y, Vector2D.UP)
|
||||
val angle = atan2.normalized
|
||||
return angle
|
||||
}
|
||||
|
||||
private fun removeLayer(
|
||||
layerId: String,
|
||||
sourceId: String
|
||||
) {
|
||||
mapView.mapboxMap.removeStyleLayer(layerId)
|
||||
mapView.mapboxMap.removeStyleSource(sourceId)
|
||||
}
|
||||
|
||||
override fun setTriangleVisible(visible: Boolean) {
|
||||
_terrainUiState.update { it.copy(isTriangleVisible = visible) }
|
||||
}
|
||||
|
||||
override fun setGridVisible(visible: Boolean) {
|
||||
_terrainUiState.update { it.copy(isGridVisible = visible) }
|
||||
}
|
||||
|
||||
override fun setContourVisible(visible: Boolean) {
|
||||
_terrainUiState.update { it.copy(isContourVisible = visible) }
|
||||
}
|
||||
|
||||
private fun setupFillLayer(
|
||||
style: Style,
|
||||
sourceId: String,
|
||||
layerId: String,
|
||||
features: List<Feature>
|
||||
) {
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
|
||||
val source = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(features))
|
||||
}
|
||||
style.addSource(source)
|
||||
|
||||
val layer = fillLayer(layerId, sourceId) {
|
||||
fillColor(Expression.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||
fillOpacity(1.0)
|
||||
fillAntialias(true)
|
||||
}
|
||||
style.addLayer(layer)
|
||||
}
|
||||
|
||||
private fun contoursToPolygonFeatures(contours: List<ShapeContour>, color: String, useCatmullRom: Boolean): Feature? {
|
||||
val lists = contours.drop(0).filter { it.segments.isNotEmpty() }.map { contour ->
|
||||
val start = contour.segments[0].start
|
||||
listOf(start) + contour.segments.map { it.end }
|
||||
}.map {
|
||||
if (!useCatmullRom) return@map it
|
||||
val cmr = CatmullRomChain2(it, 1.0, loop = true)
|
||||
val contour = ShapeContour.fromPoints(cmr.positions(200), true)
|
||||
val start = contour.segments[0].start
|
||||
listOf(start) + contour.segments.map { it.end }
|
||||
}.map { points -> points.map { it.toMapboxPoint() } }
|
||||
|
||||
if (lists.isEmpty()) {
|
||||
Log.w(TAG, "contoursToPolygonFeatures: 没有有效的轮廓数据")
|
||||
return null
|
||||
}
|
||||
|
||||
val polygon = Polygon.fromLngLats(lists)
|
||||
return Feature.fromGeometry(polygon).apply {
|
||||
addStringProperty("color", color)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContourLabelingVisible(visible: Boolean) {
|
||||
context.toast("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setGridHeightDiffVisible(visible: Boolean) {
|
||||
context.toast("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setControllableArrowVisible(visible: Boolean) {
|
||||
if (visible != terrainUiState.value.isControllableArrowVisible) {
|
||||
_terrainUiState.update { it.copy(isControllableArrowVisible = visible) }
|
||||
if (visible) {
|
||||
with(terrainManager.terrainData.value) {
|
||||
gridModel?.let { gridModel ->
|
||||
displayControllableArrow(
|
||||
gridModel = gridModel,
|
||||
slopeDirection = slopeDirection
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mapView.mapboxMap.removeStyleLayer("$CONTROLLABLE_ARROW_LAYER_ID-head")
|
||||
removeLayer(
|
||||
layerId = CONTROLLABLE_ARROW_LAYER_ID,
|
||||
sourceId = CONTROLLABLE_ARROW_SOURCE_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enabledCatmullRom(enabled: Boolean) {
|
||||
_terrainUiState.update {
|
||||
it.copy(useCatmullRom = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTriangle(triangles: List<Triangle>) {
|
||||
mapView.displayTriangle(
|
||||
triangles = triangles,
|
||||
sourceId = TRIANGLE_SOURCE_ID,
|
||||
layerId = TRIANGLE_LAYER_ID
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateGrid(gridModel: GridModel) {
|
||||
mapView.displayGridModel(
|
||||
grid = gridModel,
|
||||
sourceId = GRID_SOURCE_ID,
|
||||
layerId = GRID_LAYER_ID,
|
||||
palette = gridPalette::palette
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateContour(contoursLines: List<List<ShapeContour>>, useCatmullRom: Boolean) {
|
||||
val features = contoursLines.mapIndexed { index, contours ->
|
||||
contoursToPolygonFeatures(
|
||||
contours = contours,
|
||||
color = colors.value[index],
|
||||
useCatmullRom = useCatmullRom
|
||||
)
|
||||
}
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
setupFillLayer(
|
||||
style = style,
|
||||
sourceId = CONTOUR_SOURCE_ID,
|
||||
layerId = CONTOUR_LAYER_ID,
|
||||
features = features.filterNotNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateGridHeightDiff() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateControllableArrow() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateGridType(type: GridType) {
|
||||
_terrainUiState.update { it.copy(gridType = type) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TRIANGLE_LAYER_ID = "triangle_layer_id"
|
||||
const val GRID_LAYER_ID = "grid_layer_id"
|
||||
const val CONTOUR_LAYER_ID = "contour_layer_id"
|
||||
const val CONTOUR_LABELING_LAYER_ID = "contour_labeling_layer_id"
|
||||
const val GRID_HEIGHT_DIFF_LAYER_ID = "grid_height_diff_layer_id"
|
||||
const val CONTROLLABLE_ARROW_LAYER_ID = "controllable_arrow_layer_id"
|
||||
|
||||
const val TRIANGLE_SOURCE_ID = "triangle_source_id"
|
||||
const val GRID_SOURCE_ID = "grid_source_id"
|
||||
const val CONTOUR_SOURCE_ID = "contour_source_id"
|
||||
const val CONTOUR_LABELING_SOURCE_ID = "contour_labeling_source_id"
|
||||
const val GRID_HEIGHT_DIFF_SOURCE_ID = "grid_height_diff_source_id"
|
||||
const val CONTROLLABLE_ARROW_SOURCE_ID = "controllable_arrow_source_id"
|
||||
}
|
||||
}
|
||||
54
android/src/main/java/com/icegps/geotools/TriangleDisplay.kt
Normal file
54
android/src/main/java/com/icegps/geotools/TriangleDisplay.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.geotools.ktx.toMapboxPoint
|
||||
import com.icegps.triangulation.Triangle
|
||||
import com.mapbox.geojson.Feature
|
||||
import com.mapbox.geojson.FeatureCollection
|
||||
import com.mapbox.geojson.LineString
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.extension.style.layers.addLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.lineLayer
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
|
||||
import com.mapbox.maps.extension.style.sources.addSource
|
||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/28
|
||||
*/
|
||||
fun MapView.displayTriangle(
|
||||
triangles: List<Triangle>,
|
||||
sourceId: String,
|
||||
layerId: String
|
||||
) {
|
||||
val features = triangles.map {
|
||||
listOf(it.x1, it.x2, it.x3)
|
||||
}.map {
|
||||
fromPoints(points = it, closed = true)
|
||||
}.map {
|
||||
it.map { line ->
|
||||
val lineA = line.a.toMapboxPoint()
|
||||
val lineB = line.b.toMapboxPoint()
|
||||
LineString.fromLngLats(listOf(lineA, lineB))
|
||||
}.map {
|
||||
Feature.fromGeometry(it)
|
||||
}
|
||||
}.flatten()
|
||||
mapboxMap.getStyle { style ->
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
val source = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(features))
|
||||
}
|
||||
style.addSource(source)
|
||||
|
||||
val layer = lineLayer(layerId, sourceId) {
|
||||
lineWidth(1.5)
|
||||
lineJoin(LineJoin.ROUND)
|
||||
lineOpacity(1.0)
|
||||
lineColor("#ff0000")
|
||||
}
|
||||
|
||||
style.addLayer(layer)
|
||||
}
|
||||
}
|
||||
17
android/src/main/java/com/icegps/geotools/ktx/GeoPoint.kt
Normal file
17
android/src/main/java/com/icegps/geotools/ktx/GeoPoint.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import com.icegps.shared.model.GeodeticCoordinate
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/27
|
||||
*/
|
||||
fun GeodeticCoordinate.niceStr(): String {
|
||||
return "[$longitude, $latitude, $altitude]".format(this)
|
||||
}
|
||||
|
||||
fun List<GeodeticCoordinate>.niceStr(): String {
|
||||
return joinToString(", ", "[", "]") {
|
||||
it.niceStr()
|
||||
}
|
||||
}
|
||||
49
android/src/main/java/com/icegps/geotools/ktx/GridModel.kt
Normal file
49
android/src/main/java/com/icegps/geotools/ktx/GridModel.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import com.icegps.geotools.GridModel
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/27
|
||||
*/
|
||||
fun GridModel.volume(targetHeight: Double): Double {
|
||||
val cellArea = cellSize * cellSize
|
||||
var volume = 0.0
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
getValue(r, c) ?: continue
|
||||
volume += cellArea * targetHeight
|
||||
}
|
||||
}
|
||||
return volume
|
||||
}
|
||||
|
||||
fun GridModel.averageHeight(): Double {
|
||||
return cells.filterNotNull().average()
|
||||
}
|
||||
|
||||
fun GridModel.optimalHeight(): Double {
|
||||
val cellArea = cellSize * cellSize
|
||||
var volume = 0.0
|
||||
var count = 0
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
val height = getValue(r, c) ?: continue
|
||||
volume += height * cellArea
|
||||
count++
|
||||
}
|
||||
}
|
||||
return volume / count / cellArea
|
||||
}
|
||||
|
||||
fun GridModel.volumeSum(): Double {
|
||||
val cellArea = cellSize * cellSize
|
||||
var volume = 0.0
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
val height = getValue(r, c) ?: continue
|
||||
volume += cellArea * height
|
||||
}
|
||||
}
|
||||
return volume
|
||||
}
|
||||
15
android/src/main/java/com/icegps/geotools/ktx/Point.kt
Normal file
15
android/src/main/java/com/icegps/geotools/ktx/Point.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.mapbox.geojson.Point
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/28
|
||||
*/
|
||||
fun Point.toVector2D(): Vector2D {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
val enu = geoHelper.wgs84ToENU(lon = longitude(), lat = latitude(), hgt = 0.0)
|
||||
return Vector2D(enu.x, enu.y)
|
||||
}
|
||||
9
android/src/main/res/drawable/ic_marker_red.xml
Normal file
9
android/src/main/res/drawable/ic_marker_red.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512,85.3c-164.9,0 -298.7,133.7 -298.7,298.7 0,164.9 298.7,554.7 298.7,554.7s298.7,-389.7 298.7,-554.7c0,-164.9 -133.7,-298.7 -298.7,-298.7zM512,533.3a149.3,149.3 0,1 1,0 -298.7,149.3 149.3,0 0,1 0,298.7z"
|
||||
android:fillColor="#FF3D00"/>
|
||||
</vector>
|
||||
@@ -7,11 +7,78 @@
|
||||
android:orientation="vertical"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="3" />
|
||||
android:layout_weight="3">
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#4D000000"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_slope_percentage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="坡度:0.00°" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_area"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="包围面积:114514.0 m²" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_current_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="当前高度:114514.0 m" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_target_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="目标高度:114514.0 m" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/target_height_button"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_optimal_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="最佳高度:114514.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cut_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="挖土方量:114514.0 m³" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_fill_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="填土方量:114514.0 m³" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
@@ -172,6 +239,11 @@
|
||||
android:valueTo="100" />
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/target_height"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch_design_surface"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -185,6 +257,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:switchPadding="16dp"
|
||||
android:text="显示计算的坡面" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch_catmull_rom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:switchPadding="16dp"
|
||||
android:text="使用 CatmullRom 创建平滑曲线" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
@@ -163,6 +163,12 @@
|
||||
android:valueTo="100" />
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/target_height"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch_design_surface"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -176,11 +182,85 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:switchPadding="16dp"
|
||||
android:text="显示计算的坡面" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch_catmull_rom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:switchPadding="16dp"
|
||||
android:text="使用 CatmullRom 创建平滑曲线" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3" />
|
||||
android:layout_weight="3">
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_slope_percentage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="坡度:0.00°" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_area"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="包围面积:114514.0 m²" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_current_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="当前高度:114514.0 m" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_target_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="目标高度:114514.0 m" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/target_height_button"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_optimal_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="最佳高度:114514.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cut_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="挖土方量:114514.0 m³" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_fill_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="填土方量:114514.0 m³" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
304
android/src/main/res/layout/activity_map.xml
Normal file
304
android/src/main/res/layout/activity_map.xml
Normal file
@@ -0,0 +1,304 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".MapActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="5">
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#4D000000"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_slope_percentage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="坡度:0.00°" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_area"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="包围面积:114514.0 m²" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_current_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="当前高度:114514.0 m" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_target_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="目标高度:114514.0 m"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_optimal_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="最佳高度:114514.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cut_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="挖土方量:114514.0 m³" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_fill_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
tools:text="填土方量:114514.0 m³" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="2">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="三角网图层" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_triangle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="栅格图层" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_grid_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="等高线图层" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_contour_line"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="等高线标注图层" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_contour_line_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="高差数字图层" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_grid_height_diff"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="地形趋势箭头图层" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_controllable_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="栅格大小" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_cell_size"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:value="1"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="100" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="坡向" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_slope_direction"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:value="0"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="360" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="坡度" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_slope_percentage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:value="0"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="200" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="目标高度" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_target_height"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:value="0"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="100" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="高度范围" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/slider_height_range"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:valueFrom="-100"
|
||||
android:valueTo="100" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
62
android/src/test/java/com/icegps/geotools/StepTest.kt
Normal file
62
android/src/test/java/com/icegps/geotools/StepTest.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/27
|
||||
*/
|
||||
class StepTest {
|
||||
@Test
|
||||
fun testStep() {
|
||||
val contourSize = 6
|
||||
val heightRange = 30.0..80.0
|
||||
val step = (heightRange.endInclusive - heightRange.start) / contourSize
|
||||
val zip = (0..contourSize).map { index ->
|
||||
heightRange.start + index * step
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
zip.forEach {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStepWithTargetHeight() {
|
||||
val targetHeight = 50.0
|
||||
val heightRange = 30.0..80.0
|
||||
val targetHeightRange = (targetHeight - 1..targetHeight + 1)
|
||||
val frontHalfHeight = targetHeightRange.start - heightRange.start
|
||||
val backHalfHeight = heightRange.endInclusive - targetHeightRange.endInclusive
|
||||
val frontHalfStep = frontHalfHeight / 6
|
||||
val backHalfStep = backHalfHeight / 6
|
||||
val frontHalfZip = (0..6).map { index ->
|
||||
heightRange.start + index * frontHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val backHalfZip = (0..6).map { index ->
|
||||
targetHeightRange.endInclusive + index * backHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
|
||||
(frontHalfZip + listOf(targetHeightRange) + backHalfZip).forEach {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun getContours(targetHeight: Double) {
|
||||
val targetHeightRange = (targetHeight - 1..targetHeight + 1)
|
||||
val heightRange = 30.0..80.0
|
||||
val frontHalfHeight = targetHeightRange.start - heightRange.start
|
||||
val backHalfHeight = heightRange.endInclusive - targetHeightRange.endInclusive
|
||||
val frontHalfStep = frontHalfHeight / 6
|
||||
val backHalfStep = backHalfHeight / 6
|
||||
val frontHalfZip = (0..6).map { index ->
|
||||
heightRange.start + index * frontHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val backHalfZip = (0..6).map { index ->
|
||||
targetHeightRange.endInclusive + index * backHalfStep
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
|
||||
(frontHalfZip + listOf(targetHeightRange) + backHalfZip).forEach {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.geotools.ktx.area
|
||||
import com.icegps.geotools.ktx.averageHeight
|
||||
import com.icegps.geotools.ktx.niceStr
|
||||
import com.icegps.geotools.ktx.optimalHeight
|
||||
import com.icegps.geotools.ktx.volume
|
||||
import com.icegps.geotools.ktx.volumeSum
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.triangulation.delaunayTriangulation
|
||||
import org.junit.Test
|
||||
@@ -14,34 +20,63 @@ import kotlin.math.max
|
||||
class TriangulationToGridTest {
|
||||
@Test
|
||||
fun testTriangulationToGrid() {
|
||||
val points = listOf(
|
||||
/*val points = listOf(
|
||||
Vector3D(-10.0, 10.0, 0.0),
|
||||
Vector3D(10.0, 10.0, 10.0),
|
||||
Vector3D(-10.0, -10.0, 20.0),
|
||||
Vector3D(10.0, -10.0, 30.0),
|
||||
)*/
|
||||
val pathGenerator = PathGenerator(
|
||||
centerLon = 114.24357285,
|
||||
centerLat = 22.70470464,
|
||||
widthMeters = 50.0,
|
||||
heightMeters = 100.0,
|
||||
minHeight = 100.0,
|
||||
maxHeight = 102.0,
|
||||
numZigzagLines = 10,
|
||||
pointSpacingMeters = 5.0,
|
||||
)
|
||||
points.map {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
val points = pathGenerator.generatePath().map {
|
||||
geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude).copy(z = it.altitude)
|
||||
}.map { Vector3D(it.x, it.y, it.z) }
|
||||
if (false) points.map {
|
||||
it / 8
|
||||
}.niceStr().let(::println)
|
||||
val area = points.area
|
||||
val cellSize = max(area.x + area.width, area.y + area.height) / 10
|
||||
println("area: ${area}")
|
||||
val cellSize = max(area.x + area.width, area.y + area.height) / 8
|
||||
val triangulation = points.delaunayTriangulation()
|
||||
val triangles = triangulation.triangles()
|
||||
val grid = triangulationToGrid(
|
||||
delaunator = triangulation,
|
||||
area = area,
|
||||
triangles = triangles,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
grid.string().let(::println)
|
||||
val slopeResult = SlopeCalculator.calculateSlope(
|
||||
/*val slopeResult = SlopeCalculator.calculateSlope(
|
||||
grid = grid,
|
||||
slopeDirection = 0.0,
|
||||
slopePercentage = 100.0,
|
||||
slopePercentage = 0.0,
|
||||
baseHeightOffset = 0.0
|
||||
)*/
|
||||
val targetHeight = 100.0
|
||||
val slopeResult = SlopeCalculator.calculateSlopeByTargetHeight(
|
||||
grid = grid,
|
||||
slopeDirection = 0.0,
|
||||
slopePercentage = 0.0,
|
||||
targetHeight = targetHeight
|
||||
)
|
||||
slopeResult.designSurface.string().let(::println)
|
||||
println("原来的 Volume: ${grid.volumeSum()}")
|
||||
println("做坡的 Volume: ${slopeResult.designSurface.volumeSum()}")
|
||||
println("目标高度:${targetHeight}")
|
||||
println("原来的土方量: ${grid.volumeSum()}")
|
||||
println("目标土方量: ${slopeResult.designSurface.volumeSum()}")
|
||||
println(slopeResult.earthworkResult)
|
||||
|
||||
println("最佳高度:${grid.optimalHeight()}")
|
||||
println("栅格平均高度:${grid.averageHeight()}")
|
||||
|
||||
println("目标高度是 $targetHeight 的挖填土方量:${grid.volume(targetHeight)}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,17 +90,11 @@ fun GridModel.string() = buildString {
|
||||
}
|
||||
}
|
||||
|
||||
fun GridModel.volumeSum(): Double {
|
||||
var volume = 0.0
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
val height = getValue(r, c) ?: continue
|
||||
volume += height
|
||||
}
|
||||
}
|
||||
return volume
|
||||
}
|
||||
|
||||
fun Double.format(): String {
|
||||
return "%.1f".format(this)
|
||||
}
|
||||
|
||||
fun Rectangle.string(): String = buildString {
|
||||
appendLine("x = ${x.format()}, y = ${y.format()}")
|
||||
appendLine("width = ${width.format()}, height = ${height.format()}")
|
||||
}
|
||||
Reference in New Issue
Block a user