完成了土方量计算

This commit is contained in:
2025-11-26 15:28:39 +08:00
parent 0d15c60606
commit 2525d30c80
11 changed files with 1077 additions and 29 deletions

View File

@@ -28,8 +28,11 @@ import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.openrndr.extra.shapes.splines.CatmullRomChain2 import org.openrndr.extra.shapes.splines.CatmullRomChain2
@@ -54,7 +57,7 @@ class ContoursManager(
private var contourSize: Int = 6 private var contourSize: Int = 6
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0 private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
private var cellSize: Double? = 10.0 private var cellSize: Double? = 10.0
private val simplePalette = SimplePalette( val simplePalette = SimplePalette(
range = 0.0..100.0 range = 0.0..100.0
) )
@@ -103,18 +106,21 @@ class ContoursManager(
} }
private var isGridVisible: Boolean = true private var isGridVisible: Boolean = true
private var gridModel: GridModel? = null private var _gridModel = MutableStateFlow<GridModel?>(null)
val gridModel = _gridModel.asStateFlow()
fun setGridVisible(visible: Boolean) { fun setGridVisible(visible: Boolean) {
if (visible != isGridVisible) { if (visible != isGridVisible) {
isGridVisible = visible isGridVisible = visible
if (visible) { if (visible) {
if (gridModel != null) mapView.displayGridModel( _gridModel.value?.let { gridModel ->
grid = gridModel!!, mapView.displayGridModel(
grid = gridModel,
sourceId = gridSourceId, sourceId = gridSourceId,
layerId = gridLayerId, layerId = gridLayerId,
palette = simplePalette::palette palette = simplePalette::palette
) )
}
} else { } else {
mapView.mapboxMap.getStyle { style -> mapView.mapboxMap.getStyle { style ->
try { try {
@@ -149,12 +155,15 @@ class ContoursManager(
} }
} }
private var job: Job? = null
fun refresh() { fun refresh() {
val points = points val points = points
if (points.size <= 3) { if (points.size <= 3) {
context.toast("points size ${points.size}") context.toast("points size ${points.size}")
return return
} }
job?.cancel()
scope.launch { scope.launch {
mapView.mapboxMap.getStyle { style -> mapView.mapboxMap.getStyle { style ->
val step = heightRange.endInclusive / contourSize val step = heightRange.endInclusive / contourSize
@@ -175,7 +184,7 @@ class ContoursManager(
delaunator = triangulation, delaunator = triangulation,
cellSize = cellSize, cellSize = cellSize,
) )
this@ContoursManager.gridModel = gridModel this@ContoursManager._gridModel.value = gridModel
if (isGridVisible) mapView.displayGridModel( if (isGridVisible) mapView.displayGridModel(
grid = gridModel, grid = gridModel,
sourceId = gridSourceId, sourceId = gridSourceId,
@@ -183,7 +192,7 @@ class ContoursManager(
palette = simplePalette::palette palette = simplePalette::palette
) )
} }
scope.launch(Dispatchers.Default) { job = scope.launch(Dispatchers.Default) {
val lineFeatures = mutableListOf<List<Feature>>() val lineFeatures = mutableListOf<List<Feature>>()
val features = zip.mapIndexed { index, range -> val features = zip.mapIndexed { index, range ->
async { async {
@@ -264,10 +273,10 @@ class ContoursManager(
style.addSource(source) style.addSource(source)
val layer = lineLayer(layerId, sourceId) { val layer = lineLayer(layerId, sourceId) {
lineColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色 lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
lineWidth(1.0) lineWidth(1.0)
lineCap(LineCap.Companion.ROUND) lineCap(LineCap.ROUND)
lineJoin(LineJoin.Companion.ROUND) lineJoin(LineJoin.ROUND)
lineOpacity(0.8) lineOpacity(0.8)
} }
style.addLayer(layer) style.addLayer(layer)
@@ -288,7 +297,7 @@ class ContoursManager(
style.addSource(source) style.addSource(source)
val layer = fillLayer(layerId, sourceId) { val layer = fillLayer(layerId, sourceId) {
fillColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色 fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
fillOpacity(0.5) fillOpacity(0.5)
fillAntialias(true) fillAntialias(true)
} }

View File

@@ -0,0 +1,197 @@
package com.icegps.orx
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.Vector2D
import com.icegps.orx.ktx.toMapboxPoint
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.LineString
import com.mapbox.geojson.Point
import com.mapbox.geojson.Polygon
import com.mapbox.maps.MapView
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.generated.LineLayer
import com.mapbox.maps.extension.style.layers.properties.generated.LineCap
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
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
/**
* 设置趋势箭头图层
*/
fun setupTrendLayer(
style: Style,
trendSourceId: String,
trendLayerId: String,
features: List<Feature>
) {
val trendSource = geoJsonSource(trendSourceId) {
featureCollection(FeatureCollection.fromFeatures(features))
}
try {
style.removeStyleLayer(trendLayerId)
} catch (_: Exception) {
}
try {
style.removeStyleLayer("$trendLayerId-head")
} catch (_: Exception) {
}
if (style.styleSourceExists(trendSourceId)) {
style.removeStyleSource(trendSourceId)
}
style.addSource(trendSource)
val lineLayer = LineLayer(trendLayerId, trendSourceId).apply {
lineColor(Expression.toColor(Expression.get("color")))
lineWidth(4.0)
lineCap(LineCap.ROUND)
lineJoin(LineJoin.ROUND)
}
style.addLayer(lineLayer)
val headLayer = FillLayer("$trendLayerId-head", trendSourceId).apply {
fillColor(Expression.toColor(Expression.get("color")))
}
style.addLayer(headLayer)
}
fun MapView.displayControllableArrow(
grid: GridModel,
sourceId: String = "controllable-source-id-0",
layerId: String = "controllable-layer-id-0",
arrowScale: Double = 0.4,
angle: Angle,
onHeadArrowChange: (List<Point>) -> Unit
) {
mapboxMap.getStyle { style ->
val centerX = (grid.minX + grid.maxX) / 2
val centerY = (grid.minY + grid.maxY) / 2
val regionWidth = grid.maxX - grid.minX
val regionHeight = grid.maxY - grid.minY
val arrowLength = min(regionWidth, regionHeight) * arrowScale * 1.0
val arrowDirectionRad = angle.radians
val endX = centerX + sin(arrowDirectionRad) * arrowLength
val endY = centerY + cos(arrowDirectionRad) * arrowLength
val arrowLine = LineString.fromLngLats(
listOf(
Vector2D(centerX, centerY),
Vector2D(endX, endY)
).map { it.toMapboxPoint() }
)
val arrowFeature = Feature.fromGeometry(arrowLine)
arrowFeature.addStringProperty("color", "#0000FF")
arrowFeature.addStringProperty("type", "overall-trend")
// 创建箭头头部
val headSize = arrowLength * 0.2
val leftRad = arrowDirectionRad + Math.PI * 0.8
val rightRad = arrowDirectionRad - Math.PI * 0.8
val leftX = endX + sin(leftRad) * headSize
val leftY = endY + cos(leftRad) * headSize
val rightX = endX + sin(rightRad) * headSize
val rightY = endY + cos(rightRad) * headSize
val headRing = listOf(
Vector2D(endX, endY),
Vector2D(leftX, leftY),
Vector2D(rightX, rightY),
Vector2D(endX, endY)
).map { it.toMapboxPoint() }
onHeadArrowChange(headRing)
val headPolygon = Polygon.fromLngLats(listOf(headRing))
val headFeature = Feature.fromGeometry(headPolygon)
headFeature.addStringProperty("color", "#0000FF")
headFeature.addStringProperty("type", "overall-trend")
val features = listOf(arrowFeature, headFeature)
// 设置图层
setupTrendLayer(style, sourceId, layerId, features)
}
}
fun calculateArrowData(
grid: GridModel,
angle: Angle,
arrowScale: Double = 0.4
): ArrowData {
val centerX = (grid.minX + grid.maxX) / 2
val centerY = (grid.minY + grid.maxY) / 2
val regionWidth = grid.maxX - grid.minX
val regionHeight = grid.maxY - grid.minY
val arrowLength = min(regionWidth, regionHeight) * arrowScale * 1.0
val arrowDirectionRad = angle.radians
val endX = centerX + sin(arrowDirectionRad) * arrowLength
val endY = centerY + cos(arrowDirectionRad) * arrowLength
val arrowLine = listOf(
Vector2D(centerX, centerY),
Vector2D(endX, endY)
)
// 创建箭头头部
val headSize = arrowLength * 0.2
val leftRad = arrowDirectionRad + Math.PI * 0.8
val rightRad = arrowDirectionRad - Math.PI * 0.8
val leftX = endX + sin(leftRad) * headSize
val leftY = endY + cos(leftRad) * headSize
val rightX = endX + sin(rightRad) * headSize
val rightY = endY + cos(rightRad) * headSize
val headRing = listOf(
Vector2D(endX, endY),
Vector2D(leftX, leftY),
Vector2D(rightX, rightY),
Vector2D(endX, endY)
)
return ArrowData(
arrowLine = arrowLine,
headRing = headRing
)
}
data class ArrowData(
val arrowLine: List<Vector2D>,
val headRing: List<Vector2D>
)
fun MapView.displayControllableArrow(
sourceId: String = "controllable-source-id-0",
layerId: String = "controllable-layer-id-0",
arrowData: ArrowData
) {
mapboxMap.getStyle { style ->
val (arrowLine, headRing) = arrowData
val arrowFeature = Feature.fromGeometry(LineString.fromLngLats(arrowLine.map { it.toMapboxPoint() }))
arrowFeature.addStringProperty("color", "#0000FF")
arrowFeature.addStringProperty("type", "overall-trend")
val headPolygon = Polygon.fromLngLats(listOf(headRing.map { it.toMapboxPoint() }))
val headFeature = Feature.fromGeometry(headPolygon)
headFeature.addStringProperty("color", "#0000FF")
headFeature.addStringProperty("type", "overall-trend")
val features = listOf(arrowFeature, headFeature)
// 设置图层
setupTrendLayer(style, sourceId, layerId, features)
}
}

View File

@@ -0,0 +1,144 @@
package com.icegps.orx
import android.util.Log
import com.icegps.math.geometry.Vector2D
import com.icegps.orx.ktx.toMapboxPoint
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Polygon
import com.mapbox.maps.MapView
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.generated.LineLayer
import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
/**
* @author tabidachinokaze
* @date 2025/11/26
*/
/**
* 绘制斜坡设计结果
*/
fun MapView.displaySlopeResult(
originalGrid: GridModel,
slopeResult: SlopeResult,
sourceId: String = "slope-result",
layerId: String = "slope-layer",
palette: (Double?) -> String,
showDesignHeight: Boolean
) {
val elevationList = mutableListOf<Double>()
mapboxMap.getStyle { style ->
val features = mutableListOf<Feature>()
val designGrid = slopeResult.designSurface
// 对比测试,将绘制到原来图形的左边
// val minX = originalGrid.minX * 2 - originalGrid.maxX
val minX = originalGrid.minX
val maxY = originalGrid.maxY
val cellSize = originalGrid.cellSize
for (r in 0 until originalGrid.rows) {
for (c in 0 until originalGrid.cols) {
val originalElev = originalGrid.getValue(r, c) ?: continue
val designElev = designGrid.getValue(r, c) ?: continue
elevationList.add(designElev)
// 计算填挖高度
val heightDiff = designElev - originalElev
// 计算栅格边界
val x0 = minX + c * cellSize
val y0 = maxY - r * cellSize
val x1 = x0 + cellSize
val y1 = y0 - cellSize
// 1. 创建多边形要素(背景色)
val ring = listOf(
Vector2D(x0, y0),
Vector2D(x1, y0),
Vector2D(x1, y1),
Vector2D(x0, y1),
Vector2D(x0, y0)
).map { it.toMapboxPoint() }
val poly = Polygon.fromLngLats(listOf(ring))
val feature = Feature.fromGeometry(poly)
if (showDesignHeight) {
// 显示设计高度,测试坡向是否正确,和高度是否计算正确
feature.addStringProperty("color", palette(designElev))
} else {
// 显示高差
feature.addStringProperty("color", palette(heightDiff))
}
// 显示原始高度
// feature.addStringProperty("color", palette(originalElev))
features.add(feature)
}
}
Log.d("displayGridWithDirectionArrows", "对比区域的土方量计算: ${elevationList.sum()}, 平均值:${elevationList.average()}")
// 设置图层
setupEarthworkLayer(style, sourceId, layerId, features)
}
}
/**
* 完整的土方工程图层设置 - 修正版
*/
private fun setupEarthworkLayer(
style: Style,
sourceId: String,
layerId: String,
features: List<Feature>,
) {
// 创建数据源
val source = geoJsonSource(sourceId) {
featureCollection(FeatureCollection.fromFeatures(features))
}
// 清理旧图层
try {
style.removeStyleLayer(layerId)
} catch (_: Exception) {
}
try {
style.removeStyleLayer("$layerId-arrow")
} catch (_: Exception) {
}
try {
style.removeStyleLayer("$layerId-outline")
} catch (_: Exception) {
}
try {
style.removeStyleLayer("$layerId-text")
} catch (_: Exception) {
}
if (style.styleSourceExists(sourceId)) {
style.removeStyleSource(sourceId)
}
// 添加数据源
style.addSource(source)
// 主填充图层
val fillLayer = FillLayer(layerId, sourceId).apply {
fillColor(Expression.toColor(Expression.get("color")))
fillOpacity(0.7)
}
style.addLayer(fillLayer)
// 边框图层
val outlineLayer = LineLayer("$layerId-outline", sourceId).apply {
lineColor("#333333")
lineWidth(1.0)
lineOpacity(0.5)
}
style.addLayer(outlineLayer)
}

View File

@@ -0,0 +1,438 @@
package com.icegps.orx
import android.graphics.PointF
import android.util.Log
import com.icegps.common.helper.GeoHelper
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(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
)
)
}
/**
* 生成斜坡设计面网格(用于可视化)
*/
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)}")
appendLine("填方: ${"%.1f".format(fillVolume)}")
appendLine("净土方: ${"%.1f".format(netVolume)}")
appendLine("挖方面积: ${"%.1f".format(cutArea)}")
appendLine("填方面积: ${"%.1f".format(fillArea)}")
appendLine("总面积:${"%.1f".format(totalArea)}")
}
}
}
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 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
}
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
}
}

View File

@@ -19,7 +19,14 @@ data class GridModel(
val cols: Int, val cols: Int,
val cellSize: Double, val cellSize: Double,
val cells: Array<Double?> val cells: Array<Double?>
) ) {
fun getValue(row: Int, col: Int): Double? {
if (row !in 0..<rows || col < 0 || col >= cols) {
return null
}
return cells[row * cols + col]
}
}
fun triangulationToGrid( fun triangulationToGrid(
delaunator: DelaunayTriangulation3D, delaunator: DelaunayTriangulation3D,

View File

@@ -10,12 +10,16 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.RangeSlider
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.icegps.common.helper.GeoHelper import com.icegps.common.helper.GeoHelper
import com.icegps.math.geometry.degrees
import com.icegps.orx.databinding.ActivityMainBinding import com.icegps.orx.databinding.ActivityMainBinding
import com.icegps.shared.model.GeoPoint import com.icegps.shared.model.GeoPoint
import com.mapbox.geojson.Point import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView import com.mapbox.maps.MapView
import com.mapbox.maps.plugin.gestures.addOnMapClickListener import com.mapbox.maps.plugin.gestures.addOnMapClickListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -26,6 +30,7 @@ class MainActivity : AppCompatActivity() {
ViewModelProvider(this)[MainViewModel::class.java] ViewModelProvider(this)[MainViewModel::class.java]
} }
private lateinit var contoursManager: ContoursManager private lateinit var contoursManager: ContoursManager
private lateinit var earthworkManager: EarthworkManager
init { init {
initGeoHelper() initGeoHelper()
@@ -36,6 +41,7 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge() enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
mapView = binding.mapView mapView = binding.mapView
earthworkManager = EarthworkManager(mapView, lifecycleScope)
setContentView(binding.root) setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -52,27 +58,17 @@ class MainActivity : AppCompatActivity() {
.build() .build()
) )
val points = coordinateGenerate1() val points = coordinateGenerate()
val polygonTest = PolygonTest(mapView)
polygonTest.clear()
val innerPoints = points.map { it[0] }
val outerPoints = points.map { it[1] }
if (false) polygonTest.update(
outer = outerPoints,
inner = innerPoints,
other = points.map { it[2] }
)
// divider // divider
contoursManager = ContoursManager( contoursManager = ContoursManager(
context = this, context = this,
mapView = mapView, mapView = mapView,
scope = lifecycleScope scope = lifecycleScope
) )
val points2 = points.flatten()
contoursManager.updateContourSize(6) contoursManager.updateContourSize(6)
contoursManager.updatePoints(points2) contoursManager.updatePoints(points)
val height = points2.map { it.z } val height = points.map { it.z }
val min = height.min() val min = height.min()
val max = height.max() val max = height.max()
contoursManager.updateHeightRange((min / 2)..max) contoursManager.updateHeightRange((min / 2)..max)
@@ -125,6 +121,7 @@ class MainActivity : AppCompatActivity() {
override fun onStopTrackingTouch(slider: Slider) { override fun onStopTrackingTouch(slider: Slider) {
contoursManager.updateCellSize(slider.value.toDouble()) contoursManager.updateCellSize(slider.value.toDouble())
contoursManager.refresh()
} }
} }
) )
@@ -135,13 +132,78 @@ class MainActivity : AppCompatActivity() {
binding.clearPoints.setOnClickListener { binding.clearPoints.setOnClickListener {
viewModel.clearPoints() viewModel.clearPoints()
} }
binding.slopeDirection.addOnSliderTouchListener(
object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
}
override fun onStopTrackingTouch(slider: Slider) {
earthworkManager.updateSlopeDirection(slider.value.degrees)
}
}
)
binding.slopePercentage.addOnSliderTouchListener(
object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
}
override fun onStopTrackingTouch(slider: Slider) {
earthworkManager.updateSlopePercentage(slider.value.toDouble())
}
}
)
binding.designHeight.addOnSliderTouchListener(
object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
}
override fun onStopTrackingTouch(slider: Slider) {
earthworkManager.updateDesignHeight(slider.value.toDouble())
}
}
)
binding.switchDesignSurface.setOnCheckedChangeListener { button, isChecked ->
showDesignHeight.value = isChecked
}
earthworkManager.setupOnMoveListener()
initData() initData()
} }
private val showDesignHeight = MutableStateFlow(false)
private fun initData() { private fun initData() {
viewModel.points.onEach { viewModel.points.onEach {
contoursManager.updatePoints(it) contoursManager.updatePoints(it)
contoursManager.updateHeightRange() contoursManager.updateHeightRange()
contoursManager.refresh()
}.launchIn(lifecycleScope)
contoursManager.gridModel.filterNotNull().onEach {
earthworkManager.updateGridModel(it)
}.launchIn(lifecycleScope)
earthworkManager.slopeDirection.onEach {
binding.slopeDirection.value = it.degrees.toFloat()
}.launchIn(lifecycleScope)
combine(
earthworkManager.slopeDirection,
earthworkManager.slopePercentage,
earthworkManager.baseHeightOffset,
contoursManager.gridModel,
showDesignHeight
) { slopeDirection, slopePercentage, baseHeightOffset, gridModel, showDesignHeight ->
gridModel?.let { gridModel ->
val slopeResult: SlopeResult = SlopeCalculator.calculateSlope(
grid = gridModel,
slopeDirection = slopeDirection.degrees,
slopePercentage = slopePercentage,
baseHeightOffset = baseHeightOffset
)
mapView.displaySlopeResult(
originalGrid = gridModel,
slopeResult = slopeResult,
palette = contoursManager.simplePalette::palette,
showDesignHeight = showDesignHeight
)
}
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
} }
} }

