feat: 添加 ahrs 和 excavator-bucket-position 模块

This commit is contained in:
2025-05-30 00:41:39 +08:00
parent 74ea2ad62a
commit 784f2324bd
17 changed files with 2303 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
plugins {
kotlin("jvm")
}
repositories {
mavenCentral()
}
dependencies {
implementation(project(":libs:ahrs"))
}

View File

@@ -0,0 +1,263 @@
package com.icegps.excavator.model
import kotlin.math.*
/**
* BLH -> ENU
*
* @author lm
* @date 2025/3/12
* @link https://gist.github.com/komasaru/6ce0634475923ddac597f868288c54e9
*/
class BlhToEnu {
companion object {
private const val PI_180 = Math.PI / 180.0
// WGS84 坐标参数
private const val A = 6378137.0 // a地球椭球体长半径赤道面平均半径
private const val ONE_F = 298.257223563 // 1 / f地球椭球体扁平率 = (a - b) / a
private val B = A * (1.0 - 1.0 / ONE_F) // b地球椭球体短半径
private val E2 = (1.0 / ONE_F) * (2 - (1.0 / ONE_F))
// e^2 = 2 * f - f * f = (a^2 - b^2) / a^2
private val ED2 = E2 * A * A / (B * B) // e'^2 = (a^2 - b^2) / b^2
}
private var originLat: Double = 0.0
private var originLon: Double = 0.0
private var originHeight: Double = 0.0
private var isOriginSet: Boolean = false
fun resetEnuBenchmarkPoint() {
isOriginSet = false
}
fun wgs84ToEnu(lat: Double, lon: Double, height: Double = 0.0): DoubleArray {
if (!isOriginSet) {
originLat = lat
originLon = lon
originHeight = height
isOriginSet = true
return doubleArrayOf(0.0, 0.0, 0.0)
}
val enu = blh2enu(originLat, originLon, originHeight, lat, lon, height)
var az = atan2(enu[0], enu[1]) * 180.0 / Math.PI
if (az < 0.0) {
az += 360.0
}
val el = atan2(
enu[2],
sqrt(enu[0] * enu[0] + enu[1] * enu[1])
) * 180.0 / Math.PI
val dst = sqrt(enu.sumOf { it * it })
// println("--->")
// println(
// """
// ENU: E = ${enu[0].format(3)}m
// N = ${enu[1].format(3)}m
// U = ${enu[2].format(3)}m
// 方位角 = ${az.format(3)}°
// 仰角 = ${el.format(3)}°
// 距离 = ${dst.format(3)}m
// """.trimIndent()
// )
return enu
}
fun enuToWgs84(e: Double, n: Double, u: Double): DoubleArray {
if (!isOriginSet) {
return doubleArrayOf(0.0, 0.0, 0.0)
}
val blh = enu2blh(originLat, originLon, originHeight, e, n, u)
// println("--->")
// println(
// """
// BLH: Beta = ${blh[0].format(8)}°
// Lambda = ${blh[1].format(8)}°
// Height = ${blh[2].format(3)}m
// """.trimIndent()
// )
return blh
}
private fun Double.format(digits: Int) = "%.${digits}f".format(this)
/**
* BLH -> ENU 转换(East, North, Up)
*
* @param bO 原点 Beta(纬度)
* @param lO 原点 Lambda(经度)
* @param hO 原点 Height(高度)
* @param b 目标点 Beta(纬度)
* @param l 目标点 Lambda(经度)
* @param h 目标点 Height(高度)
* @return ENU 坐标 [e, n, u]
*/
private fun blh2enu(bO: Double, lO: Double, hO: Double, b: Double, l: Double, h: Double): DoubleArray {
val (xO, yO, zO) = blh2ecef(bO, lO, hO)
val (x, y, z) = blh2ecef(b, l, h)
val mat0 = matZ(90.0)
val mat1 = matY(90.0 - bO)
val mat2 = matZ(lO)
val mat = mulMat(mulMat(mat0, mat1), mat2)
return rotate(mat, doubleArrayOf(x - xO, y - yO, z - zO))
}
/**
* BLH -> ECEF 转换
*
* @param lat 纬度
* @param lon 经度
* @param ht 高度
* @return ECEF 坐标 [x, y, z]
*/
private fun blh2ecef(lat: Double, lon: Double, ht: Double): DoubleArray {
val n = { x: Double -> A / sqrt(1.0 - E2 * sin(x * PI_180).pow(2)) }
val x = (n(lat) + ht) * cos(lat * PI_180) * cos(lon * PI_180)
val y = (n(lat) + ht) * cos(lat * PI_180) * sin(lon * PI_180)
val z = (n(lat) * (1.0 - E2) + ht) * sin(lat * PI_180)
return doubleArrayOf(x, y, z)
}
/**
* ENU -> BLH 转换
*
* @param e East 坐标
* @param n North 坐标
* @param u Up 坐标
* @return WGS84 坐标 [纬度, 经度, 高度]
*/
private fun enu2blh(bO: Double, lO: Double, hO: Double, e: Double, n: Double, u: Double): DoubleArray {
val mat0 = matZ(-lO)
val mat1 = matY(-(90.0 - bO))
val mat2 = matZ(-90.0)
val mat = mulMat(mulMat(mat0, mat1), mat2)
val enu = doubleArrayOf(e, n, u)
val xyz = rotate(mat, enu)
val (xO, yO, zO) = blh2ecef(bO, lO, hO)
val x = xyz[0] + xO
val y = xyz[1] + yO
val z = xyz[2] + zO
return ecef2blh(x, y, z)
}
/**
* ECEF -> BLH 转换
* @param x ECEF X 坐标
* @param y ECEF Y 坐标
* @param z ECEF Z 坐标
* @return WGS84 坐标 [纬度, 经度, 高度]
*/
private fun ecef2blh(x: Double, y: Double, z: Double): DoubleArray {
val p = sqrt(x * x + y * y)
val theta = atan2(z * A, p * B)
val sinTheta = sin(theta)
val cosTheta = cos(theta)
val lat = atan2(
z + ED2 * B * sinTheta.pow(3),
p - E2 * A * cosTheta.pow(3)
)
val lon = atan2(y, x)
val sinLat = sin(lat)
val n = A / sqrt(1.0 - E2 * sinLat * sinLat)
val h = p / cos(lat) - n
return doubleArrayOf(
lat * 180.0 / Math.PI,
lon * 180.0 / Math.PI,
h
)
}
/**
* 以 x 轴为轴的旋转矩阵
*
* @param ang 旋转角度(°)
* @return 旋转矩阵(3x3)
*/
private fun matX(ang: Double): Array<DoubleArray> {
val a = ang * PI_180
val c = cos(a)
val s = sin(a)
return arrayOf(
doubleArrayOf(1.0, 0.0, 0.0),
doubleArrayOf(0.0, c, s),
doubleArrayOf(0.0, -s, c)
)
}
/**
* 以 y 轴为轴的旋转矩阵
*
* @param ang 旋转角度(°)
* @return 旋转矩阵(3x3)
*/
private fun matY(ang: Double): Array<DoubleArray> {
val a = ang * PI_180
val c = cos(a)
val s = sin(a)
return arrayOf(
doubleArrayOf(c, 0.0, -s),
doubleArrayOf(0.0, 1.0, 0.0),
doubleArrayOf(s, 0.0, c)
)
}
/**
* 以 z 轴为轴的旋转矩阵
*
* @param ang 旋转角度(°)
* @return 旋转矩阵(3x3)
*/
private fun matZ(ang: Double): Array<DoubleArray> {
val a = ang * PI_180
val c = cos(a)
val s = sin(a)
return arrayOf(
doubleArrayOf(c, s, 0.0),
doubleArrayOf(-s, c, 0.0),
doubleArrayOf(0.0, 0.0, 1.0)
)
}
/**
* 两个矩阵(3x3)的乘积
*
* @param matA 3x3 矩阵
* @param matB 3x3 矩阵
* @return 3x3 矩阵
*/
private fun mulMat(matA: Array<DoubleArray>, matB: Array<DoubleArray>): Array<DoubleArray> {
return Array(3) { k ->
DoubleArray(3) { j ->
(0..2).sumOf { i -> matA[k][i] * matB[i][j] }
}
}
}
/**
* 点的旋转
*
* @param mat 3x3 旋转矩阵
* @param pt 旋转前坐标 [x, y, z]
* @return 旋转后坐标 [x, y, z]
*/
private fun rotate(mat: Array<DoubleArray>, pt: DoubleArray): DoubleArray {
return DoubleArray(3) { j ->
(0..2).sumOf { i -> mat[j][i] * pt[i] }
}
}
}

