493 lines
16 KiB
Kotlin
493 lines
16 KiB
Kotlin
package com.icegps.geotools
|
|
|
|
import android.graphics.PointF
|
|
import android.util.Log
|
|
import com.icegps.geotools.ktx.toVector2D
|
|
import com.icegps.math.geometry.Angle
|
|
import com.icegps.math.geometry.Vector2D
|
|
import com.icegps.math.geometry.degrees
|
|
import com.icegps.shared.ktx.TAG
|
|
import com.mapbox.android.gestures.MoveGestureDetector
|
|
import com.mapbox.geojson.Point
|
|
import com.mapbox.maps.MapView
|
|
import com.mapbox.maps.ScreenCoordinate
|
|
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.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.combine
|
|
import kotlinx.coroutines.flow.launchIn
|
|
import kotlin.math.abs
|
|
import kotlin.math.cos
|
|
import kotlin.math.sin
|
|
|
|
/**
|
|
* @author tabidachinokaze
|
|
* @date 2025/11/26
|
|
*/
|
|
object SlopeCalculator {
|
|
fun calculateSlope(
|
|
grid: GridModel,
|
|
slopeDirection: Double,
|
|
slopePercentage: Double,
|
|
baseHeightOffset: Double = 0.0
|
|
): SlopeResult {
|
|
val centerX = (grid.minX + grid.maxX) / 2
|
|
val centerY = (grid.minY + grid.maxY) / 2
|
|
|
|
val elevations = grid.cells.filterNotNull()
|
|
val baseElevation = elevations.average() + baseHeightOffset
|
|
|
|
val basePoint: Triple<Double, Double, Double> = Triple(centerX, centerY, baseElevation)
|
|
|
|
val earthworkResult = EarthworkCalculator.calculateForSlopeDesign(
|
|
grid = grid,
|
|
basePoint = basePoint,
|
|
slope = slopePercentage,
|
|
aspect = slopeDirection
|
|
)
|
|
|
|
return SlopeResult(
|
|
slopeDirection = slopeDirection,
|
|
slopePercentage = slopePercentage,
|
|
baseHeightOffset = baseHeightOffset,
|
|
baseElevation = baseElevation,
|
|
earthworkResult = earthworkResult,
|
|
designSurface = generateSlopeDesignGrid(
|
|
grid = grid,
|
|
basePoint = basePoint,
|
|
slopePercentage = slopePercentage,
|
|
slopeDirection = slopeDirection
|
|
)
|
|
)
|
|
}
|
|
|
|
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
|
|
)
|
|
}
|
|
|
|
|
|
/**
|
|
* 生成斜坡设计面网格(用于可视化)
|
|
*/
|
|
private fun generateSlopeDesignGrid(
|
|
grid: GridModel,
|
|
basePoint: Triple<Double, Double, Double>,
|
|
slopePercentage: Double,
|
|
slopeDirection: Double
|
|
): GridModel {
|
|
val designCells = Array<Double?>(grid.rows * grid.cols) { null }
|
|
val (baseX, baseY, baseElev) = basePoint
|
|
val slopeRatio = slopePercentage / 100.0
|
|
|
|
for (r in 0 until grid.rows) {
|
|
for (c in 0 until grid.cols) {
|
|
if (grid.getValue(r, c) != null) {
|
|
val cellX = grid.minX + (c + 0.5) * (grid.maxX - grid.minX) / grid.cols
|
|
val cellY = grid.minY + (r + 0.5) * (grid.maxY - grid.minY) / grid.rows
|
|
|
|
val designElev = calculateSlopeElevation(
|
|
pointX = cellX,
|
|
pointY = cellY,
|
|
baseX = baseX,
|
|
baseY = baseY,
|
|
baseElev = baseElev,
|
|
slopeRatio = slopeRatio,
|
|
slopeDirection = slopeDirection
|
|
)
|
|
designCells[r * grid.cols + c] = designElev
|
|
}
|
|
}
|
|
}
|
|
return GridModel(
|
|
minX = grid.minX,
|
|
maxX = grid.maxX,
|
|
minY = grid.minY,
|
|
maxY = grid.maxY,
|
|
rows = grid.rows,
|
|
cols = grid.cols,
|
|
cellSize = grid.cellSize,
|
|
cells = designCells
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 斜坡高程计算
|
|
*/
|
|
fun calculateSlopeElevation(
|
|
pointX: Double,
|
|
pointY: Double,
|
|
baseX: Double,
|
|
baseY: Double,
|
|
baseElev: Double,
|
|
slopeRatio: Double,
|
|
slopeDirection: Double
|
|
): Double {
|
|
val dx = (pointX - baseX) * cos(Math.toRadians(baseY))
|
|
val dy = (pointY - baseY)
|
|
|
|
val slopeRad = (slopeDirection.degrees - 90.degrees).normalized.radians
|
|
|
|
val projection = dx * cos(slopeRad) + dy * sin(slopeRad)
|
|
val heightDiff = projection * slopeRatio
|
|
|
|
return baseElev + heightDiff
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 斜面设计
|
|
*
|
|
* @property slopeDirection 坡向 (度)
|
|
* @property slopePercentage 坡度 (%)
|
|
* @property baseHeightOffset 基准面高度偏移 (m)
|
|
* @property baseElevation 基准点高程 (m)
|
|
* @property earthworkResult 土方量结果
|
|
* @property designSurface 设计面网格(用于可视化)
|
|
*/
|
|
data class SlopeResult(
|
|
val slopeDirection: Double,
|
|
val slopePercentage: Double,
|
|
val baseHeightOffset: Double,
|
|
val baseElevation: Double,
|
|
val earthworkResult: EarthworkResult,
|
|
val designSurface: GridModel
|
|
)
|
|
|
|
object EarthworkCalculator {
|
|
/**
|
|
* @param grid 栅格网模型
|
|
* @param designElevation 设计高程
|
|
*/
|
|
fun calculateForFlatDesign(
|
|
grid: GridModel,
|
|
designElevation: Double
|
|
): EarthworkResult {
|
|
var cutVolume = 0.0
|
|
var fillVolume = 0.0
|
|
var cutArea = 0.0
|
|
var fillArea = 0.0
|
|
val cellArea = grid.cellSize * grid.cellSize
|
|
|
|
for (r in 0 until grid.rows) {
|
|
for (c in 0 until grid.cols) {
|
|
val originalElev = grid.getValue(r, c) ?: continue
|
|
|
|
val heightDiff = designElevation - originalElev
|
|
|
|
val volume = heightDiff * cellArea
|
|
|
|
if (volume > 0) {
|
|
fillVolume += volume
|
|
fillArea += cellArea
|
|
} else if (volume < 0) {
|
|
cutVolume += abs(volume)
|
|
cutArea += cellArea
|
|
}
|
|
}
|
|
}
|
|
|
|
return EarthworkResult(
|
|
cutVolume = cutVolume,
|
|
fillVolume = fillVolume,
|
|
netVolume = fillVolume - cutVolume,
|
|
cutArea = cutArea,
|
|
fillArea = fillArea,
|
|
totalArea = cutArea + fillArea
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 计算斜面设计的土方量
|
|
*/
|
|
fun calculateForSlopeDesign(
|
|
grid: GridModel,
|
|
basePoint: Triple<Double, Double, Double>,
|
|
slope: Double,
|
|
aspect: Double
|
|
): EarthworkResult {
|
|
var cutVolume = 0.0
|
|
var fillVolume = 0.0
|
|
var cutArea = 0.0
|
|
var fillArea = 0.0
|
|
val cellArea = grid.cellSize * grid.cellSize
|
|
|
|
val (baseX, baseY, baseElev) = basePoint
|
|
val slopeRatio = slope / 100.0
|
|
|
|
for (r in 0 until grid.rows) {
|
|
for (c in 0 until grid.cols) {
|
|
val originalElev = grid.getValue(r, c) ?: continue
|
|
|
|
val cellX = grid.minX + (c + 0.5) * (grid.maxX - grid.minX) / grid.cols
|
|
val cellY = grid.minY + (r + 0.5) * (grid.maxY - grid.minY) / grid.rows
|
|
|
|
val designElev = SlopeCalculator.calculateSlopeElevation(
|
|
pointX = cellX,
|
|
pointY = cellY,
|
|
baseX = baseX,
|
|
baseY = baseY,
|
|
baseElev = baseElev,
|
|
slopeRatio = slopeRatio,
|
|
slopeDirection = aspect
|
|
)
|
|
|
|
val heightElev = designElev - originalElev
|
|
val volume = heightElev * cellArea
|
|
|
|
if (volume > 0) {
|
|
fillVolume += volume
|
|
fillArea += cellArea
|
|
} else if (volume < 0) {
|
|
cutVolume += abs(volume)
|
|
cutArea += cellArea
|
|
}
|
|
}
|
|
}
|
|
|
|
return EarthworkResult(
|
|
cutVolume = cutVolume,
|
|
fillVolume = fillVolume,
|
|
netVolume = fillVolume - cutVolume,
|
|
cutArea = cutArea,
|
|
fillArea = fillArea,
|
|
totalArea = cutArea + fillArea
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 土方量计算结果
|
|
* @property cutVolume 挖方量 (m³)
|
|
* @property fillVolume 填方量 (m³)
|
|
* @property netVolume 净土方量 (m³)
|
|
* @property cutArea 挖方面积 (m²)
|
|
* @property fillArea 填方面积 (m²)
|
|
* @property totalArea 总面积 (m²)
|
|
*/
|
|
data class EarthworkResult(
|
|
val cutVolume: Double,
|
|
val fillVolume: Double,
|
|
val netVolume: Double,
|
|
val cutArea: Double,
|
|
val fillArea: Double,
|
|
val totalArea: Double
|
|
) {
|
|
override fun toString(): String {
|
|
return buildString {
|
|
appendLine("EarthworkResult")
|
|
appendLine("挖方: ${"%.1f".format(cutVolume)} m³")
|
|
appendLine("填方: ${"%.1f".format(fillVolume)} m³")
|
|
appendLine("净土方: ${"%.1f".format(netVolume)} m³")
|
|
appendLine("挖方面积: ${"%.1f".format(cutArea)} m²")
|
|
appendLine("填方面积: ${"%.1f".format(fillArea)} m²")
|
|
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(
|
|
private val mapView: MapView,
|
|
private val scope: CoroutineScope
|
|
) {
|
|
private val arrowSourceId: String = "controllable-source-id-0"
|
|
private val arrowLayerId: String = "controllable-layer-id-0"
|
|
private var listener: OnMoveListener? = null
|
|
|
|
private var gridModel = MutableStateFlow<GridModel?>(null)
|
|
private val arrowHead = MutableStateFlow(emptyList<Vector2D>())
|
|
private var arrowCenter = MutableStateFlow(Vector2D(0.0, 0.0))
|
|
private var arrowEnd = MutableStateFlow(Vector2D(0.0, 1.0))
|
|
private var _slopeDirection = MutableStateFlow(0.degrees)
|
|
val slopeDirection = _slopeDirection.asStateFlow()
|
|
private val _slopePercentage = MutableStateFlow(90.0)
|
|
val slopePercentage = _slopePercentage.asStateFlow()
|
|
private val _baseHeightOffset = MutableStateFlow(0.0)
|
|
val baseHeightOffset = _baseHeightOffset.asStateFlow()
|
|
|
|
init {
|
|
combine(
|
|
arrowCenter,
|
|
arrowEnd,
|
|
gridModel
|
|
) { center, arrow, gridModel ->
|
|
gridModel?.let { gridModel ->
|
|
// _slopeDirection.value = angle
|
|
displayControllableArrow(gridModel, getSlopeDirection(arrow, center))
|
|
}
|
|
}.launchIn(scope)
|
|
combine(
|
|
_slopeDirection,
|
|
gridModel
|
|
) { slopeDirection, gridModel ->
|
|
gridModel?.let {
|
|
displayControllableArrow(it, slopeDirection)
|
|
}
|
|
}.launchIn(scope)
|
|
}
|
|
|
|
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 displayControllableArrow(gridModel: GridModel, slopeDirection: Angle) {
|
|
val arrowData = calculateArrowData(
|
|
grid = gridModel,
|
|
angle = slopeDirection,
|
|
)
|
|
arrowHead.value = arrowData.headRing
|
|
mapView.displayControllableArrow(
|
|
sourceId = arrowSourceId,
|
|
layerId = arrowLayerId,
|
|
arrowData = arrowData,
|
|
)
|
|
}
|
|
|
|
fun removeOnMoveListener() {
|
|
listener?.let(mapView.mapboxMap::removeOnMoveListener)
|
|
listener = null
|
|
}
|
|
|
|
fun setupOnMoveListener() {
|
|
listener = object : OnMoveListener {
|
|
private var beginning: Boolean = false
|
|
private var isDragging: Boolean = false
|
|
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) {
|
|
isDragging = true
|
|
}
|
|
if (isDragging) {
|
|
arrowEnd.value = point
|
|
}
|
|
return isDragging
|
|
}
|
|
|
|
override fun onMoveBegin(detector: MoveGestureDetector) {
|
|
Log.d(TAG, "onMoveBegin: $detector")
|
|
beginning = true
|
|
}
|
|
|
|
override fun onMoveEnd(detector: MoveGestureDetector) {
|
|
Log.d(TAG, "onMoveEnd: $detector")
|
|
val point = getCoordinate(detector.focalPoint)
|
|
val arrow = point.toVector2D()
|
|
if (beginning && isDragging) {
|
|
arrowEnd.value = arrow
|
|
val center = arrowCenter.value
|
|
_slopeDirection.value = getSlopeDirection(arrow, center)
|
|
}
|
|
Log.d(
|
|
TAG,
|
|
buildString {
|
|
appendLine("onMoveEnd: ")
|
|
appendLine("${point.longitude()}, ${point.latitude()}")
|
|
}
|
|
)
|
|
isDragging = false
|
|
beginning = false
|
|
}
|
|
}.also(mapView.mapboxMap::addOnMoveListener)
|
|
}
|
|
|
|
fun updateGridModel(gridModel: GridModel) {
|
|
this.gridModel.value = gridModel
|
|
calculateArrowCenter(gridModel)
|
|
}
|
|
|
|
private fun calculateArrowCenter(gridModel: GridModel) {
|
|
val centerX = (gridModel.minX + gridModel.maxX) / 2
|
|
val centerY = (gridModel.minY + gridModel.maxY) / 2
|
|
arrowCenter.value = Vector2D(centerX, centerY)
|
|
}
|
|
|
|
fun updateSlopeDirection(angle: Angle) {
|
|
_slopeDirection.value = angle
|
|
}
|
|
|
|
fun updateSlopePercentage(value: Double) {
|
|
_slopePercentage.value = value
|
|
}
|
|
|
|
fun updateDesignHeight(value: Double) {
|
|
_baseHeightOffset.value = value
|
|
}
|
|
} |