View File

@@ -0,0 +1,44 @@
package com.icegps.orx
import com.icegps.math.geometry.Vector2D
import com.icegps.math.geometry.Vector3D
import com.icegps.math.geometry.toVector2D
/**
* @author tabidachinokaze
* @date 2025/11/26
*/
object RayCastingAlgorithm {
/**
* 使用射线法判断点是否在多边形内
* @param point 测试点
* @param polygon 多边形顶点列表
* @return true如果在多边形内
*/
fun isPointInPolygon(point: Vector2D, polygon: List<Vector2D>): Boolean {
if (polygon.size < 3) return false
val x = point.x
val y = point.y
var inside = false
var j = polygon.size - 1
for (i in polygon.indices) {
val xi = polygon[i].x
val yi = polygon[i].y
val xj = polygon[j].x
val yj = polygon[j].y
val intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
if (intersect) inside = !inside
j = i
}
return inside
}
fun isPointInPolygon(point: Vector3D, polygon: List<Vector3D>): Boolean {
return isPointInPolygon(point.toVector2D(), polygon.map { it.toVector2D() })
}
}

View File

@@ -0,0 +1,15 @@
package com.icegps.orx.ktx
import com.icegps.common.helper.GeoHelper
import com.icegps.math.geometry.Vector2D
import com.mapbox.geojson.Point
/**
* @author tabidachinokaze
* @date 2025/11/26
*/
fun Vector2D.toMapboxPoint(): Point {
val geoHelper = GeoHelper.getSharedInstance()
val wgs84 = geoHelper.enuToWGS84Object(GeoHelper.ENU(x = x, y = y))
return Point.fromLngLat(wgs84.lon, wgs84.lat)
}