View File

@@ -0,0 +1,62 @@
package com.icegps.excavator.model
/**
* @author xjh
* @date 2025/2/25
*/
// 添加一个全局变量来存储缓存的LCE值
var cachedLCE: Double = 0.0
/**
* 挖掘机参数配置类
*/
data class ExcavatorConfig(
/**
* RTK 相关距离参数
*/
val rtkConfig: RTKConfiguration = RTKConfiguration(
mainRtkToBoomAxis = 1.0, // 主 RTK 相位中心到支架底部的垂直距离
subRtkToBoomAxis = 1.0, // 副 RTK 相位中心到支架底部的垂直距离
mainRtkFootToJoint1 = 1.0, // 主 RTK 在大臂上的垂足到关节 1 的距离 (不必须)
subRtkFootToJoint2 = 1.0 // 副 RTK 在大臂上的垂足到关节 2 的距离
),
/**
* 挖掘机尺寸信息
*/
val excavatorDims: ExcavatorDimensions = ExcavatorDimensions(
boom = 1.000, // 大臂长度(不必须)
stick = 1.0, // 小臂长度
bucket = 1.0, // 挖斗长度(不必要)
mainBoomWidth = 1.0, // 主天线处大臂宽度
subBoomWidth = 1.0, // 副天线处大臂宽度
mainAntennaBracketWidth = 1.0, // 主天线支架宽度
subAntennaBracketWidth = 1.0, // 副天线支架宽度
mainAntennaBottomToBoomAxis = 1.0, // 主天线底面到大臂轴心的垂直距离
subAntennaBottomToBoomAxis = 1.0 // 副天线底面到大臂轴心的垂直距离
),
/**
* 连杆长度参数
*/
val linkageLengths: LinkageLengths = LinkageLengths(
LAB = 1.0, // 惰连杆长度(AB)
LBC = 1.0, // 斗连杆长度(BC)
LCD = 1.0, // 前后轴套长度(CD)
LAD = 1.0, // 伪轴至斗齿长度(AD)
LAE = 1.0, // 伪轴至斗齿距离(AE)
LCE = cachedLCE, // (CE)
LDE = 1.0, // 斗轴至斗齿距离(DE)
W = 1.0 // 铲斗宽度(W)
),
// 安装模式
val antennaInstallationMode: AntennaInstallationMode = AntennaInstallationMode.LEFT,
val sensorInstallationMode: SensorInstallationMode = SensorInstallationMode.CUSTOM,
// 传感器偏移量
val stickOffsetEulerAngles: EulerAngles = EulerAngles(0.0, 0.0, 0.0),
val idleConnectingRodOffsetEulerAngles: EulerAngles = EulerAngles(0.0, 0.0, 0.0)
)

View File

