完成了土方量计算
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
197
android/src/main/java/com/icegps/orx/ControllableArrow.kt
Normal file
197
android/src/main/java/com/icegps/orx/ControllableArrow.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
144
android/src/main/java/com/icegps/orx/DisplaySlopeResult.kt
Normal file
144
android/src/main/java/com/icegps/orx/DisplaySlopeResult.kt
Normal 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)
|
||||||
|
}
|
||||||
438
android/src/main/java/com/icegps/orx/EarthworkManager.kt
Normal file
438
android/src/main/java/com/icegps/orx/EarthworkManager.kt
Normal 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)} 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²")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
android/src/main/java/com/icegps/orx/RayCastingAlgorithm.kt
Normal file
44
android/src/main/java/com/icegps/orx/RayCastingAlgorithm.kt
Normal 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() })
|
||||||
|
}
|
||||||
|
}
|
||||||
15
android/src/main/java/com/icegps/orx/ktx/Vector2D.kt
Normal file
15
android/src/main/java/com/icegps/orx/ktx/Vector2D.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user