完成了土方量计算

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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.openrndr.extra.shapes.splines.CatmullRomChain2
@@ -54,7 +57,7 @@ class ContoursManager(
private var contourSize: Int = 6
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
private var cellSize: Double? = 10.0
private val simplePalette = SimplePalette(
val simplePalette = SimplePalette(
range = 0.0..100.0
)
@@ -103,18 +106,21 @@ class ContoursManager(
}
private var isGridVisible: Boolean = true
private var gridModel: GridModel? = null
private var _gridModel = MutableStateFlow<GridModel?>(null)
val gridModel = _gridModel.asStateFlow()
fun setGridVisible(visible: Boolean) {
if (visible != isGridVisible) {
isGridVisible = visible
if (visible) {
if (gridModel != null) mapView.displayGridModel(
grid = gridModel!!,
sourceId = gridSourceId,
layerId = gridLayerId,
palette = simplePalette::palette
)
_gridModel.value?.let { gridModel ->
mapView.displayGridModel(
grid = gridModel,
sourceId = gridSourceId,
layerId = gridLayerId,
palette = simplePalette::palette
)
}
} else {
mapView.mapboxMap.getStyle { style ->
try {
@@ -149,12 +155,15 @@ class ContoursManager(
}
}
private var job: Job? = null
fun refresh() {
val points = points
if (points.size <= 3) {
context.toast("points size ${points.size}")
return
}
job?.cancel()
scope.launch {
mapView.mapboxMap.getStyle { style ->
val step = heightRange.endInclusive / contourSize
@@ -175,7 +184,7 @@ class ContoursManager(
delaunator = triangulation,
cellSize = cellSize,
)
this@ContoursManager.gridModel = gridModel
this@ContoursManager._gridModel.value = gridModel
if (isGridVisible) mapView.displayGridModel(
grid = gridModel,
sourceId = gridSourceId,
@@ -183,7 +192,7 @@ class ContoursManager(
palette = simplePalette::palette
)
}
scope.launch(Dispatchers.Default) {
job = scope.launch(Dispatchers.Default) {
val lineFeatures = mutableListOf<List<Feature>>()
val features = zip.mapIndexed { index, range ->
async {
@@ -264,10 +273,10 @@ class ContoursManager(
style.addSource(source)
val layer = lineLayer(layerId, sourceId) {
lineColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
lineWidth(1.0)
lineCap(LineCap.Companion.ROUND)
lineJoin(LineJoin.Companion.ROUND)
lineCap(LineCap.ROUND)
lineJoin(LineJoin.ROUND)
lineOpacity(0.8)
}
style.addLayer(layer)
@@ -288,7 +297,7 @@ class ContoursManager(
style.addSource(source)
val layer = fillLayer(layerId, sourceId) {
fillColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
fillOpacity(0.5)
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 cellSize: 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(
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.Slider
import com.icegps.common.helper.GeoHelper
import com.icegps.math.geometry.degrees
import com.icegps.orx.databinding.ActivityMainBinding
import com.icegps.shared.model.GeoPoint
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.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -26,6 +30,7 @@ class MainActivity : AppCompatActivity() {
ViewModelProvider(this)[MainViewModel::class.java]
}
private lateinit var contoursManager: ContoursManager
private lateinit var earthworkManager: EarthworkManager
init {
initGeoHelper()
@@ -36,6 +41,7 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
mapView = binding.mapView
earthworkManager = EarthworkManager(mapView, lifecycleScope)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -52,27 +58,17 @@ class MainActivity : AppCompatActivity() {
.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
contoursManager = ContoursManager(
context = this,
mapView = mapView,
scope = lifecycleScope
)
val points2 = points.flatten()
contoursManager.updateContourSize(6)
contoursManager.updatePoints(points2)
val height = points2.map { it.z }
contoursManager.updatePoints(points)
val height = points.map { it.z }
val min = height.min()
val max = height.max()
contoursManager.updateHeightRange((min / 2)..max)
@@ -125,6 +121,7 @@ class MainActivity : AppCompatActivity() {
override fun onStopTrackingTouch(slider: Slider) {
contoursManager.updateCellSize(slider.value.toDouble())
contoursManager.refresh()
}
}
)
@@ -135,13 +132,78 @@ class MainActivity : AppCompatActivity() {
binding.clearPoints.setOnClickListener {
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()
}
private val showDesignHeight = MutableStateFlow(false)
private fun initData() {
viewModel.points.onEach {
contoursManager.updatePoints(it)
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)
}
}

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`
*/
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> {
return delaunay.neighbors(pointIndex)

View File

@@ -112,6 +112,72 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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>
</ScrollView>
</LinearLayout>

View File

@@ -103,6 +103,72 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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>
<com.mapbox.maps.MapView