@@ -0,0 +1,603 @@
package com.icegps.excavator.model
import java.util.Locale
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
/**
* 3D 向量类,用于表示位置和方向
*/
data class Vector3(val x: Double, val y: Double, val z: Double) {
operator fun plus(other: Vector3) = Vector3(x + other.x, y + other.y, z + other.z)
operator fun minus(other: Vector3) = Vector3(x - other.x, y - other.y, z - other.z)
operator fun times(scalar: Double) = Vector3(x * scalar, y * scalar, z * scalar)
operator fun div(scalar: Double) = Vector3(x / scalar, y / scalar, z / scalar)
operator fun unaryMinus() = Vector3(-x, -y, -z)
fun norm(): Double = sqrt(x * x + y * y + z * z)
fun normalize(): Vector3 = this / norm()
fun dot(other: Vector3): Double = x * other.x + y * other.y + z * other.z
fun cross(other: Vector3): Vector3 = Vector3(
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x
)
fun distanceTo(other: Vector3): Double = (this - other).magnitude()
fun magnitude(): Double = sqrt(x * x + y * y + z * z)
companion object {
val ZERO = Vector3(0.0, 0.0, 0.0)
val UP = Vector3(0.0, 0.0, 1.0)
}
}
/**
* 天线安装方式
*/
enum class AntennaInstallationMode {
LEFT, // 左侧安装
RIGHT // 右侧安装
}
/**
* 传感器安装方式
*/
enum class SensorInstallationMode {
CUSTOM, // 自定义安装方式
VERTICAL, // 垂直安装(正装):传感器的 Z 轴与机体 Z 轴平行
HORIZONTAL // 水平安装(侧装):传感器的 Y 轴与机体 Z 轴平行
}
/**
* E 点计算方式
*/
enum class ECalculationMethod {
USE_AE, // 使用 AE 方式计算
USE_CE, // 使用 CE 方式计算
}
/**
* RTK 相关距离参数
*/
data class RTKConfiguration(
val mainRtkToBoomAxis: Double, // 主 RTK 相位中心到支架底部的垂直距离
val subRtkToBoomAxis: Double, // 副 RTK 相位中心到支架底部的垂直距离
val mainRtkFootToJoint1: Double, // 主 RTK 在大臂上的垂足到关节 1 的距离
val subRtkFootToJoint2: Double // 副 RTK 在大臂上的垂足到关节 2 的距离
)
/**
* 挖掘机尺寸信息
*/
data class ExcavatorDimensions(
val boom: Double, // 大臂长度
val stick: Double, // 小臂长度
val bucket: Double, // 铲斗长度
val mainBoomWidth: Double, // 主天线处大臂宽度
val subBoomWidth: Double, // 副天线处大臂宽度
val mainAntennaBracketWidth: Double, // 主天线支架宽度
val subAntennaBracketWidth: Double, // 副天线支架宽度
val mainAntennaBottomToBoomAxis: Double, // 主天线底面到大小臂轴心的垂直距离
val subAntennaBottomToBoomAxis: Double // 副天线底面到大小臂轴心的垂直距离
)
/**
* 连杆长度参数
*/
data class LinkageLengths(
val LAD: Double, // A 点到 D 点的距离
val LAB: Double, // A 点到 B 点的距离
val LBC: Double, // B 点到 C 点的距离
val LCD: Double, // C 点到 D 点的距离
val LAE: Double, // A 点到 E 点的距离
val LCE: Double, // C 点到 E 点的距离
val LDE: Double, // D 点到 E 点的距离
val W: Double // E1 到 E2 的距离(铲斗宽度)
)
/**
* 欧拉角
*/
data class EulerAngles(
val yaw: Double, // 偏航角
val pitch: Double, // 俯仰角
val roll: Double // 横滚角
) {
fun toList(): List<Double> = listOf(yaw, pitch, roll)
}
/**
* 挖掘机关键点
*/
data class ExcavatorKeyPoints(
val joint1: Vector3, // 关节1大臂起点位置
val joint2: Vector3, // 关节2大臂末端/小臂起点位置)
val joint3: Vector3, // 关节3小臂末端/铲斗连接点位置)
val A: Vector3, // A点
val B: Vector3, // B点
val C: Vector3, // C点
val D: Vector3, // D点
val E: Vector3, // 齿尖中心点
val E1: Vector3, // 齿尖左侧点
val E2: Vector3 // 齿尖右侧点
)
/**
* 挖掘机位置计算器
*
* @author lm
* @date 2025/3/5
*/
class ExcavatorPositionCalculator {
/**
* 应用欧拉角偏移量
*/
private fun applyOffsets(angles: EulerAngles, offset: EulerAngles): EulerAngles {
return EulerAngles(
yaw = angles.yaw - offset.yaw,
pitch = angles.pitch - offset.pitch,
roll = angles.roll - offset.roll
)
}
/**
* 计算挖掘机关键点位置
*
* 机体坐标系:
* - X 轴:指向机器前方
* - Y 轴:指向机器右侧
* - Z 轴:指向机器上方
*
* @param mainRtkPos 主 RTK 相位中心的 ENU 坐标
* @param subRtkPos 副 RTK 相位中心的 ENU 坐标
* @param rtkConfig RTK 相关配置参数
* @param excavatorDims 挖掘机尺寸参数
* @param linkageLengths 连杆长度参数
* @param stickAngleSensor 小臂传感器的欧拉角,单位为弧度
* @param idleConnectingRodAngleSensor 惰连杆传感器的欧拉角,单位为弧度
* @param antennaInstallationMode 天线安装模式
* @param sensorInstallationMode 传感器安装模式
* @param eCalculationMethod E 点计算方式
* @param stickOffsetEulerAngles 小臂传感器的欧拉角偏移量,单位为弧度
* @param idleConnectingRodOffsetEulerAngles 惰连杆传感器的欧拉角偏移量,单位为弧度
* @return ExcavatorKeyPoints 包含所有关键点坐标的结果对象
*/
fun calculateExcavatorKeyPoints(
mainRtkPos: Vector3,
subRtkPos: Vector3,
rtkConfig: RTKConfiguration,
excavatorDims: ExcavatorDimensions,
linkageLengths: LinkageLengths,
stickAngleSensor: EulerAngles,
idleConnectingRodAngleSensor: EulerAngles,
antennaInstallationMode: AntennaInstallationMode = AntennaInstallationMode.LEFT,
sensorInstallationMode: SensorInstallationMode = SensorInstallationMode.VERTICAL,
eCalculationMethod: ECalculationMethod = ECalculationMethod.USE_CE,
stickOffsetEulerAngles: EulerAngles = EulerAngles(0.0, 0.0, 0.0),
idleConnectingRodOffsetEulerAngles: EulerAngles = EulerAngles(0.0, 0.0, 0.0)
): ExcavatorKeyPoints {
// 应用偏移量
val stickAngleSensorCorrected = applyOffsets(stickAngleSensor, stickOffsetEulerAngles)
val idleConnectingRodAngleSensorCorrected = applyOffsets(idleConnectingRodAngleSensor, idleConnectingRodOffsetEulerAngles)
// 根据传感器安装模式选择正确的角度
val stickAngle = when (sensorInstallationMode) {
SensorInstallationMode.CUSTOM -> stickAngleSensorCorrected.roll
SensorInstallationMode.VERTICAL -> stickAngleSensorCorrected.pitch
SensorInstallationMode.HORIZONTAL -> stickAngleSensorCorrected.yaw
}
val idleConnectingRodAngle = when (sensorInstallationMode) {
SensorInstallationMode.CUSTOM -> idleConnectingRodAngleSensorCorrected.roll
SensorInstallationMode.VERTICAL -> idleConnectingRodAngleSensorCorrected.pitch
SensorInstallationMode.HORIZONTAL -> idleConnectingRodAngleSensorCorrected.yaw
}
// 计算天线方向向量
val antennaVector = subRtkPos - mainRtkPos
val antennaDirection = antennaVector.normalize()
// 计算垂直于天线方向的向量(水平方向)
val antennaPerpendicular = antennaDirection.cross(Vector3.UP).normalize()
// 计算垂直于天线方向且指向"上方"的向量(垂直方向)
val antennaUp = antennaPerpendicular.cross(antennaDirection).normalize()
// 根据天线安装模式确定偏移方向
val offsetDirection = when (antennaInstallationMode) {
AntennaInstallationMode.LEFT -> -antennaPerpendicular
AntennaInstallationMode.RIGHT -> antennaPerpendicular
}
// 计算天线到大臂中心线的偏移
val mainOffset = (excavatorDims.mainBoomWidth + excavatorDims.mainAntennaBracketWidth) / 2
val subOffset = (excavatorDims.subBoomWidth + excavatorDims.subAntennaBracketWidth) / 2
// 计算 RTK 到大臂中心线的垂足
val mainRtkFoot = mainRtkPos - antennaUp * (rtkConfig.mainRtkToBoomAxis + excavatorDims.mainAntennaBottomToBoomAxis) - offsetDirection * mainOffset
val subRtkFoot = subRtkPos - antennaUp * (rtkConfig.subRtkToBoomAxis + excavatorDims.subAntennaBottomToBoomAxis) - offsetDirection * subOffset
// 计算大臂实际方向向量
val boomVector = subRtkFoot - mainRtkFoot
val boomDirection = boomVector.normalize()
// 计算连接点1的位置大臂起点
val joint1Pos = mainRtkFoot + boomDirection * rtkConfig.mainRtkFootToJoint1
// 计算连接点2的位置大臂末端/小臂起点)
val joint2Pos = subRtkFoot - boomDirection * rtkConfig.subRtkFootToJoint2
// 计算 yaw 角(大臂在水平面上的方向)
val boomYaw = atan2(boomDirection.y, boomDirection.x)
// 计算小臂的方向
val stickDirection = Vector3(
cos(boomYaw) * cos(stickAngle),
sin(boomYaw) * cos(stickAngle),
sin(stickAngle)
).normalize()
// 确保小臂方向与大臂方向在同一平面
val boomNormal = boomDirection.cross(Vector3.UP)
val stickDirectionAdjusted = (stickDirection - boomNormal * stickDirection.dot(boomNormal)).normalize()
// 计算连接点3的位置小臂末端 / 铲斗连接点)
val joint3Pos = joint2Pos + stickDirectionAdjusted * excavatorDims.stick
// 设置 D 点
val D = joint3Pos
// 计算 A 点坐标
val A = D - stickDirectionAdjusted * linkageLengths.LAD
// 计算 B 点坐标
val idleConnectingRodDirection = Vector3(
cos(boomYaw) * cos(idleConnectingRodAngle),
sin(boomYaw) * cos(idleConnectingRodAngle),
sin(idleConnectingRodAngle)
)
val B = A + idleConnectingRodDirection * linkageLengths.LAB
val planeNormal = boomDirection.cross(Vector3.UP).normalize()
// 使用球面交点方法计算 C 点坐标
val (candidateC1, candidateC2) = findIntersection3d(B, linkageLengths.LBC, D, linkageLengths.LCD, planeNormal)
.let { (c1, c2) -> Pair(c1 ?: D, c2 ?: D) }
// 选择正确的 C 点
val C = selectCorrectCPoint(A, B, D, candidateC1, candidateC2)
// 计算 E 点坐标
val (candidateE1, candidateE2) = when (eCalculationMethod) {
ECalculationMethod.USE_AE ->
findIntersection3d(A, linkageLengths.LAE, D, linkageLengths.LDE, planeNormal)
.let { (e1, e2) -> Pair(e1 ?: D, e2 ?: D) }
ECalculationMethod.USE_CE ->
findIntersection3d(C, linkageLengths.LCE, D, linkageLengths.LDE, planeNormal)
.let { (e1, e2) -> Pair(e1 ?: D, e2 ?: D) }
}
// 选择正确的 E 点(位于 AB 的对面)
val ABMidpoint = (A + B) * 0.5
val ABToD = D - ABMidpoint
val E = if ((candidateE1 - ABMidpoint).dot(ABToD) > (candidateE2 - ABMidpoint).dot(ABToD)) candidateE1 else candidateE2
// 计算 E1 和 E2 点坐标
val boomDirectionHorizontal = Vector3(
boomDirection.x,
boomDirection.y,
0.0
).normalize()
// 计算挖掘机朝向的右侧向量
val rightVector = boomDirectionHorizontal.cross(Vector3.UP).normalize()
// 计算铲斗方向向量
val bucketDirection = (E - D).normalize()
// 计算铲斗左右方向向量
var bucketLeftRight = bucketDirection.cross(stickDirectionAdjusted).normalize()
// 确保 bucketLeftRight 与 rightVector 方向一致
if (bucketLeftRight.dot(rightVector) < 0) {
bucketLeftRight = -bucketLeftRight
}
// 计算 E1左铲尖和 E2右铲尖
val E1 = E - bucketLeftRight * (linkageLengths.W / 2)
val E2 = E + bucketLeftRight * (linkageLengths.W / 2)
return ExcavatorKeyPoints(
joint1 = joint1Pos,
joint2 = joint2Pos,
joint3 = joint3Pos,
A = A,
B = B,
C = C,
D = D,
E = E,
E1 = E1,
E2 = E2
)
}
/**
* 限制角度在 -55 到 55 度之间
*/
private fun limitAngle(angle: Double): Double {
return angle.coerceIn(-55.0, 55.0)
}
/**
* 选择正确的 C 点,基于多个几何和运动学条件
*
* @param A 已知的四连杆点坐标
* @param B 已知的四连杆点坐标
* @param D 已知的四连杆点坐标
* @param C1 候选 C 点坐标
* @param C2 候选 C 点坐标
* @return 选中的 C 点坐标
*/
private fun selectCorrectCPoint(A: Vector3, B: Vector3, D: Vector3, C1: Vector3, C2: Vector3): Vector3 {
fun scoreCPoint(C: Vector3): Double {
/**
* 对 C 点进行综合评分
*/
if (!isCoplanar(A, B, C, D) || !isValidQuadrilateral(A, B, C, D)) {
return Double.NEGATIVE_INFINITY
}
val angleScore = 1.0 / (1.0 + abs(calculateAngleSum(A, B, C, D) - Math.PI))
val transmissionScore = 1.0 / (1.0 + calculateTransmissionAngle(A, B, C, D))
val singularScore = if (checkSingularPosition(A, B, C, D)) 0.0 else 1.0
return angleScore + transmissionScore + singularScore
}
val C1Score = scoreCPoint(C1)
val C2Score = scoreCPoint(C2)
return if (C1Score > C2Score) C1 else C2
}
/**
* 检查四点是否共面
*/
private fun isCoplanar(A: Vector3, B: Vector3, C: Vector3, D: Vector3, tolerance: Double = 1e-6): Boolean {
return abs((D - A).dot((B - A).cross(C - A))) < tolerance
}
/**
* 检查是否为有效的四边形
*/
private fun isValidQuadrilateral(A: Vector3, B: Vector3, C: Vector3, D: Vector3): Boolean {
val points = listOf(A, B, C, D)
if (points.any { p1 -> points.any { p2 -> p1 != p2 && p1.distanceTo(p2) < 1e-6 } }) {
return false // 存在重合点
}
return !(lineIntersection(A, B, C, D) || lineIntersection(A, D, B, C))
}
/**
* 检查两条线段是否相交
*/
private fun lineIntersection(p1: Vector3, p2: Vector3, p3: Vector3, p4: Vector3): Boolean {
val v1 = p2 - p1
val v2 = p4 - p3
val normal = v1.cross(v2)
if (normal.magnitude() < 1e-6) {
return false // 平行或共线
}
val t = (p3 - p1).dot(v2.cross(normal)) / v1.dot(v2.cross(normal))
val u = (p1 - p3).dot(v1.cross(normal)) / v2.dot(v1.cross(normal))
return t in 0.0..1.0 && u in 0.0..1.0
}
/**
* 计算 ∠BAD 和 ∠BCD 的和
*/
private fun calculateAngleSum(A: Vector3, B: Vector3, C: Vector3, D: Vector3): Double {
val BA = A - B
val BC = C - B
val DA = A - D
val DC = C - D
val angleBAD = acos(BA.dot(DA) / (BA.magnitude() * DA.magnitude()))
val angleBCD = acos(BC.dot(DC) / (BC.magnitude() * DC.magnitude()))
return angleBAD + angleBCD
}
/**
* 计算传动角,返回与 90 度的偏差
*/
private fun calculateTransmissionAngle(A: Vector3, B: Vector3, C: Vector3, D: Vector3): Double {
val BA = A - B
val BC = C - B
val DC = C - D
val DA = A - D
val angle1 = acos(BA.dot(BC) / (BA.magnitude() * BC.magnitude()))
val angle2 = acos(DC.dot(DA) / (DC.magnitude() * DA.magnitude()))
return min(abs(Math.PI / 2 - angle1), abs(Math.PI / 2 - angle2))
}
/**
* 检查是否处于奇异位置
*/
private fun checkSingularPosition(A: Vector3, B: Vector3, C: Vector3, D: Vector3, tolerance: Double = 1e-6): Boolean {
val BA = A - B
val BC = C - B
val DC = C - D
val DA = A - D
return (abs(BA.dot(BC) / (BA.magnitude() * BC.magnitude())) > 1 - tolerance ||
abs(DC.dot(DA) / (DC.magnitude() * DA.magnitude())) > 1 - tolerance)
}
/**
* 计算 3D 空间中两个球面在给定平面上的交点
*
* @param center1 第一个球的中心点坐标
* @param radius1 第一个球的半径
* @param center2 第二个球的中心点坐标
* @param radius2 第二个球的半径
* @param planeNormal 平面的法向量
* @return 两个交点的坐标,如果没有交点则返回 null, null
*/
private fun findIntersection3d(center1: Vector3, radius1: Double, center2: Vector3, radius2: Double, planeNormal: Vector3): Pair<Vector3?, Vector3?> {
// 计算两个球心之间的向量
val centerVector = center2 - center1
// 计算中心向量在平面法线上的投影
val projection = planeNormal * centerVector.dot(planeNormal)
// 计算中心向量在平面内的分量
val centerVectorInPlane = centerVector - projection
// 计算平面内两圆心距离
val d = centerVectorInPlane.magnitude()
// 检查是否有交点,如果两圆心距离大于两半径之和,或小于两半径之差,则没有交点
if (d > radius1 + radius2 || d < abs(radius1 - radius2)) {
return Pair(null, null)
}
// 计算交点到 center1 的距离
val a = (radius1.pow(2) - radius2.pow(2) + d.pow(2)) / (2 * d)
// 计算交点到连接线的垂直距离
val h = sqrt(radius1.pow(2) - a.pow(2))
// 计算交点在连接线上的位置
val intersectionBase = center1 + centerVectorInPlane * (a / d)
// 计算垂直于连接线和平面法线的向量
val perpendicular = centerVectorInPlane.cross(planeNormal).normalize()
// 计算两个交点
val intersection1 = intersectionBase + perpendicular * h
val intersection2 = intersectionBase - perpendicular * h
return Pair(intersection1, intersection2)
}
/**
* 将角度转换为弧度
*/
fun degreesToRadians(degrees: Double): Double = degrees * PI / 180.0
/**
* 将弧度转换为角度
*/
fun radiansToDegrees(radians: Double): Double = radians * 180.0 / PI
/**
* 将 ±180 度系统转换为 0-360 度系统
*/
fun convert180To360(angle: Double): Double {
return when {
angle in -90.0..90.0 -> 90.0 - angle
angle > 90.0 -> 450.0 - angle
else -> 90.0 - angle // angle < -90
}
}
/**
* 将 0-360 度系统转换为 ±180 度系统
*/
fun convert360To180(angle: Double): Double {
return when {
angle in 0.0..180.0 -> 90.0 - angle
else -> 450.0 - angle // 180 < angle < 360
}
}
/**
* 将欧拉角格式化为字符串,同时显示弧度和度数
*/
fun formatEulerAngles(eulerAngles: EulerAngles): String {
return eulerAngles.toList().joinToString(", ") { angle ->
String.format(Locale.CHINA, "%.4f rad (%.2f°)", angle, radiansToDegrees(angle))
}
}
/**
* 计算两点之间的俯仰角
*
* 范围为 ±180 度:
* - 0度水平向前
* - +90度垂直向上
* - -90度垂直向下
* - ±180度水平向后
*/
fun calculatePitch(point1: Vector3, point2: Vector3): Double {
// 计算两点之间的差值
val dx = point2.x - point1.x
val dy = point2.y - point1.y
val dz = point2.z - point1.z
// 计算水平距离
val horizontalDistance = sqrt(dx * dx + dy * dy)
// 使用 atan2 计算俯仰角
var pitch = atan2(dz, horizontalDistance)
// 调整角度范围到 ±180 度
if (dx < 0) {
pitch = if (pitch > 0) {
PI - pitch
} else {
-PI - pitch
}
}
return pitch
}
/**
* 计算两点之间的偏航角
*/
fun calculateYaw(point1: Vector3, point2: Vector3): Double {
// 计算水平平面上的差值
val dx = point2.x - point1.x
val dy = point2.y - point1.y
// 使用 atan2 计算偏航角
val yaw = atan2(dy, dx)
return yaw
}
/**
* 计算两点之间的横滚角
*/
fun calculateRoll(point1: Vector3, point2: Vector3, upVector: Vector3): Double {
// 计算从 point1 到 point2 的方向向量并归一化
val direction = (point2 - point1).normalize()
// 计算右向量(叉乘得到垂直于 direction 和 up_vector 的向量)并归一化
val right = direction.cross(upVector).normalize()
// 计算新的上向量(叉乘得到垂直于 direction 和 right 的向量)
val newUp = right.cross(direction)
// 计算 roll 角(新的上向量与原始上向量之间的角度)
val cosRoll = newUp.dot(upVector)
var roll = acos(cosRoll.coerceIn(-1.0, 1.0))
// 确定 roll 的符号
if (right.dot(upVector) < 0) roll = -roll
return roll
}
}