View File

@@ -8,7 +8,7 @@ import org.openrndr.shape.path3D
* Kotlin/OPENRNDR idiomatic interface to `Delaunay` * Kotlin/OPENRNDR idiomatic interface to `Delaunay`
*/ */
class DelaunayTriangulation3D(val points: List<Vector3>) { class DelaunayTriangulation3D(val points: List<Vector3>) {
val delaunay: Delaunay = Delaunay.Companion.from(points.map { it.xy }) val delaunay: Delaunay = Delaunay.from(points.map { it.xy })
fun neighbors(pointIndex: Int): Sequence<Int> { fun neighbors(pointIndex: Int): Sequence<Int> {
return delaunay.neighbors(pointIndex) return delaunay.neighbors(pointIndex)

View File

@@ -112,6 +112,72 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="清除所有点" /> android:text="清除所有点" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="坡向(角度)" />
<com.google.android.material.slider.Slider
android:id="@+id/slope_direction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:valueFrom="0"
android:valueTo="360" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="坡度(%)" />
<com.google.android.material.slider.Slider
android:id="@+id/slope_percentage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:valueFrom="0"
android:valueTo="100" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设计面高度(m)" />
<com.google.android.material.slider.Slider
android:id="@+id/design_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:value="0"
android:valueFrom="-100"
android:valueTo="100" />
</LinearLayout>
<Switch
android:id="@+id/switch_design_surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:switchPadding="16dp"
android:text="显示设计面" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -103,6 +103,72 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="清除所有点" /> android:text="清除所有点" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="坡向(角度)" />
<com.google.android.material.slider.Slider
android:id="@+id/slope_direction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:valueFrom="0"
android:valueTo="360" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="坡度(%)" />
<com.google.android.material.slider.Slider
android:id="@+id/slope_percentage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:valueFrom="0"
android:valueTo="100" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设计面高度(m)" />
<com.google.android.material.slider.Slider
android:id="@+id/design_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:value="0"
android:valueFrom="-100"
android:valueTo="100" />
</LinearLayout>
<Switch
android:id="@+id/switch_design_surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:switchPadding="16dp"
android:text="显示设计面" />
</LinearLayout> </LinearLayout>
<com.mapbox.maps.MapView <com.mapbox.maps.MapView