View File

@@ -0,0 +1,306 @@
package com.icegps.excavator.model
import com.icegps.ahrs.AHRS401AParsedCanData
import java.text.SimpleDateFormat
import java.util.*
/**
* @author xjh
* @date 2025/2/25
*/
class ExcavatorPositionManager(
private val excavatorPositionCalculator: ExcavatorPositionCalculator,
private var config: ExcavatorConfig,
private var isCalibrationMode: Boolean = false
) {
companion object {
private const val TAG = "ExcavatorPositionManager"
}
// 回调接口定义
interface OnPositionCalculatedListener {
fun onPositionCalculated(logMessage: String)
}
// 回调列表
private val listeners = mutableListOf<OnPositionCalculatedListener>()
// 添加回调
fun addOnPositionCalculatedListener(listener: OnPositionCalculatedListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
// 移除回调
fun removeOnPositionCalculatedListener(listener: OnPositionCalculatedListener) {
listeners.remove(listener)
}
private var mainRTKData: RTKData? = null
private var subRTKData: RTKData? = null
private var attitudeData: RTKData? = null
var ahrsData: AHRS401AParsedCanData? = null
var subAhrsData: AHRS401AParsedCanData? = null
private var geoHelper: GeoHelper = GeoHelper.getSharedInstance()
private var keyPoints: ExcavatorKeyPoints? = null
/**
* 更新配置
*/
fun updateConfig(newConfig: ExcavatorConfig) {
this.config = newConfig
}
/**
* 设置校准模式
*/
fun setCalibrationMode(calibrationMode: Boolean) {
this.isCalibrationMode = calibrationMode
println("校准模式设置为: $calibrationMode")
}
/**
* 更新主天线RTK数据位置
*/
private fun updateMainRTKData(data: RTKData) {
if (data.isMainAntenna()) {
mainRTKData = data
println("更新主RTK数据: $data")
}
}
/**
* 更新副天线RTK数据位置
*/
private fun updateSubRTKData(data: RTKData) {
if (data.isSubAntenna()) {
subRTKData = data
println("更新副RTK数据: $data")
}
}
/**
* 更新RTK数据自动判断是主天线、副天线还是姿态数据
*/
fun updateRtkData(data: RTKData) {
if (data.isMainAntenna()) {
println("主天线 RTK定位质量:${data.fixQuality} ")
updateMainRTKData(data)
} else if (data.isSubAntenna()) {
println("副天线 RTK定位质量:${data.fixQuality} ")
updateSubRTKData(data)
} else if (data.isAttitudeData()) {
updateAttitudeData(data)
}
}
/**
* 更新姿态数据
*/
private fun updateAttitudeData(data: RTKData) {
if (data.isAttitudeData()) {
if (mainRTKData?.hasPositionData() == true && subRTKData?.hasPositionData() == true) {
attitudeData = data
println("更新姿态数据: $data")
}
}
}
/**
* 更新倾角传感器数据
*/
fun updateAhrsData(data: AHRS401AParsedCanData) {
ahrsData = data
// println("更新AHRS传感器数据: Pitch=${data.pitch}, Roll=${data.roll}, Yaw=${data.yaw}")
}
fun updateSubAhrsData(data: AHRS401AParsedCanData) {
subAhrsData = data
// println("更新subAHRS传感器数据: Pitch=${data.pitch}, Roll=${data.roll}, Yaw=${data.yaw}")
}
fun calculateBucketPoints(): ExcavatorKeyPoints? {
if (mainRTKData == null || !mainRTKData!!.hasPositionData() || !mainRTKData!!.isRtkFixed()) {
println("主天线数据无效或未固定")
return null
}
if (subRTKData == null || !subRTKData!!.hasPositionData() || !subRTKData!!.isRtkFixed()) {
println("副天线数据无效或未固定")
return null
}
// 转换主RTK WGS84坐标到ENU坐标
val mainRtkEnu = geoHelper.wgs84ToENU(
mainRTKData!!.longitude,
mainRTKData!!.latitude,
mainRTKData!!.altitude
)
val mainRtkPos = Vector3(mainRtkEnu.x, mainRtkEnu.y, mainRtkEnu.z)
// 转换副RTK WGS84坐标到ENU坐标
val subRtkEnu = geoHelper.wgs84ToENU(
subRTKData!!.longitude,
subRTKData!!.latitude,
subRTKData!!.altitude
)
val subRtkPos = Vector3(subRtkEnu.x, subRtkEnu.y, subRtkEnu.z)
// 小臂传感器的欧拉角
val stickSensor = EulerAngles(
yaw = Math.toRadians(ahrsData!!.yaw.toDouble()),
pitch = Math.toRadians(ahrsData!!.pitch.toDouble()),
roll = Math.toRadians(ahrsData!!.roll.toDouble())
)
// 惰连杆传感器的欧拉角
val idleConnectingRodAngleSensor = EulerAngles(
yaw = Math.toRadians(subAhrsData!!.yaw.toDouble()),
pitch = Math.toRadians(subAhrsData!!.pitch.toDouble()),
roll = Math.toRadians(subAhrsData!!.roll.toDouble())
)
// 选择E点计算方法校准模式下使用AE方法校准后使用CE方法
val eCalculationMethod = if (isCalibrationMode) {
ECalculationMethod.USE_AE
} else {
ECalculationMethod.USE_CE
}
println(
"传参:\n" +
"mainRtkPos: $mainRtkPos\n" +
"subRtkPos: $subRtkPos\n" +
"rtkConfig: ${config.rtkConfig}\n" +
"excavatorDims: ${config.excavatorDims}\n" +
"linkageLengths: ${config.linkageLengths}\n" +
"stickAngleSensor: $stickSensor\n" +
"idleConnectingRodAngleSensor: $idleConnectingRodAngleSensor\n" +
"antennaInstallationMode: ${config.antennaInstallationMode}\n" +
"sensorInstallationMode: ${config.sensorInstallationMode}\n" +
"eCalculationMethod: $eCalculationMethod\n" +
"isCalibrationMode: $isCalibrationMode"
)
// 计算铲斗位置
keyPoints = excavatorPositionCalculator.calculateExcavatorKeyPoints(
mainRtkPos = mainRtkPos,
subRtkPos = subRtkPos,
rtkConfig = config.rtkConfig,
excavatorDims = config.excavatorDims,
linkageLengths = config.linkageLengths,
stickAngleSensor = stickSensor,
idleConnectingRodAngleSensor = idleConnectingRodAngleSensor,
antennaInstallationMode = config.antennaInstallationMode,
sensorInstallationMode = config.sensorInstallationMode,
stickOffsetEulerAngles = config.stickOffsetEulerAngles,
eCalculationMethod = eCalculationMethod,
idleConnectingRodOffsetEulerAngles = config.idleConnectingRodOffsetEulerAngles
)
notifyListeners(true, keyPoints, stickSensor, idleConnectingRodAngleSensor)
// println("铲斗位置计算成功——经纬度: ${getKeyPointsWGS84Positions()}")
return keyPoints
}
fun getKeyPointsWGS84Positions(): Map<String, Triple<Double, Double, Double>>? {
return keyPoints?.let { keyPoints ->
mapOf(
"joint1" to convertToWGS84(keyPoints.joint1),
"joint2" to convertToWGS84(keyPoints.joint2),
"joint3" to convertToWGS84(keyPoints.joint3),
"A" to convertToWGS84(keyPoints.A),
"B" to convertToWGS84(keyPoints.B),
"C" to convertToWGS84(keyPoints.C),
"D" to convertToWGS84(keyPoints.D),
"E" to convertToWGS84(keyPoints.E),
"E1" to convertToWGS84(keyPoints.E1),
"E2" to convertToWGS84(keyPoints.E2)
)
}
}
private fun convertToWGS84(vector: Vector3): Triple<Double, Double, Double> {
val enu = GeoHelper.Enu(vector.x, vector.y, vector.z)
val wgs84 = geoHelper.enuToWGS84(enu)
return Triple(wgs84[0], wgs84[1], wgs84[2]) // 经度, 纬度, 高度
}
private fun notifyListeners(
success: Boolean,
keyPoints: ExcavatorKeyPoints?,
stickSensor: EulerAngles?,
idleConnectingRodAngleSensor: EulerAngles?
) {
val timestamp = Date()
val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.CHINA)
val timeString = dateFormat.format(timestamp)
// 构建日志消息
val logMessage = buildString {
append("===== $timeString =====\n")
// 添加RTK数据信息
append("RTK数据状态:\n")
mainRTKData?.let {
append("主天线: 经度=${String.format("%.8f", it.longitude)}, ")
append("纬度=${String.format("%.8f", it.latitude)}, ")
append("高度=${String.format("%.3f", it.altitude)}m, ")
append("质量=${it.fixQuality}\n")
} ?: append("主天线: 无数据\n")
subRTKData?.let {
append("副天线: 经度=${String.format("%.8f", it.longitude)}, ")
append("纬度=${String.format("%.8f", it.latitude)}, ")
append("高度=${String.format("%.3f", it.altitude)}m, ")
append("质量=${it.fixQuality}\n")
} ?: append("副天线: 无数据\n")
// 添加传感器数据
stickSensor?.let {
append("传感器0: Roll=${String.format("%.2f", Math.toDegrees(it.roll))}°, ")
append("Pitch=${String.format("%.2f", Math.toDegrees(it.pitch))}°, ")
append("Yaw=${String.format("%.2f", Math.toDegrees(it.yaw))}°\n")
} ?: append("传感器0: 无数据\n")
idleConnectingRodAngleSensor?.let {
append("传感器1: Roll=${String.format("%.2f", Math.toDegrees(it.roll))}°, ")
append("Pitch=${String.format("%.2f", Math.toDegrees(it.pitch))}°, ")
append("Yaw=${String.format("%.2f", Math.toDegrees(it.yaw))}°\n")
} ?: append("传感器1: 无数据\n")
// 添加计算方法和CE长度信息
val eCalculationMethod = if (isCalibrationMode) {
ECalculationMethod.USE_AE
} else {
ECalculationMethod.USE_CE
}
append("【计算方法】: $eCalculationMethod\n")
append("【连杆长度】: ${config.linkageLengths}\n\n")
// 添加计算结果
if (success && keyPoints != null) {
append("铲斗位置计算成功:\n")
val wgs84Positions = getKeyPointsWGS84Positions()
wgs84Positions?.forEach { (name, position) ->
val (lon, lat, alt) = position
append("$name: 经度=${String.format("%.8f", lon)}, ")
append("纬度=${String.format("%.8f", lat)}, ")
append("高度=${String.format("%.3f", alt)}m\n")
}
} else {
append("铲斗位置计算失败\n")
}
append("\n")
}
// 通知所有监听器
val listenersCopy = ArrayList(listeners) // 创建副本避免并发修改
listenersCopy.forEach { listener ->
listener.onPositionCalculated(logMessage)
}
}
}

View File

@@ -0,0 +1,243 @@
package com.icegps.excavator.model
import kotlin.math.*
/**
* WGS84、EPSG3857、ENU 的坐标转换工具类
*
* @author lm
* @date 2024/8/2
*/
class GeoHelper private constructor() {
companion object {
private var sharedInstance: GeoHelper? = null
fun getSharedInstance(): GeoHelper {
return sharedInstance ?: GeoHelper().also { sharedInstance = it }
}
fun createInstance(): GeoHelper = GeoHelper()
}
// WGS-84 ellipsoid parameters
private val RADIUS = 6378137.0 // Major radius
private val RADIUS_B = 6356752.314245 // Minor radius
private val E = (RADIUS * RADIUS - RADIUS_B * RADIUS_B) / (RADIUS * RADIUS) // Eccentricity
private val HALF_SIZE = Math.PI * RADIUS // Half circumference of Earth
private val DEG2RAD = Math.PI / 180 // Degrees to radians conversion factor
private val RAD2DEG = 180 / Math.PI // Radians to degrees conversion factor
private val RE_WGS84 = 6378137.0 // Earth's equatorial radius in WGS84
private val FE_WGS84 = 1.0 / 298.257223563 // Flattening of the WGS84 ellipsoid
private var isFirstPoint = true
private val bPos = DoubleArray(3)
private var bECEF = DoubleArray(3)
private val rPos = DoubleArray(3)
private var rECEF = DoubleArray(3)
private val vECEF = DoubleArray(3)
private var useBlhToEnu: Boolean = true
private var blhToEnu: BlhToEnu = BlhToEnu()
/**
* 重置 ENU 基准点
* 调用此方法后,下一次 wgs84ToENU 调用将设置新的基准点
*/
fun resetEnuBenchmarkPoint() {
if (useBlhToEnu) {
blhToEnu.resetEnuBenchmarkPoint()
return
}
isFirstPoint = true
}
/**
* 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标
* 如果是第一个点,它将被设置为 ENU 坐标系的基准点
*
* @param lon 经度(度)
* @param lat 纬度(度)
* @param alt 高度(米)
* @return 包含 ENU 坐标的 Enu 对象
*/
fun wgs84ToENU(lon: Double, lat: Double, alt: Double): Enu {
if (useBlhToEnu) {
val enu = blhToEnu.wgs84ToEnu(lon = lon, lat = lat, height = alt)
return Enu(enu[0], enu[1], enu[2])
}
if (isFirstPoint) setEnuBenchmark(lon, lat, alt)
rPos[0] = lat * DEG2RAD
rPos[1] = lon * DEG2RAD
rPos[2] = alt
rECEF = pos2ecef(rPos)
vECEF[0] = rECEF[0] - bECEF[0]
vECEF[1] = rECEF[1] - bECEF[1]
vECEF[2] = rECEF[2] - bECEF[2]
val enuDoubleArray = ecef2enu(bPos, vECEF)
return Enu(enuDoubleArray[0], enuDoubleArray[1], enuDoubleArray[2])
}
/**
* 设置 ENU 坐标系的基准点
*
* @param lon 基准点经度(度)
* @param lat 基准点纬度(度)
* @param alt 基准点高度(米)
*/
private fun setEnuBenchmark(lon: Double, lat: Double, alt: Double) {
bPos[0] = lat * DEG2RAD
bPos[1] = lon * DEG2RAD
bPos[2] = alt
bECEF = pos2ecef(bPos)
isFirstPoint = false
}
/**
* 将 ENU 坐标转换为 WGS84 坐标
*
* @param enu 包含 ENU 坐标的 Enu 对象
* @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray
*/
fun enuToWGS84(enu: Enu): DoubleArray {
if (useBlhToEnu) {
val wgs84 = blhToEnu.enuToWgs84(e = enu.x, n = enu.y, u = enu.z)
return doubleArrayOf(wgs84[1], wgs84[0], wgs84[2])
}
val enuArray = doubleArrayOf(enu.x, enu.y, enu.z)
val enuToEcefMatrix = xyz2enu(bPos)
val ecefArray = matmul(charArrayOf('T', 'N'), 3, 1, 3, 1.0, enuToEcefMatrix, enuArray, 0.0)
vECEF[0] = bECEF[0] + ecefArray[0]
vECEF[1] = bECEF[1] + ecefArray[1]
vECEF[2] = bECEF[2] + ecefArray[2]
return ecef2pos(vECEF)
}
/**
* 将 WGS84 坐标转换为 EPSG3857 坐标
*
* @param lon 经度(度)
* @param lat 纬度(度)
* @return 包含 EPSG3857 坐标的 EPSG3857 对象
*/
fun wgs84ToEPSG3857(lon: Double, lat: Double): EPSG3857 {
val x = lon * HALF_SIZE / 180
var y = RADIUS * ln(tan(Math.PI * (lat + 90) / 360))
y = y.coerceIn(-HALF_SIZE, HALF_SIZE)
return EPSG3857(x, y)
}
/**
* 将 EPSG3857 坐标转换为 WGS84 坐标
*
* @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象
* @return 包含 WGS84 坐标 {经度, 纬度} 的 DoubleArray
*/
fun epsg3857ToWGS84(epsg3857: EPSG3857): DoubleArray {
val lon = (epsg3857.x / HALF_SIZE) * 180.0
val lat = (2 * atan(exp(epsg3857.y / RADIUS)) - Math.PI / 2) * RAD2DEG
return doubleArrayOf(lon, lat)
}
private fun pos2ecef(pos: DoubleArray): DoubleArray {
val (lat, lon, alt) = pos
val sinp = sin(lat)
val cosp = cos(lat)
val sin_l = sin(lon)
val cos_l = cos(lon)
val e2 = FE_WGS84 * (2.0 - FE_WGS84)
val v = RE_WGS84 / sqrt(1.0 - e2 * sinp * sinp)
return doubleArrayOf(
(v + alt) * cosp * cos_l,
(v + alt) * cosp * sin_l,
(v * (1.0 - e2) + alt) * sinp
)
}
private fun ecef2enu(pos: DoubleArray, r: DoubleArray): DoubleArray {
val E = xyz2enu(pos)
return matmul(charArrayOf('N', 'N'), 3, 1, 3, 1.0, E, r, 0.0)
}
private fun matmul(
tr: CharArray,
n: Int,
k: Int,
m: Int,
alpha: Double,
A: DoubleArray,
B: DoubleArray,
beta: Double
): DoubleArray {
val f = when {
tr[0] == 'N' && tr[1] == 'N' -> 1
tr[0] == 'N' && tr[1] == 'T' -> 2
tr[0] == 'T' && tr[1] == 'N' -> 3
else -> 4
}
val C = DoubleArray(n * k)
for (i in 0 until n) {
for (j in 0 until k) {
var d = 0.0
when (f) {
1 -> for (x in 0 until m) d += A[i + x * n] * B[x + j * m]
2 -> for (x in 0 until m) d += A[i + x * n] * B[j + x * k]
3 -> for (x in 0 until m) d += A[x + i * m] * B[x + j * m]
4 -> for (x in 0 until m) d += A[x + i * m] * B[j + x * k]
}
C[i + j * n] = alpha * d + beta * C[i + j * n]
}
}
return C
}
private fun xyz2enu(pos: DoubleArray): DoubleArray {
val (lat, lon) = pos
val sinp = sin(lat)
val cosp = cos(lat)
val sin_l = sin(lon)
val cos_l = cos(lon)
return doubleArrayOf(
-sin_l, cos_l, 0.0,
-sinp * cos_l, -sinp * sin_l, cosp,
cosp * cos_l, cosp * sin_l, sinp
)
}
private fun ecef2pos(ecef: DoubleArray): DoubleArray {
val (x, y, z) = ecef
val a = RE_WGS84
val b = a * (1 - FE_WGS84)
val e2 = (a * a - b * b) / (a * a)
val e2p = (a * a - b * b) / (b * b)
val r2 = x * x + y * y
val r = sqrt(r2)
val E2 = a * a - b * b
val F = 54 * b * b * z * z
val G = r2 + (1 - e2) * z * z - e2 * E2
val c = (e2 * e2 * F * r2) / (G * G * G)
val s = cbrt(1 + c + sqrt(c * c + 2 * c))
val P = F / (3 * (s + 1 / s + 1) * (s + 1 / s + 1) * G * G)
val Q = sqrt(1 + 2 * e2 * e2 * P)
val r0 =
-(P * e2 * r) / (1 + Q) + sqrt(0.5 * a * a * (1 + 1.0 / Q) - P * (1 - e2) * z * z / (Q * (1 + Q)) - 0.5 * P * r2)
val U = sqrt((r - e2 * r0) * (r - e2 * r0) + z * z)
val V = sqrt((r - e2 * r0) * (r - e2 * r0) + (1 - e2) * z * z)
val Z0 = b * b * z / (a * V)
val lon = atan2(y, x) * RAD2DEG
val lat = atan((z + e2p * Z0) / r) * RAD2DEG
val alt = U * (1 - b * b / (a * V))
return doubleArrayOf(lon, lat, alt)
}
data class EPSG3857(var x: Double = 0.0, var y: Double = 0.0)
data class Enu(var x: Double = 0.0, var y: Double = 0.0, var z: Double = 0.0)
}

View File

@@ -0,0 +1,56 @@
package com.icegps.excavator.model
/**
* @author xjh
* @date 2025/2/25
*/
/**
* 数据类存储RTK位置信息和姿态信息包括航向角、俯仰角、滚转角
*/
data class RTKData(
val latitude: Double = 0.0, // 纬度,单位:度
val longitude: Double = 0.0, // 经度,单位:度
val altitude: Double = 0.0, // 高度,单位:米
val fixQuality: Int = 0, // 定位质量0=未定1=单点2=伪距4=固定5=浮动
val satellites: Int = 0, // 使用的卫星数量
val hdop: Double = 0.0, // 水平精度因子
val time: String = "", // 时间戳(用于所有类型)
val messageType: String = "GNGGA", // 消息类型GNGGA、GNGGAH或GNHPR
val yaw: Double = 0.0, // 航向角,单位:度
val pitch: Double = 0.0, // 俯仰角,单位:度
val roll: Double = 0.0 // 滚转角,单位:度
) {
/**
* 检查RTK是否固定
*/
fun isRtkFixed(): Boolean = fixQuality == 4
/**
* 检查是否为主天线数据
*/
fun isMainAntenna(): Boolean = messageType == "GNGGA"
/**
* 检查是否为副天线数据
*/
fun isSubAntenna(): Boolean = messageType == "GNGGAH"
/**
* 检查是否为姿态数据(如航向、俯仰、滚转)
*/
fun isAttitudeData(): Boolean = messageType == "GNHPR"
/**
* 是否为有效的位置数据(主或副天线)
*/
fun hasPositionData(): Boolean = latitude != 0.0 && longitude != 0.0 && altitude != 0.0
override fun toString(): String {
return if (isAttitudeData()) {
"RTKData($messageType: heading=$yaw, pitch=$pitch, roll=$roll, time=$time)"
} else {
"RTKData($messageType: lat=$latitude, lon=$longitude, alt=$altitude, fix=$fixQuality, sats=$satellites, hdop=$hdop, time=$time)"
}
}
}