initial commit

This commit is contained in:
2025-11-13 18:31:10 +08:00
commit da58415989
137 changed files with 10167 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/.kotlin

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

49
app/build.gradle Normal file
View File

@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace 'com.icegps.geotools'
compileSdk 36
defaultConfig {
applicationId "com.icegps.geotools"
minSdk 28
targetSdk 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '11'
}
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material
implementation 'com.mapbox.maps:android-ndk27:11.16.2'
implementation libs.androidx.activity
implementation libs.androidx.constraintlayout
implementation project(':delaunator')
implementation project(':math')
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.icegps.geotools
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.icegps.geotools", appContext.packageName)
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Include this permission to grab user's general location -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Include only if your app benefits from precise location access. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Geotools"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,274 @@
package com.icegps.common.helper
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
/**
* 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 getOriginLat(): Double = originLat
fun getOriginLon(): Double = originLon
fun getOriginHeight(): Double = originHeight
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 height 高度
* @return ECEF 坐标 [x, y, z]
*/
private fun blh2ecef(lat: Double, lon: Double, height: Double): DoubleArray {
val n = { x: Double -> A / sqrt(1.0 - E2 * sin(x * PI_180).pow(2)) }
val x = (n(lat) + height) * cos(lat * PI_180) * cos(lon * PI_180)
val y = (n(lat) + height) * cos(lat * PI_180) * sin(lon * PI_180)
val z = (n(lat) * (1.0 - E2) + height) * 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,419 @@
package com.icegps.common.helper
import android.os.Parcel
import android.os.Parcelable
import kotlin.math.atan
import kotlin.math.atan2
import kotlin.math.cbrt
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.tan
/**
* WGS84、EPSG3857、ENU 的坐标转换工具类
*
* @author lm
* @date 2024/8/2
*/
class GeoHelper private constructor() {
companion object {
private var sharedInstance: GeoHelper? = null
fun getSharedInstance(): GeoHelper = 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 var firstPoint = DoubleArray(3)
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 = true
private var blhToEnu = BlhToEnu()
/**
* 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标
* 如果是第一个点,它将被设置为 ENU 坐标系的基准点
*
* @param lon 经度(度)
* @param lat 纬度(度)
* @param hgt 高度(米)
* @return 包含 ENU 坐标的 Enu 对象
*/
fun wgs84ToENU(lon: Double, lat: Double, hgt: Double): ENU {
if (useBlhToEnu) {
val enu = blhToEnu.wgs84ToEnu(lon = lon, lat = lat, height = hgt)
return ENU(enu[0], enu[1], enu[2])
}
if (isFirstPoint) setEnuBenchmark(lon, lat, hgt)
rPos[0] = lat * DEG2RAD
rPos[1] = lon * DEG2RAD
rPos[2] = hgt
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])
}
/**
* 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标
* 如果是第一个点,它将被设置为 ENU 坐标系的基准点
*
* @param wgs84 WGS84 坐标对象
* @return 包含 ENU 坐标的 Enu 对象
*/
fun wgs84ObjectToENU(wgs84: WGS84): ENU = wgs84ToENU(wgs84.lon, wgs84.lat, wgs84.hgt)
/**
* 是否已设置 ENU 坐标系的基准点
*/
fun isEnuBenchmarkSet(): Boolean = !isFirstPoint
/**
* 设置 ENU 坐标系的基准点
*
* @param lon 基准点经度(度)
* @param lat 基准点纬度(度)
* @param hgt 基准点高度(米)
*/
private fun setEnuBenchmark(lon: Double, lat: Double, hgt: Double) {
firstPoint = doubleArrayOf(lon, lat, hgt)
bPos[0] = lat * DEG2RAD
bPos[1] = lon * DEG2RAD
bPos[2] = hgt
bECEF = pos2ecef(bPos)
isFirstPoint = false
}
/**
* 获取 ENU 坐标系的基准点
*
* @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray
*/
fun getEnuBenchmarkPoint(): DoubleArray {
if (useBlhToEnu) {
return doubleArrayOf(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight())
}
return firstPoint
}
/**
* 获取 ENU 坐标系的基准点
*
* @return 包含 WGS84 坐标的 WGS84 对象
*/
fun getEnuBenchmarkPointAsWGS84(): WGS84 {
if (useBlhToEnu) {
return WGS84(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight())
}
return WGS84(firstPoint[0], firstPoint[1], firstPoint[2])
}
/**
* 重置 ENU 基准点
* 调用此方法后,下一次 wgs84ToENU 调用将设置新的基准点
*/
fun resetEnuBenchmarkPoint() {
if (useBlhToEnu) {
blhToEnu.resetEnuBenchmarkPoint()
return
}
isFirstPoint = true
}
/**
* 将 ENU (East-North-Up) 坐标转换为 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)
}
/**
* 将 ENU (East-North-Up) 坐标转换为 WGS84 坐标
*
* @param enu 包含 ENU 坐标的 Enu 对象
* @return 包含 WGS84 坐标的 WGS84 对象
*/
fun enuToWGS84Object(enu: ENU): WGS84 {
val wgs84Array = enuToWGS84(enu)
return WGS84(wgs84Array[0], wgs84Array[1], wgs84Array[2])
}
/**
* 将 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)
}
/**
* 将 WGS84 坐标转换为 EPSG3857 坐标
*
* @param wgs84 WGS84 坐标对象
* @return 包含 EPSG3857 坐标的 EPSG3857 对象
*/
fun wgs84ObjectToEPSG3857(wgs84: WGS84): EPSG3857 = wgs84ToEPSG3857(wgs84.lon, wgs84.lat)
/**
* 将 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)
}
/**
* 将 EPSG3857 坐标转换为 WGS84 坐标
*
* @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象
* @return 包含 WGS84 坐标的 WGS84 对象
*/
fun epsg3857ToWGS84Object(epsg3857: EPSG3857): WGS84 {
val wgs84Array = epsg3857ToWGS84(epsg3857)
return WGS84(wgs84Array[0], wgs84Array[1], 0.0)
}
fun pos2ecef(pos: DoubleArray): DoubleArray {
val (lat, lon, hgt) = 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 + hgt) * cosp * cos_l,
(v + hgt) * cosp * sin_l,
(v * (1.0 - e2) + hgt) * sinp
)
}
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)
}
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
}
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
)
}
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 hgt = U * (1 - b * b / (a * V))
return doubleArrayOf(lon, lat, hgt)
}
data class WGS84(var lon: Double = 0.0, var lat: Double = 0.0, var hgt: Double = 0.0) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readDouble(),
parcel.readDouble(),
parcel.readDouble()
)
constructor(wgs84: DoubleArray) : this(
lon = wgs84.getOrElse(0) { 0.0 },
lat = wgs84.getOrElse(1) { 0.0 },
hgt = wgs84.getOrElse(2) { 0.0 }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeDouble(lon)
parcel.writeDouble(lat)
parcel.writeDouble(hgt)
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<WGS84> {
override fun createFromParcel(parcel: Parcel): WGS84 {
return WGS84(parcel)
}
override fun newArray(size: Int): Array<WGS84?> {
return arrayOfNulls(size)
}
}
override fun toString(): String {
return "WGS84(lon=$lon, lat=$lat, hgt=$hgt)"
}
}
data class EPSG3857(var x: Double = 0.0, var y: Double = 0.0) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readDouble(),
parcel.readDouble()
)
constructor(epsG3857: DoubleArray) : this(
x = epsG3857.getOrElse(0) { 0.0 },
y = epsG3857.getOrElse(1) { 0.0 }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeDouble(x)
parcel.writeDouble(y)
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<EPSG3857> {
override fun createFromParcel(parcel: Parcel): EPSG3857 {
return EPSG3857(parcel)
}
override fun newArray(size: Int): Array<EPSG3857?> {
return arrayOfNulls(size)
}
}
override fun toString(): String {
return "EPSG3857(x=$x, y=$y)"
}
}
data class ENU(var x: Double = 0.0, var y: Double = 0.0, var z: Double = 0.0) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readDouble(),
parcel.readDouble(),
parcel.readDouble()
)
constructor(enu: DoubleArray) : this(
x = enu.getOrElse(0) { 0.0 },
y = enu.getOrElse(1) { 0.0 },
z = enu.getOrElse(2) { 0.0 }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeDouble(x)
parcel.writeDouble(y)
parcel.writeDouble(z)
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<ENU> {
override fun createFromParcel(parcel: Parcel): ENU {
return ENU(parcel)
}
override fun newArray(size: Int): Array<ENU?> {
return arrayOfNulls(size)
}
}
override fun toString(): String {
return "ENU(x=$x, y=$y, z=$z)"
}
}
}

View File

@@ -0,0 +1,83 @@
package com.icegps.geotools
import com.icegps.geotools.model.IPoint
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
/**
* @author tabidachinokaze
* @date 2025/11/5
*/
object GeoJsonUtils {
// 生成三角形PolygonFeatureCollection
fun <T : IPoint> trianglesToPolygons(delaunator: Delaunator<T>): FeatureCollection {
val features = mutableListOf<Feature>()
val tris = delaunator.triangles
// triangles 是按 3 个索引为一组三角形存储
var i = 0
while (i <= tris.lastIndex) {
val a = tris[i]
val b = tris[i + 1]
val c = tris[i + 2]
val pa = delaunator.points[a]
val pb = delaunator.points[b]
val pc = delaunator.points[c]
// Polygon 要求外环首尾闭合
val ring = listOf(
Point.fromLngLat(pa.x, pa.y),
Point.fromLngLat(pb.x, pb.y),
Point.fromLngLat(pc.x, pc.y),
Point.fromLngLat(pa.x, pa.y)
)
val polygon = Polygon.fromLngLats(listOf(ring))
val feature = Feature.fromGeometry(polygon)
// 可把三角形的顶点 id/索引用作属性,方便后续交互
feature.addNumberProperty("i0", a)
feature.addNumberProperty("i1", b)
feature.addNumberProperty("i2", c)
features.add(feature)
i += 3
}
return FeatureCollection.fromFeatures(features)
}
// 生成边LineStringFeatureCollection每条边只输出一次
fun <T : IPoint> trianglesToUniqueEdges(delaunator: Delaunator<T>): FeatureCollection {
val features = mutableListOf<Feature>()
val seen = HashSet<Pair<Int, Int>>()
val tris = delaunator.triangles
var i = 0
while (i <= tris.lastIndex) {
val a = tris[i]
val b = tris[i + 1]
val c = tris[i + 2]
val edges = listOf(Pair(a, b), Pair(b, c), Pair(c, a))
for ((p, q) in edges) {
// 归一化顺序避免重复p,qq,p
val key = if (p <= q) Pair(p, q) else Pair(q, p)
if (seen.add(key)) {
val pp = delaunator.points[p]
val qq = delaunator.points[q]
val line = LineString.fromLngLats(
listOf(
Point.fromLngLat(pp.x, pp.y),
Point.fromLngLat(qq.x, qq.y)
)
)
val feature = Feature.fromGeometry(line)
feature.addNumberProperty("p0", p)
feature.addNumberProperty("p1", q)
features.add(feature)
}
}
i += 3
}
return FeatureCollection.fromFeatures(features)
}
}

View File

@@ -0,0 +1,529 @@
package com.icegps.geotools
/**
* @author tabidachinokaze
* @date 2025/11/13
*/
// Imports根据你项目调整
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import androidx.core.graphics.toColorInt
import com.icegps.geotools.model.DPoint
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.geojson.Polygon
import com.mapbox.maps.MapView
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.rasterLayer
import com.mapbox.maps.extension.style.layers.properties.generated.Visibility
import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.ImageSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import com.mapbox.maps.extension.style.sources.generated.imageSource
import com.mapbox.maps.extension.style.sources.getSourceAs
import com.mapbox.maps.extension.style.sources.updateImage
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.atan
import kotlin.math.ceil
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.min
import kotlin.math.tan
// -----------------------------
// Helper: WebMercator projection (EPSG:3857)
// -----------------------------
fun lonToMercX(lon: Double): Double = lon * 20037508.34 / 180.0
fun latToMercY(lat: Double): Double {
val y = ln(tan((90.0 + lat) * PI / 360.0)) / (PI / 180.0)
return y * 20037508.34 / 180.0
}
fun mercXToLon(x: Double): Double = x * 180.0 / 20037508.34
fun mercYToLat(y: Double): Double {
val v = y * 180.0 / 20037508.34
return 180.0 / PI * (2.0 * atan(exp(v * PI / 180.0)) - PI / 2.0)
}
// -----------------------------
// Geometry helpers
// -----------------------------
data class Vec2(val x: Double, val y: Double)
/** 点是否在三角形内(在 mercator 坐标系中) — 使用重心 / 矩阵法 */
fun pointInTriangle(pt: Vec2, a: Vec2, b: Vec2, c: Vec2): Boolean {
val v0x = c.x - a.x
val v0y = c.y - a.y
val v1x = b.x - a.x
val v1y = b.y - a.y
val v2x = pt.x - a.x
val v2y = pt.y - a.y
val dot00 = v0x * v0x + v0y * v0y
val dot01 = v0x * v1x + v0y * v1y
val dot02 = v0x * v2x + v0y * v2y
val dot11 = v1x * v1x + v1y * v1y
val dot12 = v1x * v2x + v1y * v2y
val denom = dot00 * dot11 - dot01 * dot01
if (denom == 0.0) return false
val invDenom = 1.0 / denom
val u = (dot11 * dot02 - dot01 * dot12) * invDenom
val v = (dot00 * dot12 - dot01 * dot02) * invDenom
return u >= 0 && v >= 0 && u + v <= 1
}
/** 可选:用三角形顶点值做双线性/重心内插(这里示例:按顶点值插值)
* valueAtVerts: DoubleArray of length 3 for the triangle's vertex values
*/
fun barycentricInterpolate(pt: Vec2, a: Vec2, b: Vec2, c: Vec2, values: DoubleArray): Double {
// compute areas (using cross product) as barycentric weights
val area = { p1: Vec2, p2: Vec2, p3: Vec2 ->
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
}
val areaTotal = area(a, b, c)
if (areaTotal == 0.0) return values[0]
val wA = area(pt, b, c) / areaTotal
val wB = area(pt, c, a) / areaTotal
val wC = area(pt, a, b) / areaTotal
return values[0] * wA + values[1] * wB + values[2] * wC
}
// -----------------------------
// 主函数:把 Delaunay 转成规则栅格(格子中心采样)
// -----------------------------
data class GridCell(val row: Int, val col: Int, val centerLon: Double, val centerLat: Double, var value: Double? = null)
data class GridModel(
val minLon: Double, val minLat: Double,
val maxLon: Double, val maxLat: Double,
val rows: Int,
val cols: Int,
val cellSizeMeters: Double,
val cells: Array<Double?> // length rows*cols, row-major: idx = r*cols + c
)
fun triangulationToGrid(
delaunator: Delaunator<DPoint>,
cellSizeMeters: Double = 50.0, // 每个格子的边长(米)
maxSidePixels: Int = 5000 // 限制 max rows/cols 防止 OOM可选
): GridModel {
val pts = delaunator.points
require(pts.isNotEmpty()) { "points empty" }
// 1) bbox in lon/lat
var minLon = Double.POSITIVE_INFINITY
var maxLon = Double.NEGATIVE_INFINITY
var minLat = Double.POSITIVE_INFINITY
var maxLat = Double.NEGATIVE_INFINITY
for (p in pts) {
if (p.x < minLon) minLon = p.x
if (p.x > maxLon) maxLon = p.x
if (p.y < minLat) minLat = p.y
if (p.y > maxLat) maxLat = p.y
}
if (minLon == maxLon) {
minLon -= 0.0001; maxLon += 0.0001
}
if (minLat == maxLat) {
minLat -= 0.0001; maxLat += 0.0001
}
// 2) 转为 mercator
val minX = lonToMercX(minLon)
val maxX = lonToMercX(maxLon)
val minY = latToMercY(minLat)
val maxY = latToMercY(maxLat)
val widthMeters = maxX - minX
val heightMeters = maxY - minY
// rows/cols
var cols = ceil(widthMeters / cellSizeMeters).toInt()
var rows = ceil(heightMeters / cellSizeMeters).toInt()
// 防止过大
if (cols > maxSidePixels) cols = maxSidePixels
if (rows > maxSidePixels) rows = maxSidePixels
// prepare output array
val cells = Array<Double?>(rows * cols) { null }
// 准备点/三角形在 mercator 下的缓存坐标
val mercPts = pts.map { p -> Vec2(lonToMercX(p.x), latToMercY(p.y)) }
// triangles 数组(每 3 个为一组)
val triIdx = delaunator.triangles
val triCount = triIdx.size / 3
// For potential vertex values: if you have scalar per vertex, prepare here.
// Example: create placeholder values (e.g., 0.0). Replace with your actual values if available.
val vertexValues = DoubleArray(pts.size) { 0.0 }
// 3) iterate triangles and rasterize onto grid by checking the grid cells that intersect triangle bbox
for (ti in 0 until triCount) {
val i0 = triIdx[3 * ti]
val i1 = triIdx[3 * ti + 1]
val i2 = triIdx[3 * ti + 2]
val a = mercPts[i0]
val b = mercPts[i1]
val c = mercPts[i2]
// triangle bbox in mercator
val tminX = minOf(a.x, b.x, c.x)
val tmaxX = maxOf(a.x, b.x, c.x)
val tminY = minOf(a.y, b.y, c.y)
val tmaxY = maxOf(a.y, b.y, c.y)
// convert bbox to grid indices (clamp)
val colMin = ((tminX - minX) / cellSizeMeters).toInt().coerceIn(0, cols - 1)
val colMax = ((tmaxX - minX) / cellSizeMeters).toInt().coerceIn(0, cols - 1)
val rowMin = ((maxY - tmaxY) / cellSizeMeters).toInt().coerceIn(0, rows - 1) // 注意 Y 方向
val rowMax = ((maxY - tminY) / cellSizeMeters).toInt().coerceIn(0, rows - 1)
// optional: get vertex values for interpolation
val triVertexVals = doubleArrayOf(vertexValues[i0], vertexValues[i1], vertexValues[i2])
for (r in rowMin..rowMax) {
for (cIdx in colMin..colMax) {
// center of this cell in mercator
val centerX = minX + (cIdx + 0.5) * cellSizeMeters
val centerY = maxY - (r + 0.5) * cellSizeMeters
val pt = Vec2(centerX, centerY)
if (pointInTriangle(pt, a, b, c)) {
// example: set cell value as triangle index, or do interpolation
// cells index:
val idx = r * cols + cIdx
// choose value: triangle index -> convert to Double
cells[idx] = ti.toDouble()
// OR for interpolation:
// val valInterp = barycentricInterpolate(pt, a, b, c, triVertexVals)
// cells[idx] = valInterp
}
}
}
}
// 4) produce GridModel and also convert bbox back to lon/lat for metadata
val grid = GridModel(
minLon = minLon,
minLat = minLat,
maxLon = maxLon,
maxLat = maxLat,
rows = rows,
cols = cols,
cellSizeMeters = cellSizeMeters,
cells = cells
)
return grid
}
// -----------------------------
// 显示:把 GridModel 渲染成 Bitmap 并用 ImageSource 显示在 Mapbox推荐
// -----------------------------
fun MapView.displayGridAsImageSource(
grid: GridModel,
testSourceId: String,
testLayerId: String,
palette: (Double?) -> Int = { v -> // 默认配色:基于三角形索引取色
if (v == null) Color.TRANSPARENT
else {
val idx = v.toInt()
val r = (50 + (idx * 37) % 200)
val g = (80 + (idx * 61) % 150)
val b = (100 + (idx * 47) % 120)
Color.argb(220, r, g, b)
}
}
) {
mapboxMap.getStyle { style ->
val cols = grid.cols
val rows = grid.rows
// 限制渲染大小防 OOM可按需缩放
val maxDim = 2048
val width = cols.coerceAtMost(maxDim)
val height = rows.coerceAtMost(maxDim)
// 如果 rows/cols 超过 maxDim我们在渲染时按比例抽样nearest neighbor
val sampleX = cols.toDouble() / width.toDouble()
val sampleY = rows.toDouble() / height.toDouble()
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.TRANSPARENT)
val paint = Paint().apply {
this.style = Paint.Style.FILL
}
for (y in 0 until height) {
val srcRow = min((y * sampleY).toInt(), rows - 1)
for (x in 0 until width) {
val srcCol = min((x * sampleX).toInt(), cols - 1)
val valCell = grid.cells[srcRow * cols + srcCol]
paint.color = palette(valCell)
// draw a single pixel rect scaled to bitmap coordinates
canvas.drawRect(RectF(x.toFloat(), y.toFloat(), (x + 1).toFloat(), (y + 1).toFloat()), paint)
}
}
// image source coords: grid bbox four corners (lon/lat)
val topLeft = listOf(grid.minLon, grid.maxLat)
val topRight = listOf(grid.maxLon, grid.maxLat)
val bottomRight = listOf(grid.maxLon, grid.minLat)
val bottomLeft = listOf(grid.minLon, grid.minLat)
val coords = listOf(topLeft, topRight, bottomRight, bottomLeft)
if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId)
val imgSource = imageSource(testSourceId) {
coordinates(coords)
}
style.addSource(imgSource)
// remove old layer if present
try {
style.removeStyleLayer(testLayerId)
} catch (_: Exception) {
}
val rasterLayer = rasterLayer(testLayerId, testSourceId) {
visibility(Visibility.VISIBLE)
}
style.addLayer(rasterLayer)
// set image (updateImage or setImage)
val src = style.getSourceAs<ImageSource>(testSourceId)
src?.updateImage(bmp) // 若你的 SDK 使用 setImage(bitmap) -> 改为 setImage
}
}
fun MapView.displayGridAsImageSourceHighRes(
grid: GridModel,
testSourceId: String,
testLayerId: String,
maxSidePx: Int = 4096, // 根据内存调整,越大越清晰但越耗内存
palette: (Double?) -> Int = { v ->
if (v == null) Color.TRANSPARENT else {
val idx = v.toInt()
Color.argb(220, (50 + (idx * 37) % 205), (80 + (idx * 61) % 175), (100 + (idx * 97) % 155))
}
}
) {
mapboxMap.getStyle { style ->
val cols = grid.cols
val rows = grid.rows
if (cols <= 0 || rows <= 0) return@getStyle
// 设备像素比(用于在高 DPI 设备上生成更清晰的位图)
val density = context.resources.displayMetrics.density // e.g. 3.0 for xxhdpi
// target bitmap size: 尽量把每个格子映射为至少 1 device-pixel
var targetW = (cols * density).toInt()
var targetH = (rows * density).toInt()
// 限制最大边长,防止 OOM
val scaleDown = max(1.0, max(targetW.toDouble() / maxSidePx, targetH.toDouble() / maxSidePx))
if (scaleDown > 1.0) {
targetW = (targetW / scaleDown).toInt().coerceAtLeast(1)
targetH = (targetH / scaleDown).toInt().coerceAtLeast(1)
}
// 如果 target 更小于 cols/rows说明被压缩我们先绘制到原始 colsxrows 的 bitmap 再用最近邻缩放到 target。
// 但为了避免一次性分配超大内存,这里用两步策略:
val srcW = cols.coerceAtMost(maxSidePx) // 防止超大
val srcH = rows.coerceAtMost(maxSidePx)
// 创建源位图(每格一个像素的近似表示)
val srcBmp = Bitmap.createBitmap(srcW, srcH, Bitmap.Config.ARGB_8888)
val canvas = Canvas(srcBmp)
canvas.drawColor(Color.TRANSPARENT)
val paint = Paint().apply {
this.style = Paint.Style.FILL
isAntiAlias = false // 关闭抗锯齿
isFilterBitmap = false // 关键:绘制/缩放时不使用双线性滤波
}
// 采样比(如果原 grid 比 src 大,则以 nearest-neighbor 采样)
val sampleX = cols.toDouble() / srcW.toDouble()
val sampleY = rows.toDouble() / srcH.toDouble()
for (y in 0 until srcH) {
val srcRow = min((y * sampleY).toInt(), grid.rows - 1)
for (x in 0 until srcW) {
val srcCol = min((x * sampleX).toInt(), grid.cols - 1)
val v = grid.cells[srcRow * grid.cols + srcCol]
paint.color = palette(v)
// draw 1x1 rect == set single pixel
canvas.drawRect(x.toFloat(), y.toFloat(), (x + 1).toFloat(), (y + 1).toFloat(), paint)
}
}
// 再把 srcBmp 最近邻缩放到 targetW x targetHfilter=false => nearest neighbor
val finalBmp = if (srcW == targetW && srcH == targetH) {
srcBmp
} else {
Bitmap.createScaledBitmap(srcBmp, targetW, targetH, /*filter=*/ false)
}
// 清理 srcBmp若不再需要
if (finalBmp !== srcBmp) {
srcBmp.recycle()
}
// 把 finalBmp 传给 mapboximageSource coords 与之前一致)
val topLeft = listOf(grid.minLon, grid.maxLat)
val topRight = listOf(grid.maxLon, grid.maxLat)
val bottomRight = listOf(grid.maxLon, grid.minLat)
val bottomLeft = listOf(grid.minLon, grid.minLat)
val coords = listOf(topLeft, topRight, bottomRight, bottomLeft)
if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId)
val imgSource = imageSource(testSourceId) { coordinates(coords) }
style.addSource(imgSource)
try {
style.removeStyleLayer(testLayerId)
} catch (_: Exception) {
}
val rasterLayer = rasterLayer(testLayerId, testSourceId) { visibility(Visibility.VISIBLE) }
style.addLayer(rasterLayer)
val src = style.getSourceAs<ImageSource>(testSourceId)
src?.updateImage(finalBmp) // 或 setImage(finalBmp) 视 SDK 而定
}
}
// -----------------------------
// 可选:把每个格子做成 GeoJSON Polygons每格一个 Fill并显示交互式但格子多时非常慢
// -----------------------------
fun MapView.displayGridAsGeoJsonPolygons(
grid: GridModel,
testSourceId: String,
testLayerId: String,
palette: (Double?) -> String = { v ->
if (v == null) "#00000000" else {
val idx = v.toInt()
// 生成 hex color 例如 #RRGGBB
String.format("#%02X%02X%02X", (50 + (idx * 37) % 200), (80 + (idx * 61) % 150), (100 + (idx * 47) % 120))
}
}
) {
mapboxMap.getStyle { style ->
val features = mutableListOf<Feature>()
val minX = lonToMercX(grid.minLon)
val maxY = latToMercY(grid.maxLat)
val cellMeters = grid.cellSizeMeters
for (r in 0 until grid.rows) {
for (c in 0 until grid.cols) {
val idx = r * grid.cols + c
val v = grid.cells[idx] ?: continue
// compute four corners in mercator
val x0 = minX + c * cellMeters
val y0 = maxY - r * cellMeters
val x1 = x0 + cellMeters
val y1 = y0 - cellMeters
// to lon/lat
val lon0 = mercXToLon(x0);
val lat0 = mercYToLat(y0)
val lon1 = mercXToLon(x1);
val lat1 = mercYToLat(y1)
val ring = listOf(
Point.fromLngLat(lon0, lat0),
Point.fromLngLat(lon1, lat0),
Point.fromLngLat(lon1, lat1),
Point.fromLngLat(lon0, lat1),
Point.fromLngLat(lon0, lat0)
)
val poly = Polygon.fromLngLats(listOf(ring))
val f = Feature.fromGeometry(poly)
f.addStringProperty("color", palette(v))
f.addNumberProperty("value", v)
features.add(f)
}
}
val fc = FeatureCollection.fromFeatures(features)
if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId)
style.addSource(geoJsonSource(testSourceId) { featureCollection(fc) })
try {
style.removeStyleLayer(testLayerId)
} catch (_: Exception) {
}
val fill = FillLayer(testLayerId, testSourceId).apply {
// 使用 property 作为颜色表达式会更灵活;这里示例直接 constant color
fillColor(Expression.toColor(Expression.get("color")))
fillOpacity(0.8)
// Note: For property-driven color you'd use expressions; kept simple here.
}
style.addLayer(fill)
}
}
fun MapView.displayGridAsGeoJsonWithHeight(
grid: GridModel,
testSourceId: String,
testLayerId: String,
heightToColor: (Double) -> Int
) {
mapboxMap.getStyle { style ->
val features = mutableListOf<Feature>()
// 计算每格经纬度跨度
val deltaLon = (grid.maxLon - grid.minLon) / grid.cols
val deltaLat = (grid.maxLat - grid.minLat) / grid.rows
for (r in 0 until grid.rows) {
for (c in 0 until grid.cols) {
val z = grid.cells[r * grid.cols + c] ?: continue
val lon0 = grid.minLon + c * deltaLon
val lon1 = grid.minLon + (c + 1) * deltaLon
val lat0 = grid.maxLat - r * deltaLat
val lat1 = grid.maxLat - (r + 1) * deltaLat
val ring = listOf(
Point.fromLngLat(lon0, lat0),
Point.fromLngLat(lon1, lat0),
Point.fromLngLat(lon1, lat1),
Point.fromLngLat(lon0, lat1),
Point.fromLngLat(lon0, lat0)
)
val poly = Polygon.fromLngLats(listOf(ring))
val f = Feature.fromGeometry(poly)
// 添加高度属性
f.addNumberProperty("value", z)
// 根据回调生成颜色
val colorInt = heightToColor(z)
val colorStr = String.format("#%08X", colorInt)
f.addStringProperty("color", colorStr)
features.add(f)
}
}
val fc = FeatureCollection.fromFeatures(features)
// 添加或更新 GeoJSON Source
if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId)
style.addSource(geoJsonSource(testSourceId) { featureCollection(fc) })
// 创建 FillLayer 并使用 feature.color
try { style.removeStyleLayer(testLayerId) } catch (_: Exception) {}
val fillLayer = FillLayer(testLayerId, testSourceId).apply {
fillColor(Expression.toColor(Expression.get("color")))
fillOpacity(0.9)
}
style.addLayer(fillLayer)
}
}

View File

@@ -0,0 +1,594 @@
package com.icegps.geotools
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.icegps.common.helper.GeoHelper
import com.icegps.geotools.ktx.TAG
import com.icegps.geotools.ktx.niceStr
import com.icegps.geotools.model.DPoint
import com.icegps.math.geometry.Vector3D
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.MultiPoint
import com.mapbox.geojson.Point
import com.mapbox.geojson.Polygon
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.expressions.generated.Expression.Companion.get
import com.mapbox.maps.extension.style.layers.addLayer
import com.mapbox.maps.extension.style.layers.addLayerBelow
import com.mapbox.maps.extension.style.layers.generated.FillLayer
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.generated.rasterLayer
import com.mapbox.maps.extension.style.layers.generated.symbolLayer
import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
import com.mapbox.maps.extension.style.layers.properties.generated.TextAnchor
import com.mapbox.maps.extension.style.layers.properties.generated.TextJustify
import com.mapbox.maps.extension.style.layers.properties.generated.Visibility
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.ImageSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import com.mapbox.maps.extension.style.sources.generated.imageSource
import com.mapbox.maps.extension.style.sources.getSourceAs
import com.mapbox.maps.extension.style.sources.updateImage
import com.mapbox.maps.extension.style.style
import kotlin.math.roundToInt
import kotlin.random.Random
class MainActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private val geoHelper = GeoHelper.getSharedInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapView = MapView(this)
setContentView(mapView)
initGeoHelper()
val pointsVector = coordinateGenerate()
val pointsLatLng = pointsVector.map {
val wgS84 = geoHelper.enuToWGS84Object(
GeoHelper.ENU(x = it.x, y = it.y, z = it.z)
)
Point.fromLngLat(wgS84.lon, wgS84.lat, wgS84.hgt)
}
Log.d(
TAG,
buildString {
appendLine("enu 坐标集合")
appendLine(pointsVector.niceStr())
}
)
/*val symbolLayer = SymbolLayer("point-layer", "point-source").apply {
this.iconImage("marker-icon")
}*/
// mapView.mapboxMap.addLayer(symbolLayer)
val delaunator = Delaunator(points = pointsLatLng.map {
DPoint(x = it.longitude(), y = it.latitude(), z = it.altitude())
})
val polygons = GeoJsonUtils.trianglesToPolygons(delaunator)
// 显示三角网
if (false) mapView.loadGeoJson(delaunator)
// val rasterLayer = RasterLayer("raster-layer", sourceId = "raster-source")
if (false) mapView.loadRaterFromTin(delaunator)
// 加载栅格网,但是没有显示出来
if (false) mapView.loadImageTest(RasterUtils.boundingBox(pointsVector.map { com.icegps.geotools.model.Point(it.x, it.y) }))
// 测试显示栅格图
// mapView.loadTestRater()
mapView.loadRasterFromResource()
val testCenter = getCenter(testCoordinates2)
// 显示三角网的各点
if (false) mapView.mapboxMap.getStyle { style ->
val geoJsonSource = GeoJsonSource.Builder(sourceId = "geojson-source")
.feature(
value = Feature.fromGeometry(
MultiPoint.fromLngLats(pointsLatLng)
)
)
// .featureCollection(polygons)
.build()
style.addSource(geoJsonSource)
// style.addLayer(FillLayer("geojson-layer", "geojson-source"))
style.addImage(
"marker-icon",
getBitmapFromDrawable(R.drawable.ic_pile_marker)
)
val symbol = symbolLayer("symbol-layer", "geojson-source") {
// filter(eq(get("geometry.type"), literal("Point")))
iconImage("marker-icon")
iconAllowOverlap(true)
iconAnchor(IconAnchor.BOTTOM)
textField(get("name")) // 显示 properties.name
textAnchor(TextAnchor.TOP)
textJustify(TextJustify.CENTER)
}
style.addLayer(symbol)
}
// 显示三角网的另一种方式
if (false) mapView.showVoronoiAsPolygons(
delaunator = delaunator,
testSourceId = "polygons-source-id-0",
testLayerId = "polygons-layer-id-0"
)
if (false) mapView.rasterizeDelaunayToMap(
delaunator = delaunator,
testSourceId = "polygons-source-id-1",
testLayerId = "polygons-layer-id-1",
)
// 显示栅格模型
val gridModel = triangulationToGrid(
delaunator = delaunator,
cellSizeMeters = 2.0,
maxSidePixels = 6553500
)
val palette: (Double?) -> Int = { v ->
if (v == null) Color.MAGENTA else { // null 显 magenta 便于确认
val idx = v.toInt()
Color.rgb((idx * 37) and 0xFF, (idx * 61) and 0xFF, (idx * 97) and 0xFF)
}
}
if (false) mapView.displayGridAsImageSourceHighRes(
grid = gridModel,
testSourceId = "raster-source-id-1",
testLayerId = "raster-layer-id-1",
)
if (true) mapView.displayGridAsGeoJsonPolygons(
grid = gridModel,
testSourceId = "raster-source-id-0",
testLayerId = "raster-layer-id-0",
)
mapView.mapboxMap.setCamera(
CameraOptions.Builder()
//.center(pointsLatLng.first())
//.center(testCenter)
.center(delaunator.points.first().let { Point.fromLngLat(it.x, it.y) })
.pitch(0.0)
.zoom(18.0)
.bearing(0.0)
.build()
)
}
}
fun initGeoHelper(base: DPoint = DPoint(114.476060, 22.771073, 30.897)) {
val geoHelper = GeoHelper.getSharedInstance()
geoHelper.wgs84ToENU(
lon = base.x,
lat = base.y,
hgt = base.z
)
}
fun coordinateGenerate(): List<Vector3D> {
val dPoints = (0..60).map {
Vector3D(x, y, z)
}
return dPoints
}
val minX = -20.0
val maxX = 20.0
val minY = -20.0
val maxY = 20.0
val minZ = -20.0
val maxZ = 20.0
val x: Double get() = Random.nextDouble(minX, maxX)
val y: Double get() = Random.nextDouble(minY, maxY)
val z: Double get() = Random.nextDouble(minZ, maxZ)
fun Context.getBitmapFromDrawable(drawableResId: Int): Bitmap {
val drawable = ContextCompat.getDrawable(this, drawableResId)!!
if (drawable is BitmapDrawable) {
// 如果本身就是位图,直接返回
return drawable.bitmap
}
// 如果是 VectorDrawable 或其他类型,需手动转 Bitmap
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth.takeIf { it > 0 } ?: 1,
drawable.intrinsicHeight.takeIf { it > 0 } ?: 1,
Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
// 在 Activity/Fragment 中
fun MapView.loadGeoJson(delaunator: Delaunator<DPoint>) {
mapboxMap.getStyle { style ->
// 1) 构建 FeatureCollection选择边或三角形
val edgesFc = GeoJsonUtils.trianglesToUniqueEdges(delaunator) // 或 trianglesToPolygons(delaunator)
// 2) 添加 GeoJsonSourceid = "triangles-source")
val source = GeoJsonSource.Builder("triangles-source")
.featureCollection(edgesFc)
.build()
style.addSource(source)
// 3) 添加 LineLayer 渲染三角网边
val line = lineLayer("triangles-line-layer", "triangles-source") {
lineWidth(1.5)
lineJoin(LineJoin.ROUND)
lineOpacity(1.0)
// 可以通过 expression 使用属性动态着色,这里演示静态颜色:
lineColor("#ff0000")
}
style.addLayer(line)
// 可选:如果你用 polygons 并想填充三角形
val fill = fillLayer("triangles-fill-layer", "triangles-source") {
fillOpacity(0.2)
fillColor("#00FF00")
}
style.addLayerBelow(fill, "triangles-line-layer")
}
}
private const val ID_IMAGE_SOURCE = "image_source-id"
private const val ID_IMAGE_LAYER = "image_layer-id"
fun MapView.loadImageTest(bbox: BoundingBox) {
this.mapboxMap.loadStyle(
style(style = Style.DARK) {
+imageSource(ID_IMAGE_SOURCE) {
val bboxCoordinates: List<List<Double>> = listOf(
listOf(bbox.minX, bbox.maxY), // top-left (lon, lat)
listOf(bbox.maxX, bbox.maxY), // top-right
listOf(bbox.maxX, bbox.minY), // bottom-right
listOf(bbox.minX, bbox.minY) // bottom-left
)
coordinates(bboxCoordinates)
}
+rasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE) {}
}
) {
context.getBitmapFromDrawable(R.drawable.ic_launcher_background).let { bitmap ->
val imageSource: ImageSource? = it.getSourceAs(ID_IMAGE_SOURCE)
imageSource?.updateImage(bitmap)
}
}
}
fun MapView.loadRaterFromTin(delaunator: Delaunator<DPoint>) {
// add ImageSource
delaunator.points
val bbox = RasterUtils.boundingBox(delaunator.points)
val (floats, bitmap) = RasterUtils.rasterizeDelaunay(
delaunator = delaunator,
minX = bbox.minX,
minY = bbox.minY,
maxX = bbox.maxX,
maxY = bbox.maxY,
cols = 10,
rows = 10,
valueGetter = { it.z }
)
val bboxCoordinates: List<List<Double>> = listOf(
listOf(minX, maxY), // top-left (lon, lat)
listOf(maxX, maxY), // top-right
listOf(maxX, minY), // bottom-right
listOf(minX, minY) // bottom-left
)
mapboxMap.getStyle { style ->
// 如果已经存在同 id 的 source/layer先移除避免重复添加
val sourceId = "raster-image-source"
val layerId = "raster-layer"
try {
if (style.styleSourceExists(sourceId)) {
style.removeStyleSource(sourceId)
}
if (style.styleLayerExists(layerId)) {
style.removeStyleLayer(layerId)
}
} catch (e: Exception) {
// ignore
}
// add ImageSource直接传入 bitmap
val imageSrc = imageSource(sourceId) {
coordinates(bboxCoordinates)
}
style.addSource(imageSrc)
// add RasterLayer 显示 image source
val raster = rasterLayer(layerId, sourceId) {
rasterOpacity(0.9)
}
style.addLayer(raster)
}
}
val testSourceId = "test-raster-image-source"
val testLayerId = "test-rater-layer"
val testCoordinates = listOf(
listOf(-80.425, 46.437),
listOf(-71.516, 46.437),
listOf(-71.516, 37.936),
listOf(-80.425, 37.936)
)
val testCoordinates2 = listOf(
listOf(-122.5233, 37.7091), // 左上
listOf(-122.3566, 37.7091), // 右上
listOf(-122.3566, 37.8120), // 右下
listOf(-122.5233, 37.8120) // 左下
)
fun getCenter(coordinates: List<List<Double>>): Point? {
val minX = coordinates.minOf { it[0] }
val maxX = coordinates.maxOf { it[0] }
val minY = coordinates.minOf { it[1] }
val maxY = coordinates.maxOf { it[1] }
val centerX = (minX + maxX) / 2
val centerY = (minY + maxY) / 2
return Point.fromLngLat(centerX, centerY)
}
fun MapView.loadTestRater() {
mapboxMap.getStyle { style ->
// 示例 4 个角点(经度, 纬度按左Top、右Top、右Bottom、左Bottom 顺序
val coords = testCoordinates2
val imageSource = imageSource(
id = testSourceId,
block = {
url("https://docs.mapbox.com/mapbox-gl-js/assets/radar.gif")
coordinates(coords)
}
)
// 添加 source如果已有同 id 的 source 会导致异常,必要时先移除)
if (style.styleSourceExists(testSourceId)) {
style.removeStyleSource(testSourceId)
}
style.addSource(imageSource)
// 创建 raster layer类型为 rastersourceId 必须和上面一致
val rasterLayer = rasterLayer(
layerId = testLayerId,
sourceId = testSourceId,
block = {
// 可选:设置初始透明度或可见性
visibility(Visibility.VISIBLE)
rasterOpacity(1.0)
}
)
// 把 layer 插入在合适位置:例如放在 "water" 之上,或放到最顶层
// 如果你不知道在哪放,先加到最顶层:
style.addLayer(rasterLayer)
// 或者 style.addLayerAbove(rasterLayer, "water") 之类的
}
}
fun MapView.loadRasterFromResource() {
mapboxMap.getStyle { style ->
val coords = testCoordinates2
// 第一步:创建空的 ImageSource仅带坐标
val imageSource = imageSource(testSourceId) {
coordinates(coords)
}
// 如果之前存在旧的同名 source移除
if (style.styleSourceExists(testSourceId)) {
style.removeStyleSource(testSourceId)
}
style.addSource(imageSource)
// 第二步:创建 raster 图层
val rasterLayer = rasterLayer(
layerId = testLayerId,
sourceId = testSourceId
) {
visibility(Visibility.VISIBLE)
}
style.addLayer(rasterLayer)
// 第三步:加载本地 Bitmap 并更新到 source
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.voroni4)
style.getSourceAs<ImageSource>(testSourceId)?.updateImage(bitmap)
}
}
@Deprecated("显示效果不对")
fun MapView.showVoronoiAsPolygons(
delaunator: Delaunator<DPoint>,
testSourceId: String,
testLayerId: String
) {
mapboxMap.getStyle { style ->
// 假定 getVoronoiCells() 返回 VoronoiCell(index, points)
val features = mutableListOf<Feature>()
for (cell in delaunator.getVoronoiCells()) {
// cell.points 是 IPoint 的列表,按顺序构成多边形
val coords = cell.points.map { Point.fromLngLat(it.x, it.y) }
// GeoJSON polygon 要求首尾点相同
val ring = coords.toMutableList()
if (ring.first() != ring.last()) ring.add(ring.first())
val polygon = Polygon.fromLngLats(listOf(ring))
val f = Feature.fromGeometry(polygon)
f.addNumberProperty("index", cell.index)
features.add(f)
}
val fc = FeatureCollection.fromFeatures(features)
if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId)
style.addSource(geoJsonSource(testSourceId) {
featureCollection(fc)
})
// 添加 fill layer
if (style.styleLayerExists(testLayerId)) style.removeStyleLayer(testLayerId)
val fillLayer = FillLayer(testLayerId, testSourceId).apply {
// 这里用表达式或常量颜色,也可按 property 着色
fillOpacity(0.7)
fillColor("#ff6600")
}
style.addLayer(fillLayer)
}
}
/**
* rasterizeDelaunayToMap:
* - delaunator: 提供三角网points + triangles
* - pixelsPerDegree: 控制生成的栅格分辨率(每经度多少像素),或直接给 width/height
*/
fun MapView.rasterizeDelaunayToMap(
delaunator: Delaunator<DPoint>,
testSourceId: String,
testLayerId: String,
pixelsPerDegree: Double = 400.0 // 可调:值越大图片越精细、越大
) {
mapboxMap.getStyle { style ->
val pts = delaunator.points
if (pts.isEmpty()) return@getStyle
// 1) 计算经纬包围箱
var minLon = Double.MAX_VALUE
var maxLon = -Double.MAX_VALUE
var minLat = Double.MAX_VALUE
var maxLat = -Double.MAX_VALUE
for (p in pts) {
if (p.x < minLon) minLon = p.x
if (p.x > maxLon) maxLon = p.x
if (p.y < minLat) minLat = p.y
if (p.y > maxLat) maxLat = p.y
}
// 防止 deg=0 的情况
if (minLon == maxLon) {
minLon -= 0.0001; maxLon += 0.0001
}
if (minLat == maxLat) {
minLat -= 0.0001; maxLat += 0.0001
}
val lonSpan = maxLon - minLon
val latSpan = maxLat - minLat
// 2) 决定 bitmap 尺寸(可以用 pixelsPerDegree 或直接固定大小)
val width = (pixelsPerDegree * lonSpan).roundToInt().coerceAtLeast(64)
val height = (pixelsPerDegree * latSpan).roundToInt().coerceAtLeast(64)
// 3) 创建 bitmap 和 canvas
val bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.TRANSPARENT)
val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.style = Paint.Style.FILL
}
val paintStroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.style = Paint.Style.STROKE
strokeWidth = 1f
color = Color.BLACK
}
// helper: 经纬 -> 像素
fun lon2x(lon: Double): Float {
val rel = (lon - minLon) / lonSpan
return (rel * (width - 1)).toFloat()
}
fun lat2y(lat: Double): Float {
// 像素 y 从上到下,纬度从上(大)到下(小);所以用 (maxLat - lat)
val rel = (maxLat - lat) / latSpan
return (rel * (height - 1)).toFloat()
}
// 4) 遍历三角形: delaunator.triangles 是索引数组(三元组按顺序)
val tris = delaunator.triangles
if (tris.size % 3 != 0) {
// 非常规情况:不处理
}
val triCount = tris.size / 3
for (i in 0 until triCount) {
val i0 = tris[3 * i]
val i1 = tris[3 * i + 1]
val i2 = tris[3 * i + 2]
val p0 = pts[i0]
val p1 = pts[i1]
val p2 = pts[i2]
val path = Path().apply {
moveTo(lon2x(p0.x), lat2y(p0.y))
lineTo(lon2x(p1.x), lat2y(p1.y))
lineTo(lon2x(p2.x), lat2y(p2.y))
close()
}
// 你可以自定义着色函数:例如基于点索引、面积、属性等
// 下面示例:按 index 渐变色(仅示例)
val color = colorFromIndex(i)
paintFill.color = color
canvas.drawPath(path, paintFill)
canvas.drawPath(path, paintStroke)
}
// 5) 在 Mapbox 上创建 imageSource四角经纬
val topLeft = listOf(minLon, maxLat)
val topRight = listOf(maxLon, maxLat)
val bottomRight = listOf(maxLon, minLat)
val bottomLeft = listOf(minLon, minLat)
val coords = listOf(topLeft, topRight, bottomRight, bottomLeft)
// remove old source if exists
if (style.styleSourceExists(testSourceId)) {
style.removeStyleSource(testSourceId)
}
val imgSource = imageSource(testSourceId) {
coordinates(coords)
}
style.addSource(imgSource)
// add layer
val rasterLayer = rasterLayer(testLayerId, testSourceId) {
visibility(Visibility.VISIBLE)
}
// remove old layer if present
try {
style.removeStyleLayer(testLayerId)
} catch (_: Exception) {
}
style.addLayer(rasterLayer)
// 6) 把 bitmap 更新到 source多数新版叫 updateImage
val src = style.getSourceAs<ImageSource>(testSourceId)
src?.updateImage(bitmap) // 或 src?.setImage(bitmap) 视 SDK 而定
}
}
/** 简单的着色函数:按 index 生成颜色(示例,不必准确) */
fun colorFromIndex(i: Int): Int {
// 生成一些可视颜色
val r = (50 + (i * 37) % 200)
val g = (80 + (i * 61) % 150)
val b = (100 + (i * 47) % 120)
return Color.argb(200, r, g, b)
}

View File

@@ -0,0 +1,165 @@
package com.icegps.geotools
import android.graphics.Bitmap
import com.icegps.geotools.model.IPoint
data class BoundingBox(
val minX: Double,
val minY: Double,
val maxX: Double,
val maxY: Double,
)
/**
* @author tabidachinokaze
* @date 2025/11/5
*/
object RasterUtils {
fun boundingBox(points: List<IPoint>): BoundingBox {
return BoundingBox(
minX = points.minOf { it.x },
maxX = points.maxOf { it.x },
minY = points.minOf { it.y },
maxY = points.maxOf { it.y },
)
}
/**
* Rasterize a Delaunator mesh into a regular grid.
*
* @param delaunator your Delaunator<T : IPoint> instance (points are in map coordinates, e.g., lon/lat)
* @param minX left bound (coordinate)
* @param minY bottom bound (coordinate)
* @param maxX right bound
* @param maxY top bound
* @param cols number of columns (width) of the raster
* @param rows number of rows (height) of the raster
* @param valueGetter a function that given a point index returns the value (Double) to rasterize (e.g. elevation)
* @param noDataValue value for cells not covered by any triangle (default = Float.NaN)
* @return Pair(FloatArray(values row-major), Bitmap visualization)
*/
fun <T : IPoint> rasterizeDelaunay(
delaunator: Delaunator<T>,
minX: Double,
minY: Double,
maxX: Double,
maxY: Double,
cols: Int,
rows: Int,
valueGetter: (T) -> Double,
noDataValue: Float = Float.NaN
): Pair<FloatArray, Bitmap> {
require(cols > 0 && rows > 0)
// grid: row-major, index = row * cols + col
val raster = FloatArray(cols * rows) { noDataValue }
val cellWidth = (maxX - minX) / cols.toDouble()
val cellHeight = (maxY - minY) / rows.toDouble()
// helper: convert grid coords to world coordinates (center of cell)
fun centerX(col: Int) = minX + (col + 0.5) * cellWidth
fun centerY(row: Int) = maxY - (row + 0.5) * cellHeight // top->down rows
// For performance, iterate triangles, compute their bbox in grid coords, then fill pixels inside.
val tris = delaunator.triangles
var t = 0
while (t <= tris.lastIndex) {
val ia = tris[t]
val ib = tris[t + 1]
val ic = tris[t + 2]
val pa = delaunator.points[ia]
val pb = delaunator.points[ib]
val pc = delaunator.points[ic]
// triangle bbox in world coords
val triMinX = minOf(pa.x, pb.x, pc.x)
val triMaxX = maxOf(pa.x, pb.x, pc.x)
val triMinY = minOf(pa.y, pb.y, pc.y)
val triMaxY = maxOf(pa.y, pb.y, pc.y)
// map bbox to grid index range (clamp to raster)
val minCol = (((triMinX - minX) / cellWidth).toInt()).coerceIn(0, cols - 1)
val maxCol = (((triMaxX - minX) / cellWidth).toInt()).coerceIn(0, cols - 1)
val minRow = (((maxY - triMaxY) / cellHeight).toInt()).coerceIn(0, rows - 1)
val maxRow = (((maxY - triMinY) / cellHeight).toInt()).coerceIn(0, rows - 1)
// Precompute values at vertices
val va = valueGetter(pa)
val vb = valueGetter(pb)
val vc = valueGetter(pc)
// Precompute for barycentric / edge function method
// We'll compute barycentric weights using area method
val x0 = pa.x;
val y0 = pa.y
val x1 = pb.x;
val y1 = pb.y
val x2 = pc.x;
val y2 = pc.y
// area * 2
val denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2)
// if denom == 0 -> degenerate triangle, skip
if (kotlin.math.abs(denom) < 1e-15) {
t += 3
continue
}
for (row in minRow..maxRow) {
val cy = centerY(row)
for (col in minCol..maxCol) {
val cx = centerX(col)
// compute barycentric coordinates (l1,l2,l3) for point (cx,cy)
val l1 = ((y1 - y2) * (cx - x2) + (x2 - x1) * (cy - y2)) / denom
val l2 = ((y2 - y0) * (cx - x2) + (x0 - x2) * (cy - y2)) / denom
val l3 = 1.0 - l1 - l2
// check if inside triangle (allow tiny negative eps)
if (l1 >= -1e-8 && l2 >= -1e-8 && l3 >= -1e-8) {
val value = l1 * va + l2 * vb + l3 * vc
raster[row * cols + col] = value.toFloat()
}
}
}
t += 3
}
// Create a simple grayscale Bitmap for visualization (normalize values ignoring NaN)
var minV = Double.POSITIVE_INFINITY
var maxV = Double.NEGATIVE_INFINITY
for (v in raster) {
if (!v.isNaN()) {
if (v < minV) minV = v.toDouble()
if (v > maxV) maxV = v.toDouble()
}
}
if (minV == Double.POSITIVE_INFINITY || maxV == Double.NEGATIVE_INFINITY) {
// all nodata
minV = 0.0
maxV = 1.0
}
val bmp = Bitmap.createBitmap(cols, rows, Bitmap.Config.ARGB_8888)
for (r in 0 until rows) {
for (c in 0 until cols) {
val v = raster[r * cols + c]
val color = if (v.isNaN()) {
// transparent for nodata
0x00000000
} else {
val norm = ((v - minV) / (maxV - minV)).coerceIn(0.0, 1.0)
val gray = (norm * 255.0).toInt()
// ARGB
(0xFF shl 24) or (gray shl 16) or (gray shl 8) or gray
}
bmp.setPixel(c, r, color)
}
}
return Pair(raster, bmp)
}
}

View File

@@ -0,0 +1,7 @@
package com.icegps.geotools.ktx
/**
* @author tabidachinokaze
* @date 2025/11/5
*/
val Any.TAG: String get() = this::class.java.simpleName

View File

@@ -0,0 +1,17 @@
package com.icegps.geotools.ktx
import com.icegps.math.geometry.Vector3D
/**
* @author tabidachinokaze
* @date 2025/11/5
*/
fun Vector3D.niceStr(): String {
return "[$x, $y, $z]".format(this)
}
fun List<Vector3D>.niceStr(): String {
return joinToString(", ", "[", "]") {
it.niceStr()
}
}

View File

@@ -0,0 +1,11 @@
package com.icegps.geotools.model
/**
* @author tabidachinokaze
* @date 2025/11/5
*/
data class DPoint(
override var x: Double,
override var y: Double,
var z: Double
) : IPoint

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,29 @@
<!-- res/drawable/ic_pile_marker.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FF2196F3"> <!-- 蓝色,可修改 -->
<!-- 圆形底座 -->
<path
android:fillColor="@android:color/white"
android:strokeColor="#FF000000"
android:strokeWidth="0.5"
android:pathData="M12,22c5.523,0 10,-4.477 10,-10S17.523,2 12,2 2,6.477 2,12s4.477,10 10,10z" />
<!-- 打桩机简笔画 -->
<path
android:fillColor="#FF000000"
android:pathData="M11,7h2v11h-2z" /> <!-- 桩杆 -->
<path
android:fillColor="#FF000000"
android:pathData="M8,7h8v2h-8z" /> <!-- 桩锤 -->
<!-- 中心定位点 -->
<path
android:fillColor="#FF2196F3"
android:pathData="M12,13.5a1.5,1.5 0,1 0,0 -3,1.5,1.5 0,0 0,0 3z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Geotools" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="mapbox_access_token" translatable="false" tools:ignore="UnusedResources">pk.eyJ1IjoienpxMSIsImEiOiJjbWYzbzV1MzQwMHJvMmpvbG1wbjJwdjUyIn0.LvKjIrCv9dAFcGxOM52f2Q</string>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">geotools</string>
</resources>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Geotools" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.icegps.geotools
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

7
build.gradle Normal file
View File

@@ -0,0 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.android.library) apply false
}

1
delaunator/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

40
delaunator/build.gradle Normal file
View File

@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace 'com.icegps.geotools'
compileSdk 35
defaultConfig {
minSdk 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '11'
}
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}

View File

21
delaunator/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.icegps.geotools
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.icegps.geotools.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,625 @@
package com.icegps.geotools
import com.icegps.geotools.model.Edge
import com.icegps.geotools.model.IEdge
import com.icegps.geotools.model.IPoint
import com.icegps.geotools.model.Point
import com.icegps.geotools.model.VoronoiCell
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sqrt
interface IDelaunator<T> {
val points: List<T>
var triangles: Array<Int>
var halfedges: Array<Int>
fun getHullEdges(): List<IEdge>
fun getVoronoiCells(): Sequence<VoronoiCell>
fun getEdges(): Sequence<IEdge>
}
class Delaunator<T : IPoint>(override val points: List<T>) : IDelaunator<T> {
private val EPSILON = 2.0.pow(-52.0)
private val edgeStack = Array(512) { 0 }
override var triangles: Array<Int>
override var halfedges: Array<Int>
private val hashSize: Int
private val hullPrev: MutableList<Int>
private val hullNext: MutableList<Int>
private val hullTri: MutableList<Int>
private val hullHash: Array<Int>
private var cx: Double
private var cy: Double
private var trianglesLen: Int
private val coords: Array<Double>
private var hullStart: Int
private var hullSize: Int
private val hull: Array<Int>
init {
if (points.size < 3) {
throw IndexOutOfBoundsException("Need at least 3 points")
}
coords = Array(points.size * 2) { .0 }
points.forEachIndexed { index, point ->
coords[2 * index] = point.x
coords[2 * index + 1] = point.y
}
val n = coords.size shr 1
val maxTriangles = 2 * n - 5
triangles = Array(maxTriangles * 3) { 0 }
halfedges = Array(maxTriangles * 3) { 0 }
hashSize = ceil(sqrt(n.toDouble())).toInt()
hullPrev = MutableList(n) { 0 }
hullNext = MutableList(n) { 0 }
hullTri = MutableList(n) { 0 }
hullHash = Array(hashSize) { 0 }
val ids = Array(n) { 0 }
var minX = Double.POSITIVE_INFINITY
var minY = Double.POSITIVE_INFINITY
var maxX = Double.POSITIVE_INFINITY
var maxY = Double.POSITIVE_INFINITY
for (i in 0 until n) {
val x = coords[2 * i]
val y = coords[2 * i + 1]
if (x < minX) minX = x
if (y < minY) minY = y
if (x > maxX) maxX = x
if (y > maxY) maxY = y
ids[i] = i
}
val cx = (minX + maxX) / 2
val cy = (minY + maxY) / 2
var minDist = Double.POSITIVE_INFINITY
var i0 = 0
var i1 = 0
var i2 = 0
// pick a seed point close to the center
for (i in 0 until n) {
val d = dist(cx, cy, coords[2 * i], coords[2 * i + 1])
if (d < minDist) {
i0 = i
minDist = d
}
}
val i0x = coords[2 * i0]
val i0y = coords[2 * i0 + 1]
minDist = Double.POSITIVE_INFINITY
// find the point closest to the seed
for (i in 0 until n) {
if (i == i0) continue
val d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1])
if (d < minDist && d > 0) {
i1 = i
minDist = d
}
}
var i1x = coords[2 * i1]
var i1y = coords[2 * i1 + 1]
var minRadius = Double.POSITIVE_INFINITY
// find the third point which forms the smallest circumcircle with the first two
for (i in 0 until n) {
if (i == i0 || i == i1) continue
val r = circumRadius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1])
if (r < minRadius) {
i2 = i
minRadius = r
}
}
var i2x = coords[2 * i2]
var i2y = coords[2 * i2 + 1]
if (minRadius == Double.POSITIVE_INFINITY) {
throw Exception("No Delaunay triangulation exists for this input.")
}
if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) {
val i = i1
val x = i1x
val y = i1y
i1 = i2
i1x = i2x
i1y = i2y
i2 = i
i2x = x
i2y = y
}
val center = circumCenter(i0x, i0y, i1x, i1y, i2x, i2y)
this.cx = center.x
this.cy = center.y
val dists = Array(n) { i ->
dist(coords[2 * i], coords[2 * i + 1], center.x, center.y)
}
// sort the points by distance from the seed triangle circumcenter
quicksort(ids, dists, 0, n - 1)
// set up the seed triangle as the starting hull
hullStart = i0
hullSize = 3
hullPrev[i2] = i1
hullNext[i0] = i1
hullPrev[i0] = i2
hullNext[i1] = i2
hullPrev[i1] = i0
hullNext[i2] = i0
hullTri[i0] = 0
hullTri[i1] = 1
hullTri[i2] = 2
hullHash[hashKey(i0x, i0y)] = i0
hullHash[hashKey(i1x, i1y)] = i1
hullHash[hashKey(i2x, i2y)] = i2
trianglesLen = 0
addTriangle(i0, i1, i2, -1, -1, -1)
var xp = .0
var yp = .0
for (k in ids.indices) {
val i = ids[k]
val x = coords[2 * i]
val y = coords[2 * i + 1]
// skip near-duplicate points
if (k > 0 && abs(x - xp) <= EPSILON && abs(y - yp) <= EPSILON) continue
xp = x
yp = y
// skip seed triangle points
if (i == i0 || i == i1 || i == i2) continue
// find a visible edge on the convex hull using edge hash
var start = 0
for (j in 0 until hashSize) {
val key = hashKey(x, y)
start = hullHash[(key + j) % hashSize]
if (start != -1 && start != hullNext[start]) break
}
start = hullPrev[start]
var e = start
var q = hullNext[e]
while (!orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1])) {
e = q
if (e == start) {
e = Int.MAX_VALUE
break
}
q = hullNext[e]
}
if (e == Int.MAX_VALUE) continue // likely a near-duplicate point; skip it
// add the first triangle from the point
var t = addTriangle(e, i, hullNext[e], -1, -1, hullTri[e])
// recursively flip triangles from the point until they satisfy the Delaunay condition
hullTri[i] = legalize(t + 2)
hullTri[e] = t // keep track of boundary triangles on the hull
hullSize++
// walk forward through the hull, adding more triangles and flipping recursively
var next = hullNext[e]
q = hullNext[next]
while (orient(x, y, coords[2 * next], coords[2 * next + 1], coords[2 * q], coords[2 * q + 1])) {
t = addTriangle(next, i, q, hullTri[i], -1, hullTri[next])
hullTri[i] = legalize(t + 2)
hullNext[next] = next // mark as removed
hullSize--
next = q
q = hullNext[next]
}
// walk backward from the other side, adding more triangles and flipping
if (e == start) {
q = hullPrev[e]
while (orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1])) {
t = addTriangle(q, i, e, -1, hullTri[e], hullTri[q])
legalize(t + 2)
hullTri[q] = t
hullNext[e] = e // mark as removed
hullSize--
e = q
q = hullPrev[e]
}
}
// update the hull indices
hullPrev[i] = e
hullStart = e
hullPrev[next] = i
hullNext[e] = i
hullNext[i] = next
// save the two new edges in the hash table
hullHash[hashKey(x, y)] = i
hullHash[hashKey(coords[2 * e], coords[2 * e + 1])] = e
}
hull = Array(hullSize) { 0 }
var s = hullStart
for (i in 0 until hullSize) {
hull[i] = s
s = hullNext[s]
}
// get rid of temporary arrays
hullPrev.clear()
hullNext.clear()
hullTri.clear()
//// trim typed triangle mesh arrays
triangles = triangles.take(trianglesLen).toTypedArray()
halfedges = halfedges.take(trianglesLen).toTypedArray()
}
private fun hashKey(x: Double, y: Double): Int {
return (floor(pseudoAngle(x - cx, y - cy) * hashSize) % hashSize).toInt()
}
private fun pseudoAngle(dx: Double, dy: Double): Double {
val p = dx / (abs(dx) + abs(dy))
return (if (dy > 0) 3 - p else 1 + p) / 4 // [0..1]
}
private fun legalize(index: Int): Int {
var a = index
var i = 0
var ar: Int
// recursion eliminated with a fixed-size stack
while (true) {
val b = halfedges[a]
/* if the pair of triangles doesn't satisfy the Delaunay condition
* (p1 is inside the circumcircle of [p0, pl, pr]), flip them,
* then do the same check/flip recursively for the new pair of triangles
*
* pl pl
* /||\ / \
* al/ || \bl al/ \a
* / || \ / \
* / a||b \ flip /___ar___\
* p0\ || /p1 => p0\---bl---/p1
* \ || / \ /
* ar\ || /br b\ /br
* \||/ \ /
* pr pr
*/
val a0 = a - a % 3
ar = a0 + (a + 2) % 3
if (b == -1) { // convex hull edge
if (i == 0) break
a = edgeStack[--i]
continue
}
val b0 = b - b % 3
val al = a0 + (a + 1) % 3
val bl = b0 + (b + 2) % 3
val p0 = triangles[ar]
val pr = triangles[a]
val pl = triangles[al]
val p1 = triangles[bl]
val illegal = inCircle(
coords[2 * p0], coords[2 * p0 + 1],
coords[2 * pr], coords[2 * pr + 1],
coords[2 * pl], coords[2 * pl + 1],
coords[2 * p1], coords[2 * p1 + 1]
)
if (illegal) {
triangles[a] = p1
triangles[b] = p0
val hbl = halfedges[bl]
// edge swapped on the other side of the hull (rare); fix the halfedge reference
if (hbl == -1) {
var e = hullStart
do {
if (hullTri[e] == bl) {
hullTri[e] = a
break
}
e = hullNext[e]
} while (e != hullStart)
}
link(a, hbl)
link(b, halfedges[ar])
link(ar, bl)
val br = b0 + (b + 1) % 3
// don't worry about hitting the cap: it can only happen on extremely degenerate input
if (i < edgeStack.size) {
edgeStack[i++] = br
}
} else {
if (i == 0) break
a = edgeStack[--i]
}
}
return ar
}
private fun inCircle(
ax: Double,
ay: Double,
bx: Double,
by: Double,
cx: Double,
cy: Double,
px: Double,
py: Double
): Boolean {
val dx = ax - px
val dy = ay - py
val ex = bx - px
val ey = by - py
val fx = cx - px
val fy = cy - py
val ap = dx * dx + dy * dy
val bp = ex * ex + ey * ey
val cp = fx * fx + fy * fy
return dx * (ey * cp - bp * fy) -
dy * (ex * cp - bp * fx) +
ap * (ex * fy - ey * fx) < 0
}
private fun link(a: Int, b: Int) {
halfedges[a] = b
if (b != -1) halfedges[b] = a
}
private fun circumRadius(
ax: Double,
ay: Double,
bx: Double,
by: Double,
cx: Double,
cy: Double
): Double {
val dx = bx - ax
val dy = by - ay
val ex = cx - ax
val ey = cy - ay
val bl = dx * dx + dy * dy
val cl = ex * ex + ey * ey
val d = 0.5 / (dx * ey - dy * ex)
val x = (ey * bl - dy * cl) * d
val y = (dx * cl - ex * bl) * d
return x * x + y * y
}
private fun quicksort(ids: Array<Int>, dists: Array<Double>, left: Int, right: Int) {
if (right - left <= 20) {
for (i in left + 1..right) {
val temp = ids[i]
val tempDist = dists[temp]
var j = i - 1
while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]
ids[j + 1] = temp
}
} else {
val median = left + right shr 1
var i = left + 1
var j = right
swap(ids, median, i)
if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right)
if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right)
if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i)
val temp = ids[i]
val tempDist = dists[temp]
while (true) {
do i++ while (dists[ids[i]] < tempDist)
do j-- while (dists[ids[j]] > tempDist)
if (j < i) break
swap(ids, i, j)
}
ids[left + 1] = ids[j]
ids[j] = temp
if (right - i + 1 >= j - left) {
quicksort(ids, dists, i, right)
quicksort(ids, dists, left, j - 1)
} else {
quicksort(ids, dists, left, j - 1)
quicksort(ids, dists, i, right)
}
}
}
private fun swap(arr: Array<Int>, i: Int, j: Int) {
val tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
private fun circumCenter(
ax: Double,
ay: Double,
bx: Double,
by: Double,
cx: Double,
cy: Double
): Point {
val dx = bx - ax
val dy = by - ay
val ex = cx - ax
val ey = cy - ay
val bl = dx * dx + dy * dy
val cl = ex * ex + ey * ey
val d = 0.5 / (dx * ey - dy * ex)
val x = ax + (ey * bl - dy * cl) * d
val y = ay + (dx * cl - ex * bl) * d
return Point(x, y)
}
private fun orient(px: Double, py: Double, qx: Double, qy: Double, rx: Double, ry: Double): Boolean {
return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) < 0
}
private fun addTriangle(i0: Int, i1: Int, i2: Int, a: Int, b: Int, c: Int): Int {
val t = trianglesLen
triangles[t] = i0
triangles[t + 1] = i1
triangles[t + 2] = i2
link(t, a)
link(t + 1, b)
link(t + 2, c)
trianglesLen += 3
return t
}
private fun dist(ax: Double, ay: Double, bx: Double, by: Double): Double {
val dx = ax - bx
val dy = ay - by
return dx * dx + dy * dy
}
private fun createHull(points: List<T>): List<IEdge> {
return points.mapIndexed { index: Int, point: T ->
if (points.lastIndex == index) {
Edge(0, point, points.first())
} else {
Edge(0, point, points[index + 1])
}
}
}
private fun getHullPoints(): List<T> {
return hull.map { x -> points[x] }
}
override fun getHullEdges(): List<IEdge> {
return createHull(getHullPoints())
}
override fun getVoronoiCells(): Sequence<VoronoiCell> {
return sequence {
val seen = HashSet<Int>() // of point ids
for (triangleId in triangles.indices) {
val id = triangles[nextHalfedgeIndex(triangleId)]
if (!seen.contains(id)) {
seen.add(id)
val edges = edgesAroundPoint(triangleId)
val triangles = edges.map { x -> triangleOfEdge(x) }
val vertices = triangles.map { x -> getTriangleCenter(x) }
yield(VoronoiCell(id, vertices.toList()))
}
}
}
}
private fun getTriangleCenter(t: Int): IPoint {
val vertices = getTrianglePoints(t)
return getCentroid(vertices)
}
private fun getCentroid(points: List<IPoint>): IPoint {
var accumulatedArea = 0.0
var centerX = 0.0
var centerY = 0.0
var j = points.size - 1
for (i in points.indices) {
val temp = points[i].x * points[j].y - points[j].x * points[i].y
accumulatedArea += temp
centerX += (points[i].x + points[j].x) * temp
centerY += (points[i].y + points[j].y) * temp
j = i
}
accumulatedArea *= 3.0
return Point(
centerX / accumulatedArea,
centerY / accumulatedArea
)
}
private fun getTrianglePoints(t: Int): List<IPoint> {
return pointsOfTriangle(t).map { p -> points[p] }
}
private fun pointsOfTriangle(t: Int): List<Int> {
return edgesOfTriangle(t).map { e -> triangles[e] }
}
private fun edgesOfTriangle(t: Int): List<Int> {
return listOf(3 * t, 3 * t + 1, 3 * t + 2)
}
private fun triangleOfEdge(e: Int): Int {
return floor(e / 3.0).toInt()
}
private fun edgesAroundPoint(start: Int): Sequence<Int> {
return sequence {
var incoming = start
do {
yield(incoming)
val outgoing = nextHalfedgeIndex(incoming)
incoming = halfedges[outgoing]
} while (incoming != -1 && incoming != start)
}
}
private fun nextHalfedgeIndex(e: Int): Int {
return if (e % 3 == 2) e - 2 else e + 1
}
override fun getEdges(): Sequence<IEdge> {
return sequence {
for (e in triangles.indices) {
if (e > halfedges[e]) {
val p = points[triangles[e]]
val q = points[triangles[nextHalfedgeIndex(e)]]
yield(Edge(e, p, q))
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.icegps.geotools.model
class Edge(
override val index: Int,
override val p: IPoint,
override val q: IPoint
) : IEdge

View File

@@ -0,0 +1,7 @@
package com.icegps.geotools.model
interface IEdge {
val p: IPoint
val q: IPoint
val index: Int
}

View File

@@ -0,0 +1,6 @@
package com.icegps.geotools.model
interface IPoint {
var x: Double
var y: Double
}

View File

@@ -0,0 +1,6 @@
package com.icegps.geotools.model
interface ITriangle {
val points: List<IPoint>
val Index: Int
}

View File

@@ -0,0 +1,6 @@
package com.icegps.geotools.model
interface IVoronoiCell {
val points: List<IPoint>
val index: Int
}

View File

@@ -0,0 +1,19 @@
package com.icegps.geotools.model
data class Point(override var x: Double, override var y: Double) : IPoint {
override fun toString() = "{$x},{$y}"
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun div(other: Int): Point {
return Point(x / other, y / other)
}
}

View File

@@ -0,0 +1,6 @@
package com.icegps.geotools.model
class Triangle(
override val points: List<IPoint>,
override val Index: Int
) : ITriangle

View File

@@ -0,0 +1,6 @@
package com.icegps.geotools.model
class VoronoiCell(
override val index: Int,
override val points: List<IPoint>
) : IVoronoiCell

View File

@@ -0,0 +1,17 @@
package com.icegps.geotools
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

23
gradle.properties Normal file
View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

29
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,29 @@
[versions]
agp = "8.10.1"
kotlin = "2.0.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.12.0"
activity = "1.11.0"
constraintlayout = "2.2.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Wed Nov 05 13:47:37 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
math/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

17
math/build.gradle Normal file
View File

@@ -0,0 +1,17 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin.jvm)
}
java {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
dependencies {
testImplementation libs.kotlin.test
}

View File

@@ -0,0 +1,75 @@
@file:Suppress("NOTHING_TO_INLINE")
package com.icegps.io.util
import kotlin.math.*
//private fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this
private val MINUS_ZERO_D = -0.0
private fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this
fun Double.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String {
if (this.isNanOrInfinite()) return this.toString()
//val bits = this.toRawBits()
//val sign = (bits ushr 63) != 0L
//val exponent = (bits ushr 52) and 0b11111111111
//val fraction = bits and ((1L shl 52) - 1L)
val res = this.roundDecimalPlaces(decimalPlaces).normalizeZero().toString()
val eup = res.indexOf('E')
val elo = res.indexOf('e')
val eIndex = if (eup >= 0) eup else elo
val rez = if (eIndex >= 0) {
val base = res.substring(0, eIndex)
val exp = res.substring(eIndex + 1).toInt()
val rbase = if (base.contains(".")) base else "$base.0"
val zeros = "0".repeat(exp.absoluteValue + 2)
val part = if (exp > 0) "$rbase$zeros" else "$zeros$rbase"
val pointIndex2 = part.indexOf(".")
val pointIndex = if (pointIndex2 < 0) part.length else pointIndex2
val outIndex = pointIndex + exp
val part2 = part.replace(".", "")
buildString {
if ((0 until outIndex).all { part2[it] == '0' }) {
append('0')
} else {
append(part2, 0, outIndex)
}
append('.')
append(part2, outIndex, part2.length)
}
} else {
res
}
val pointIndex = rez.indexOf('.')
val integral = if (pointIndex >= 0) rez.substring(0, pointIndex) else rez
if (decimalPlaces == 0) return integral
val decimal = if (pointIndex >= 0) rez.substring(pointIndex + 1).trimEnd('0') else ""
return buildString(2 + integral.length + decimalPlaces) {
append(integral)
if (decimal.isNotEmpty() || !skipTrailingZeros) {
val decimalCount = min(decimal.length, decimalPlaces)
val allZeros = (0 until decimalCount).all { decimal[it] == '0' }
if (!skipTrailingZeros || !allZeros) {
append('.')
append(decimal, 0, decimalCount)
if (!skipTrailingZeros) repeat(decimalPlaces - decimalCount) { append('0') }
}
}
}
}
fun Float.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String = this.toDouble().toStringDecimal(decimalPlaces, skipTrailingZeros)
private fun Double.roundDecimalPlaces(places: Int): Double {
if (places < 0) return this
val placesFactor: Double = 10.0.pow(places.toDouble())
return round(this * placesFactor) / placesFactor
}
private fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite()
private fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite()

View File

@@ -0,0 +1,78 @@
package com.icegps.io.util
import kotlin.math.*
object NumberParser {
const val END = '\u0000'
fun parseInt(str: String, start: Int = 0, end: Int = str.length, radix: Int = 10): Int {
var n = start
return parseInt(radix) { if (n >= end) END else str[n++] }
}
fun parseDouble(str: String, start: Int = 0, end: Int = str.length): Double {
var n = start
return parseDouble { if (n >= end) END else str[n++] }
}
inline fun parseInt(radix: Int = 10, gen: (Int) -> Char): Int {
var positive = true
var out = 0
var n = 0
while (true) {
val c = gen(n++)
if (c == END) break
if (c == '-' || c == '+') {
positive = (c == '+')
} else {
val value = c.ctypeAsInt()
if (value < 0) break
out *= radix
out += value
}
}
return if (positive) out else -out
}
inline fun parseDouble(gen: (Int) -> Char): Double {
var out = 0.0
var frac = 1.0
var pointSeen = false
var eSeen = false
var negate = false
var negateExponent = false
var exponent = 0
var n = 0
while (true) {
val c = gen(n++)
if (c == END) break
when (c) {
'e', 'E' -> eSeen = true
'-' -> {
if (eSeen) negateExponent = true else negate = true
}
'.' -> pointSeen = true
else -> {
if (eSeen) {
exponent *= 10
exponent += c.ctypeAsInt()
} else {
if (pointSeen) frac /= 10
out *= 10
out += c.ctypeAsInt()
}
}
}
}
val res = (out * frac) * 10.0.pow(if (negateExponent) -exponent else exponent)
return if (negate) -res else res
}
}
@Suppress("ConvertTwoComparisonsToRangeCheck") // @TODO: Kotlin-Native doesn't optimize ranges
@PublishedApi internal fun Char.ctypeAsInt(): Int = when {
this >= '0' && this <= '9' -> this - '0'
this >= 'a' && this <= 'z' -> this - 'a' + 10
this >= 'A' && this <= 'Z' -> this - 'A' + 10
else -> -1
}

View File

@@ -0,0 +1,47 @@
package com.icegps.math
import kotlin.math.absoluteValue
////////////////////
////////////////////
/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Int.nextAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align)
/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Long.nextAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align)
/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Float.nextAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align)
/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Double.nextAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align)
/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Int.prevAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Long.prevAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Float.prevAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Double.prevAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
/** Returns whether [this] is multiple of [alignment] */
public fun Int.isAlignedTo(alignment: Int): Boolean = alignment == 0 || (this % alignment) == 0
/** Returns whether [this] is multiple of [alignment] */
public fun Long.isAlignedTo(alignment: Long): Boolean = alignment == 0L || (this % alignment) == 0L
/** Returns whether [this] is multiple of [alignment] */
public fun Float.isAlignedTo(alignment: Float): Boolean = alignment == 0f || (this % alignment) == 0f
/** Returns whether [this] is multiple of [alignment] */
public fun Double.isAlignedTo(alignment: Double): Boolean = alignment == 0.0 || (this % alignment) == 0.0
/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Float.nearestAlignedTo(align: Float): Float {
val prev = this.prevAlignedTo(align)
val next = this.nextAlignedTo(align)
return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
}
/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
public fun Double.nearestAlignedTo(align: Double): Double {
val prev = this.prevAlignedTo(align)
val next = this.nextAlignedTo(align)
return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
}

View File

@@ -0,0 +1,9 @@
package com.icegps.math
////////////////////
////////////////////
/** Converts this [Boolean] into integer: 1 for true, 0 for false */
inline fun Boolean.toInt(): Int = if (this) 1 else 0
inline fun Boolean.toByte(): Byte = if (this) 1 else 0
inline fun Byte.toBoolean(): Boolean = this.toInt() != 0

View File

@@ -0,0 +1,38 @@
package com.icegps.math
/** Clamps [this] value into the range [min] and [max] */
fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this
/** Clamps [this] value into the range [min] and [max] */
fun Long.clamp(min: Long, max: Long): Long = if (this < min) min else if (this > max) max else this
/** Clamps [this] value into the range [min] and [max] */
fun Double.clamp(min: Double, max: Double): Double = if (this < min) min else if (this > max) max else this
/** Clamps [this] value into the range [min] and [max] */
fun Float.clamp(min: Float, max: Float): Float = if ((this < min)) min else if ((this > max)) max else this
/** Clamps [this] value into the range 0 and 1 */
fun Double.clamp01(): Double = clamp(0.0, 1.0)
/** Clamps [this] value into the range 0 and 1 */
fun Float.clamp01(): Float = clamp(0f, 1f)
/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int]. The default parameters will cover the whole range of values. */
fun Long.toIntClamp(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): Int {
if (this < min) return min
if (this > max) return max
return this.toInt()
}
/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int] (where [min] must be zero or positive). The default parameters will cover the whole range of positive and zero values. */
fun Long.toUintClamp(min: Int = 0, max: Int = Int.MAX_VALUE): Int = this.toIntClamp(min, max)
/** Clamps the integer value in the 0..255 range */
fun Int.clampUByte(): Int {
val n = this and -(if (this >= 0) 1 else 0)
return (n or (0xFF - n shr 31)) and 0xFF
}
fun Int.clampUShort(): Int {
val n = this and -(if (this >= 0) 1 else 0)
return (n or (0xFFFF - n shr 31)) and 0xFFFF
}
fun Int.toShortClamped(): Short = this.clamp(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
fun Int.toByteClamped(): Byte = this.clamp(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()

View File

@@ -0,0 +1,24 @@
package com.icegps.math
////////////////////
////////////////////
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
fun Float.convertRange(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin)))
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
fun Double.convertRange(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin)))
//fun Double.convertRange(minSrc: Double, maxSrc: Double, minDst: Double, maxDst: Double): Double = (((this - minSrc) / (maxSrc - minSrc)) * (maxDst - minDst)) + minDst
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
fun Int.convertRange(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toInt()
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
fun Long.convertRange(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toLong()
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
fun Float.convertRangeClamped(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
fun Double.convertRangeClamped(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
fun Int.convertRangeClamped(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
fun Long.convertRangeClamped(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)

View File

@@ -0,0 +1,19 @@
package com.icegps.math
import kotlin.math.roundToInt
////////////////////
////////////////////
/** Divides [this] into [that] rounding to the floor */
public infix fun Int.divFloor(that: Int): Int = this / that
/** Divides [this] into [that] rounding to the ceil */
public infix fun Int.divCeil(that: Int): Int = if (this % that != 0) (this / that) + 1 else (this / that)
/** Divides [this] into [that] rounding to the round */
public infix fun Int.divRound(that: Int): Int = (this.toDouble() / that.toDouble()).roundToInt()
public infix fun Long.divCeil(other: Long): Long {
val res = this / other
if (this % other != 0L) return res + 1
return res
}

View File

@@ -0,0 +1,4 @@
package com.icegps.math
public inline fun fract(value: Float): Float = value - value.toIntFloor()
public inline fun fract(value: Double): Double = value - value.toIntFloor()

View File

@@ -0,0 +1,9 @@
package com.icegps.math
////////////////////
////////////////////
/** Performs a fast integral logarithmic of base two */
fun ilog2(v: Int): Int = if (v == 0) (-1) else (31 - v.countLeadingZeroBits())
// fun ilog2(v: Int): Int = kotlin.math.log2(v.toDouble()).toInt()
fun ilog2Ceil(v: Int): Int = kotlin.math.ceil(kotlin.math.log2(v.toDouble())).toInt()

View File

@@ -0,0 +1,14 @@
package com.icegps.math
import kotlin.math.*
interface IsAlmostEquals<T> {
fun isAlmostEquals(other: T, epsilon: Double = 0.000001): Boolean
}
interface IsAlmostEqualsF<T> {
fun isAlmostEquals(other: T, epsilon: Float = 0.0001f): Boolean
}
fun Float.isAlmostEquals(other: Float, epsilon: Float = 0.000001f): Boolean = (this - other).absoluteValue < epsilon
fun Double.isAlmostEquals(other: Double, epsilon: Double = 0.000001): Boolean = (this - other).absoluteValue < epsilon

View File

@@ -0,0 +1,4 @@
package com.icegps.math
fun Double.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-19
fun Float.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-6

View File

@@ -0,0 +1,9 @@
package com.icegps.math
////////////////////
////////////////////
/** Checks if [this] is odd (not multiple of two) */
val Int.isOdd: Boolean get() = (this % 2) == 1
/** Checks if [this] is even (multiple of two) */
val Int.isEven: Boolean get() = (this % 2) == 0

View File

@@ -0,0 +1,11 @@
package com.icegps.math
///** Check if [this] floating point value is not a number or infinite */
//public fun Float.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite()
///** Check if [this] floating point value is not a number or infinite */
//public fun Double.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite()
fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite()
fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite()

View File

@@ -0,0 +1,133 @@
package com.icegps.math
import kotlin.math.*
const val PIF = PI.toFloat()
const val PI2F = (PI * 2).toFloat()
fun Double.betweenInclusive(min: Double, max: Double): Boolean = (this >= min) && (this <= max)
fun almostEquals(a: Float, b: Float) = almostZero(a - b)
fun almostZero(a: Float) = abs(a) <= 0.0000001
fun almostEquals(a: Double, b: Double) = almostZero(a - b)
fun almostZero(a: Double) = abs(a) <= 0.0000001
fun isEquivalent(a: Double, b: Double, epsilon: Double = 0.0001): Boolean = (a - epsilon < b) && (a + epsilon > b)
fun Double.smoothstep(edge0: Double, edge1: Double): Double {
if (this < edge0) return 0.0
if (this >= edge1) return 1.0
val v = ((this - edge0) / (edge1 - edge0))//.clamp(0.0, 1.0)
return v * v * (3 - 2 * v)
}
fun log(v: Int, base: Int): Int = log(v.toDouble(), base.toDouble()).toInt()
fun ln(v: Int): Int = ln(v.toDouble()).toInt()
fun log2(v: Int): Int = log(v.toDouble(), 2.0).toInt()
fun log10(v: Int): Int = log(v.toDouble(), 10.0).toInt()
@Deprecated("", ReplaceWith("v.squared()"))
fun sq(v: Int): Int = v.squared()
@Deprecated("", ReplaceWith("v.squared()"))
fun sq(v: Float): Float = v.squared()
@Deprecated("", ReplaceWith("v.squared()"))
fun sq(v: Double): Double = v.squared()
/** Signs of the value. Zero will be converted into -1 */
val Int.signM1: Int get() = signNonZeroM1(this)
/** Signs of the value. Zero will be converted into -1 */
val Float.signM1: Float get() = signNonZeroM1(this).toFloat()
/** Signs of the value. Zero will be converted into -1 */
val Double.signM1: Double get() = signNonZeroM1(this).toDouble()
/** Signs of the value. Zero will be converted into +1 */
val Int.signP1: Int get() = signNonZeroP1(this)
/** Signs of the value. Zero will be converted into +1 */
val Float.signP1: Float get() = signNonZeroP1(this).toFloat()
/** Signs of the value. Zero will be converted into +1 */
val Double.signP1: Double get() = signNonZeroP1(this).toDouble()
/** Signs of the value. Zero will be converted into -1 */
fun signNonZeroM1(x: Int): Int = if (x <= 0) -1 else +1
/** Signs of the value. Zero will be converted into -1 */
fun signNonZeroM1(x: Float): Int = if (x <= 0) -1 else +1
/** Signs of the value. Zero will be converted into -1 */
fun signNonZeroM1(x: Double): Int = if (x <= 0) -1 else +1
/** Signs of the value. Zero will be converted into +1 */
fun signNonZeroP1(x: Int): Int = if (x >= 0) +1 else -1
/** Signs of the value. Zero will be converted into +1 */
fun signNonZeroP1(x: Float): Int = if (x >= 0) +1 else -1
/** Signs of the value. Zero will be converted into +1 */
fun signNonZeroP1(x: Double): Int = if (x >= 0) +1 else -1
fun Float.normalizeAlmostZero() = if (this.isAlmostZero()) 0f else this
fun Double.closestMultipleOf(multiple: Double): Double {
val prev = prevMultipleOf(multiple)
val next = nextMultipleOf(multiple)
return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
}
fun Int.closestMultipleOf(multiple: Int): Int {
val prev = prevMultipleOf(multiple)
val next = nextMultipleOf(multiple)
return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
}
fun Long.closestMultipleOf(multiple: Long): Long {
val prev = prevMultipleOf(multiple)
val next = nextMultipleOf(multiple)
return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
}
fun Double.nextMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
fun Int.nextMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
fun Long.nextMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
fun Double.prevMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
fun Int.prevMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
fun Long.prevMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
fun Double.isMultipleOf(multiple: Double) = multiple.isAlmostZero() || (this % multiple).isAlmostZero()
fun Int.isMultipleOf(multiple: Int) = multiple == 0 || (this % multiple) == 0
fun Long.isMultipleOf(multiple: Long) = multiple == 0L || (this % multiple) == 0L
fun Double.squared(): Double = this * this
fun Float.squared(): Float = this * this
fun Int.squared(): Int = this * this
fun min(a: Int, b: Int, c: Int) = min(min(a, b), c)
fun min(a: Float, b: Float, c: Float) = min(min(a, b), c)
fun min(a: Double, b: Double, c: Double) = min(min(a, b), c)
fun min(a: Int, b: Int, c: Int, d: Int) = min(min(min(a, b), c), d)
fun min(a: Float, b: Float, c: Float, d: Float) = min(min(min(a, b), c), d)
fun min(a: Double, b: Double, c: Double, d: Double) = min(min(min(a, b), c), d)
fun min(a: Int, b: Int, c: Int, d: Int, e: Int) = min(min(min(min(a, b), c), d), e)
fun min(a: Float, b: Float, c: Float, d: Float, e: Float) = min(min(min(min(a, b), c), d), e)
fun min(a: Double, b: Double, c: Double, d: Double, e: Double) = min(min(min(min(a, b), c), d), e)
fun max(a: Int, b: Int, c: Int) = max(max(a, b), c)
fun max(a: Float, b: Float, c: Float) = max(max(a, b), c)
fun max(a: Double, b: Double, c: Double) = max(max(a, b), c)
fun max(a: Int, b: Int, c: Int, d: Int) = max(max(max(a, b), c), d)
fun max(a: Float, b: Float, c: Float, d: Float) = max(max(max(a, b), c), d)
fun max(a: Double, b: Double, c: Double, d: Double) = max(max(max(a, b), c), d)
fun max(a: Int, b: Int, c: Int, d: Int, e: Int) = max(max(max(max(a, b), c), d), e)
fun max(a: Float, b: Float, c: Float, d: Float, e: Float) = max(max(max(max(a, b), c), d), e)
fun max(a: Double, b: Double, c: Double, d: Double, e: Double) = max(max(max(max(a, b), c), d), e)
////////////////////
////////////////////
// @TODO: Optimize this
fun Int.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length
fun Long.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length
fun Int.cycle(min: Int, max: Int): Int = ((this - min) umod (max - min + 1)) + min
fun Int.cycleSteps(min: Int, max: Int): Int = (this - min) / (max - min + 1)

View File

@@ -0,0 +1,7 @@
package com.icegps.math
//fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this
private val MINUS_ZERO_D = -0.0
private val MINUS_ZERO_F = -0.0f
fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this
fun Float.normalizeZero(): Float = if (this == MINUS_ZERO_F) 0f else this

View File

@@ -0,0 +1,22 @@
package com.icegps.math
/** Returns the next power of two of [this] */
val Int.nextPowerOfTwo: Int get() {
var v = this
v--
v = v or (v shr 1)
v = v or (v shr 2)
v = v or (v shr 4)
v = v or (v shr 8)
v = v or (v shr 16)
v++
return v
}
/** Checks if [this] value is power of two */
val Int.isPowerOfTwo: Boolean get() = this.nextPowerOfTwo == this
/** Returns the previous power of two of [this] */
val Int.prevPowerOfTwo: Int get() = if (isPowerOfTwo) this else (nextPowerOfTwo ushr 1)

View File

@@ -0,0 +1,16 @@
package com.icegps.math
import kotlin.math.*
fun Float.roundDecimalPlaces(places: Int): Float {
if (places < 0) return this
val placesFactor: Float = 10f.pow(places.toFloat())
return round(this * placesFactor) / placesFactor
}
fun Double.roundDecimalPlaces(places: Int): Double {
if (places < 0) return this
val placesFactor: Double = 10.0.pow(places.toDouble())
return round(this * placesFactor) / placesFactor
}

View File

@@ -0,0 +1,29 @@
package com.icegps.math
import kotlin.math.*
////////////////////
////////////////////
/** Converts [this] into [Int] rounding to the ceiling */
fun Float.toIntCeil(): Int = ceil(this).toInt()
/** Converts [this] into [Int] rounding to the ceiling */
fun Double.toIntCeil(): Int = ceil(this).toInt()
/** Converts [this] into [Int] rounding to the nearest */
fun Float.toIntRound(): Int = round(this).toInt()
/** Converts [this] into [Int] rounding to the nearest */
fun Double.toIntRound(): Int = round(this).toInt()
/** Converts [this] into [Int] rounding to the nearest */
fun Float.toLongRound(): Long = round(this).toLong()
/** Converts [this] into [Int] rounding to the nearest */
fun Double.toLongRound(): Long = round(this).toLong()
/** Convert this [Long] into an [Int] but throws an [IllegalArgumentException] in the case that operation would produce an overflow */
fun Long.toIntSafe(): Int = if (this in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) this.toInt() else throw IllegalArgumentException("Long doesn't fit Integer")
/** Converts [this] into [Int] rounding to the floor */
fun Float.toIntFloor(): Int = floor(this).toInt()
/** Converts [this] into [Int] rounding to the floor */
fun Double.toIntFloor(): Int = floor(this).toInt()

View File

@@ -0,0 +1,35 @@
package com.icegps.math
private val MINUS_ZERO_F = -0.0f
////////////////////
////////////////////
/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */
public infix fun Int.umod(other: Int): Int {
val rm = this % other
val remainder = if (rm == -0) 0 else rm
return when {
remainder < 0 -> remainder + other
else -> remainder
}
}
/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */
public infix fun Double.umod(other: Double): Double {
val rm = this % other
val remainder = if (rm == -0.0) 0.0 else rm
return when {
remainder < 0.0 -> remainder + other
else -> remainder
}
}
public infix fun Float.umod(other: Float): Float {
val rm = this % other
val remainder = if (rm == MINUS_ZERO_F) 0f else rm
return when {
remainder < 0f -> remainder + other
else -> remainder
}
}

View File

@@ -0,0 +1,13 @@
package com.icegps.math
////////////////////
////////////////////
/** Returns an [Int] representing this [Byte] as if it was unsigned 0x00..0xFF */
inline val Byte.unsigned: Int get() = this.toInt() and 0xFF
/** Returns an [Int] representing this [Short] as if it was unsigned 0x0000..0xFFFF */
inline val Short.unsigned: Int get() = this.toInt() and 0xFFFF
/** Returns a [Long] representing this [Int] as if it was unsigned 0x00000000L..0xFFFFFFFFL */
inline val Int.unsigned: Long get() = this.toLong() and 0xFFFFFFFFL

View File

@@ -0,0 +1,41 @@
@file:Suppress("PackageDirectoryMismatch")
package com.icegps.math.annotations
@DslMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
annotation class KorDslMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@DslMarker
annotation class ViewDslMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@DslMarker
annotation class RootViewDslMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@DslMarker
annotation class VectorDslMarker
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
annotation class KormaExperimental(val reason: String = "")
//@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
/**
* Mutable APIs follow the following convention:
*
* ```kotlin
* interface IType { val ... }
* class MType : IType(override var ...) : IType
* ```
*
* Then in usage places:
*
* ```kotlin
* fun doSomethingWith(a: IType, out: MType = MType()): MType
* ```
*
* This convention supports allocation-free APIs by being able to preallocate instances and passing them as the output.
*/
annotation class KormaMutableApi

View File

@@ -0,0 +1,55 @@
package com.icegps.math.geometry
import com.icegps.math.geometry.shape.*
import kotlin.math.*
data class AABB3D(val min: Vector3F = Vector3F(), val max: Vector3F = Vector3F()) : SimpleShape3D {
val minX: Float get() = min.x
val minY: Float get() = min.y
val minZ: Float get() = min.z
val maxX: Float get() = max.x
val maxY: Float get() = max.y
val maxZ: Float get() = max.z
val sizeX: Float get() = maxX - minX
val sizeY: Float get() = maxY - minY
val sizeZ: Float get() = maxZ - minZ
companion object {
operator fun invoke(min: Float = Float.POSITIVE_INFINITY, max: Float = Float.NEGATIVE_INFINITY): AABB3D =
AABB3D(Vector3F(min, min, min), Vector3F(max, max, max))
fun fromSphere(pos: Vector3F, radius: Float): AABB3D = AABB3D(
Vector3F(pos.x - radius, pos.y - radius, pos.z - radius),
Vector3F(pos.x + radius, pos.y + radius, pos.z + radius)
)
}
fun expandedToFit(that: AABB3D): AABB3D {
val a = this
val b = that
return AABB3D(
min = Vector3F(min(a.minX, b.minX), min(a.minY, b.minY), min(a.minZ, b.minZ)),
max = Vector3F(max(a.maxX, b.maxX), max(a.maxY, b.maxY), max(a.maxZ, b.maxZ)),
)
}
fun intersectsSphere(sphere: Sphere3D): Boolean = intersectsSphere(sphere.center, sphere.radius)
fun intersectsSphere(origin: Vector3F, radius: Float): Boolean = !(origin.x + radius < minX ||
origin.y + radius < minY ||
origin.z + radius < minZ ||
origin.x - radius > maxX ||
origin.y - radius > maxY ||
origin.z - radius > maxZ)
fun intersectsAABB(box: AABB3D): Boolean = max.x > box.min.x && min.x < box.max.x &&
max.y > box.min.y && min.y < box.max.y &&
max.z > box.min.z && min.z < box.max.z
override val center: Vector3F get() = (min + max) * 0.5f
override val volume: Float get() {
val v = (max - min)
return v.x * v.y * v.z
}
}

View File

@@ -0,0 +1,103 @@
package com.icegps.math.geometry
import com.icegps.math.interpolation.*
typealias Anchor = Anchor2D
typealias Anchor3 = Anchor3F
data class Anchor2D(val sx: Double, val sy: Double) : Interpolable<Anchor> {
fun toVector(): Vector2D = Vector2D(sx, sy)
val ratioX: Ratio get() = sx.toRatio()
val ratioY: Ratio get() = sy.toRatio()
constructor(sx: Float, sy: Float) : this(sx.toDouble(), sy.toDouble())
constructor(sx: Int, sy: Int) : this(sx.toDouble(), sy.toDouble())
inline fun withX(sx: Number): Anchor = Anchor(sx.toDouble(), sy)
inline fun withY(sy: Number): Anchor = Anchor(sx, sy.toDouble())
inline fun withX(ratioX: Ratio): Anchor = Anchor(ratioX.toDouble(), sy)
inline fun withY(ratioY: Ratio): Anchor = Anchor(sx, ratioY.toDouble())
companion object {
inline operator fun invoke(sx: Ratio, sy: Ratio): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble())
inline operator fun invoke(sx: Number, sy: Number): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble())
val TOP_LEFT: Anchor = Anchor(0f, 0f)
val TOP_CENTER: Anchor = Anchor(.5f, 0f)
val TOP_RIGHT: Anchor = Anchor(1f, 0f)
val MIDDLE_LEFT: Anchor = Anchor(0f, .5f)
val MIDDLE_CENTER: Anchor = Anchor(.5f, .5f)
val MIDDLE_RIGHT: Anchor = Anchor(1f, .5f)
val BOTTOM_LEFT: Anchor = Anchor(0f, 1f)
val BOTTOM_CENTER: Anchor = Anchor(.5f, 1f)
val BOTTOM_RIGHT: Anchor = Anchor(1f, 1f)
val TOP: Anchor get() = TOP_CENTER
val LEFT: Anchor get() = MIDDLE_LEFT
val RIGHT: Anchor get() = MIDDLE_RIGHT
val BOTTOM: Anchor get() = BOTTOM_CENTER
val CENTER: Anchor get() = MIDDLE_CENTER
}
override fun interpolateWith(ratio: Ratio, other: Anchor): Anchor = Anchor(
ratio.interpolate(this.sx, other.sx),
ratio.interpolate(this.sy, other.sy)
)
fun toNamedString(): String = when (this) {
TOP_LEFT -> "Anchor.TOP_LEFT"
TOP -> "Anchor.TOP"
TOP_RIGHT -> "Anchor.TOP_RIGHT"
LEFT -> "Anchor.LEFT"
CENTER -> "Anchor.MIDDLE_CENTER"
RIGHT -> "Anchor.RIGHT"
BOTTOM_LEFT -> "Anchor.BOTTOM_LEFT"
BOTTOM_CENTER -> "Anchor.BOTTOM_CENTER"
BOTTOM_RIGHT -> "Anchor.BOTTOM_RIGHT"
else -> toString()
}
}
operator fun Size.times(anchor: Anchor): Point = this.toVector() * anchor.toVector()
//operator fun SizeInt.times(anchor: Anchor): PointInt = (this.toVector().toFloat() * anchor.toVector()).toInt()
data class Anchor3F(val sx: Float, val sy: Float, val sz: Float) : Interpolable<Anchor3F> {
fun toVector(): Vector3F = Vector3F(sx, sy, sz)
val floatX: Float get() = sx
val floatY: Float get() = sy
val floatZ: Float get() = sz
val doubleX: Double get() = sx.toDouble()
val doubleY: Double get() = sy.toDouble()
val doubleZ: Double get() = sz.toDouble()
val ratioX: Ratio get() = sx.toRatio()
val ratioY: Ratio get() = sy.toRatio()
val ratioZ: Ratio get() = sz.toRatio()
constructor(sx: Double, sy: Double, sz: Double) : this(sx.toFloat(), sy.toFloat(), sz.toFloat())
constructor(sx: Int, sy: Int, sz: Int) : this(sx.toFloat(), sy.toFloat(), sz.toFloat())
fun withX(sx: Float): Anchor3F = Anchor3F(sx, sy, sz)
fun withX(sx: Int): Anchor3F = Anchor3F(sx.toFloat(), sy, sz)
fun withX(sx: Double): Anchor3F = Anchor3F(sx.toFloat(), sy, sz)
fun withY(sy: Float): Anchor3F = Anchor3F(sx, sy, sz)
fun withY(sy: Int): Anchor3F = Anchor3F(sx, sy.toFloat(), sz)
fun withY(sy: Double): Anchor3F = Anchor3F(sx, sy.toFloat(), sz)
fun withZ(sz: Float): Anchor3F = Anchor3F(sx, sy, sz)
fun withZ(sz: Int): Anchor3F = Anchor3F(sx, sy, sz.toFloat())
fun withZ(sz: Double): Anchor3F = Anchor3F(sx, sy, sz.toFloat())
override fun interpolateWith(ratio: Ratio, other: Anchor3F): Anchor3F = Anchor3F(
ratio.interpolate(this.sx, other.sx),
ratio.interpolate(this.sy, other.sy),
ratio.interpolate(this.sz, other.sz),
)
}

View File

@@ -0,0 +1,250 @@
package com.icegps.math.geometry
import com.icegps.math.*
import com.icegps.math.interpolation.*
import com.icegps.math.range.*
import com.icegps.number.*
import kotlin.math.*
@PublishedApi internal const val PI2 = PI * 2.0
@PublishedApi internal const val DEG2RAD = PI / 180.0
@PublishedApi internal const val RAD2DEG = 180.0 / PI
@PublishedApi internal fun Angle_shortDistanceTo(from: Angle, to: Angle): Angle {
val r0 = from.ratio.toDouble() umod 1.0
val r1 = to.ratio.toDouble() umod 1.0
val diff = (r1 - r0 + 0.5) % 1.0 - 0.5
return if (diff < -0.5) Angle.fromRatio(diff + 1.0) else Angle.fromRatio(diff)
}
@PublishedApi internal fun Angle_longDistanceTo(from: Angle, to: Angle): Angle {
val short = Angle_shortDistanceTo(from, to)
return when {
short == Angle.ZERO -> Angle.ZERO
short < Angle.ZERO -> Angle.FULL + short
else -> -Angle.FULL + short
}
}
@PublishedApi internal fun Angle_between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle {
val angle = Angle.atan2(y1 - y0, x1 - x0)
return (if (angle < Angle.ZERO) angle + Angle.FULL else angle).adjustFromUp(up)
}
@PublishedApi internal fun Angle.adjustFromUp(up: Vector2D): Angle {
Orientation.checkValidUpVector(up)
return if (up.y > 0) this else -this
}
/**
* Represents an [Angle], [ratio] is in [0, 1] range, [radians] is in [0, 2PI] range, and [degrees] in [0, 360] range
* The internal representation is in [0, 1] range to reduce rounding errors, since floating points can represent
* a lot of values in that range.
*
* The equivalent old [Angle] constructor is now [Angle.fromRadians]
*
* Angles advance counter-clock-wise, starting with 0.degrees representing the right vector:
*
* Depending on what the up vector means, then numeric values of sin might be negated.
*
* 0.degrees represent right: up=Vector2.UP: cos =+1, sin= 0 || up=Vector2.UP_SCREEN: cos =+1, sin= 0
* 90.degrees represents up: up=Vector2.UP: cos = 0, sin=+1 || up=Vector2.UP_SCREEN: cos = 0, sin=-1
* 180.degrees represents left: up=Vector2.UP: cos =-1, sin= 0 || up=Vector2.UP_SCREEN: cos =-1, sin= 0
* 270.degrees represents down: up=Vector2.UP: cos = 0, sin=-1 || up=Vector2.UP_SCREEN: cos = 0, sin=+1
*/
//@KormaValueApi
inline class Angle @PublishedApi internal constructor(
/** [0..1] ratio -> [0..360] degrees */
val radians: Double
) : Comparable<Angle>, IsAlmostEquals<Angle> {
@PublishedApi inline internal val internal: Double get() = radians
/** [0..PI * 2] radians -> [0..360] degrees */
val ratio: Ratio get() = radiansToRatio(radians)
/** [0..360] degrees -> [0..PI * 2] radians -> [0..1] ratio */
val degrees: Double get() = radiansToDegrees(radians)
val cosine: Double get() = kotlin.math.cos(radians)
val sine: Double get() = kotlin.math.sin(radians)
val tangent: Double get() = kotlin.math.tan(radians)
fun cosine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).cosine
fun sine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).sine
fun tangent(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).tangent
val absoluteValue: Angle get() = Angle(internal.absoluteValue)
fun shortDistanceTo(other: Angle): Angle = Angle.shortDistanceTo(this, other)
fun longDistanceTo(other: Angle): Angle = Angle.longDistanceTo(this, other)
operator fun times(scale: Double): Angle = Angle(this.internal * scale)
operator fun div(scale: Double): Angle = Angle(this.internal / scale)
operator fun times(scale: Float): Angle = Angle(this.internal * scale)
operator fun div(scale: Float): Angle = Angle(this.internal / scale)
operator fun times(scale: Int): Angle = Angle(this.internal * scale)
operator fun div(scale: Int): Angle = Angle(this.internal / scale)
operator fun rem(angle: Angle): Angle = Angle(this.internal % angle.internal)
infix fun umod(angle: Angle): Angle = Angle(this.internal umod angle.internal)
operator fun div(other: Angle): Double = this.internal / other.internal // Ratio
operator fun plus(other: Angle): Angle = Angle(this.internal + other.internal)
operator fun minus(other: Angle): Angle = Angle(this.internal - other.internal)
operator fun unaryMinus(): Angle = Angle(-internal)
operator fun unaryPlus(): Angle = Angle(+internal)
fun inBetweenInclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = true)
fun inBetweenExclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = false)
infix fun inBetween(range: ClosedRange<Angle>): Boolean = inBetween(range.start, range.endInclusive, inclusive = true)
infix fun inBetween(range: OpenRange<Angle>): Boolean = inBetween(range.start, range.endExclusive, inclusive = false)
fun inBetween(min: Angle, max: Angle, inclusive: Boolean): Boolean {
val nthis = this.normalized
val nmin = min.normalized
val nmax = max.normalized
@Suppress("ConvertTwoComparisonsToRangeCheck")
return when {
nmin > nmax -> nthis >= nmin || (if (inclusive) nthis <= nmax else nthis < nmax)
else -> nthis >= nmin && (if (inclusive) nthis <= nmax else nthis < nmax)
}
}
override fun isAlmostEquals(other: Angle, epsilon: Double): Boolean = this.radians.isAlmostEquals(other.radians, epsilon)
fun isAlmostZero(epsilon: Double = 0.001): Boolean = isAlmostEquals(ZERO, epsilon)
/** Normalize between 0..1 ... 0..(PI*2).radians ... 0..360.degrees */
val normalized: Angle get() = fromRatio(ratio.toDouble() umod 1.0)
/** Normalize between -.5..+.5 ... -PI..+PI.radians ... -180..+180.degrees */
val normalizedHalf: Angle get() {
val res = normalized
return if (res > Angle.HALF) -Angle.FULL + res else res
}
override operator fun compareTo(other: Angle): Int = this.ratio.compareTo(other.ratio)
//override fun compareTo(other: Angle): Int {
// //return this.radians.compareTo(other.radians) // @TODO: Double.compareTo calls EnterFrame/LeaveFrame! because it uses a Double companion object
// val left = this.ratio
// val right = other.ratio
// // @TODO: Handle infinite/NaN? Though usually this won't happen
// if (left < right) return -1
// if (left > right) return +1
// return 0
//}
override fun toString(): String = "${degrees.roundDecimalPlaces(2).niceStr}.degrees"
@Suppress("MemberVisibilityCanBePrivate")
companion object {
val EPSILON = Angle.fromRatio(0.00001)
val ZERO = Angle.fromRatio(0.0)
val QUARTER = Angle.fromRatio(0.25)
val HALF = Angle.fromRatio(0.5)
val THREE_QUARTERS = Angle.fromRatio(0.75)
val FULL = Angle.fromRatio(1.0)
inline fun fromRatio(ratio: Float): Angle = Angle(ratioToRadians(ratio.toRatio()))
inline fun fromRatio(ratio: Double): Angle = Angle(ratioToRadians(ratio.toRatio()))
inline fun fromRatio(ratio: Ratio): Angle = Angle(ratioToRadians(ratio))
inline fun fromRadians(radians: Double): Angle = Angle(radians)
inline fun fromRadians(radians: Float) = Angle(radians.toDouble())
inline fun fromRadians(radians: Int) = Angle(radians.toDouble())
inline fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees))
inline fun fromDegrees(degrees: Float) = Angle(degreesToRadians(degrees.toDouble()))
inline fun fromDegrees(degrees: Int) = Angle(degreesToRadians(degrees.toDouble()))
@Deprecated("", ReplaceWith("Angle.fromRatio(ratio).cosineD"))
inline fun cos01(ratio: Double): Double = Angle.fromRatio(ratio).cosine
@Deprecated("", ReplaceWith("Angle.fromRatio(ratio).sineD"))
inline fun sin01(ratio: Double): Double = Angle.fromRatio(ratio).sine
@Deprecated("", ReplaceWith("Angle.fromRatio(ratio).tangentD"))
inline fun tan01(ratio: Double): Double = Angle.fromRatio(ratio).tangent
inline fun atan2(x: Float, y: Float, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
inline fun atan2(x: Double, y: Double, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
inline fun atan2(p: Point, up: Vector2D = Vector2D.UP): Angle = atan2(p.x, p.y, up)
inline fun asin(v: Double): Angle = kotlin.math.asin(v).radians
inline fun asin(v: Float): Angle = kotlin.math.asin(v).radians
inline fun acos(v: Double): Angle = kotlin.math.acos(v).radians
inline fun acos(v: Float): Angle = kotlin.math.acos(v).radians
fun arcCosine(v: Double): Angle = kotlin.math.acos(v).radians
fun arcCosine(v: Float): Angle = kotlin.math.acos(v).radians
fun arcSine(v: Double): Angle = kotlin.math.asin(v).radians
fun arcSine(v: Float): Angle = kotlin.math.asin(v).radians
fun arcTangent(x: Double, y: Double): Angle = kotlin.math.atan2(x, y).radians
fun arcTangent(x: Float, y: Float): Angle = kotlin.math.atan2(x, y).radians
fun arcTangent(v: Vector2F): Angle = kotlin.math.atan2(v.x, v.y).radians
inline fun ratioToDegrees(ratio: Ratio): Double = ratio * 360.0
inline fun ratioToRadians(ratio: Ratio): Double = ratio * PI2
inline fun degreesToRatio(degrees: Double): Ratio = Ratio(degrees / 360.0)
inline fun degreesToRadians(degrees: Double): Double = degrees * DEG2RAD
inline fun radiansToRatio(radians: Double): Ratio = Ratio(radians / PI2)
inline fun radiansToDegrees(radians: Double): Double = radians * RAD2DEG
inline fun shortDistanceTo(from: Angle, to: Angle): Angle = Angle_shortDistanceTo(from, to)
inline fun longDistanceTo(from: Angle, to: Angle): Angle = Angle_longDistanceTo(from, to)
inline fun between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle = Angle_between(x0, y0, x1, y1, up)
inline fun between(x0: Int, y0: Int, x1: Int, y1: Int, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)
inline fun between(x0: Float, y0: Float, x1: Float, y1: Float, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)
inline fun between(p0: Point, p1: Point, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)
inline fun between(p0: Vector2F, p1: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)
inline fun between(ox: Double, oy: Double, x1: Double, y1: Double, x2: Double, y2: Double, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)
inline fun between(ox: Float, oy: Float, x1: Float, y1: Float, x2: Float, y2: Float, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)
inline fun between(o: Point, v1: Point, v2: Point, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
inline fun between(o: Vector2F, v1: Vector2F, v2: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
}
}
inline fun cos(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.cosine(up)
inline fun sin(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.sine(up)
inline fun tan(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.tangent(up)
inline fun cosf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.cosine(up).toFloat()
inline fun sinf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.sine(up).toFloat()
inline fun tanf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.tangent(up).toFloat()
inline fun abs(angle: Angle): Angle = angle.absoluteValue
inline fun min(a: Angle, b: Angle): Angle = Angle(min(a.internal, b.internal))
inline fun max(a: Angle, b: Angle): Angle = Angle(max(a.internal, b.internal))
fun Angle.clamp(min: Angle, max: Angle): Angle = min(max(this, min), max)
operator fun ClosedRange<Angle>.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endInclusive, inclusive = true)
operator fun OpenRange<Angle>.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endExclusive, inclusive = false)
infix fun Angle.until(other: Angle): OpenRange<Angle> = OpenRange(this, other)
val Double.degrees: Angle get() = Angle.fromDegrees(this)
val Double.radians: Angle get() = Angle.fromRadians(this)
val Int.degrees: Angle get() = Angle.fromDegrees(this)
val Int.radians: Angle get() = Angle.fromRadians(this)
val Float.degrees: Angle get() = Angle.fromDegrees(this)
val Float.radians: Angle get() = Angle.fromRadians(this)
fun Ratio.interpolateAngle(l: Angle, r: Angle, minimizeAngle: Boolean): Angle = _interpolateAngleAny(this, l, r, minimizeAngle)
fun Ratio.interpolateAngle(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true)
fun Ratio.interpolateAngleNormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true)
fun Ratio.interpolateAngleDenormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = false)
private fun _interpolateAngleAny(ratio: Ratio, l: Angle, r: Angle, minimizeAngle: Boolean = true): Angle {
if (!minimizeAngle) return Angle.fromRatio(ratio.interpolate(l.ratio, r.ratio))
val ln = l.normalized
val rn = r.normalized
return when {
(rn - ln).absoluteValue <= Angle.HALF -> Angle.fromRadians(ratio.interpolate(ln.radians, rn.radians))
ln < rn -> Angle.fromRadians(ratio.interpolate((ln + Angle.FULL).radians, rn.radians)).normalized
else -> Angle.fromRadians(ratio.interpolate(ln.radians, (rn + Angle.FULL).radians)).normalized
}
}

View File

@@ -0,0 +1,60 @@
package com.icegps.math.geometry
inline class BoundsBuilder(val bounds: Rectangle) {
val isEmpty: Boolean get() = bounds.isNIL
val isNotEmpty: Boolean get() = bounds.isNotNIL
val xmin: Double get() = kotlin.math.min(bounds.left, bounds.right)
val xmax: Double get() = kotlin.math.max(bounds.left, bounds.right)
val ymin: Double get() = kotlin.math.min(bounds.top, bounds.bottom)
val ymax: Double get() = kotlin.math.max(bounds.top, bounds.bottom)
/** Minimum value found for X. [default] if ![hasPoints] */
fun xminOr(default: Double = 0.0): Double = if (hasPoints) xmin else default
/** Maximum value found for X. [default] if ![hasPoints] */
fun xmaxOr(default: Double = 0.0): Double = if (hasPoints) xmax else default
/** Minimum value found for Y. [default] if ![hasPoints] */
fun yminOr(default: Double = 0.0): Double = if (hasPoints) ymin else default
/** Maximum value found for Y. [default] if ![hasPoints] */
fun ymaxOr(default: Double = 0.0): Double = if (hasPoints) ymax else default
val hasPoints: Boolean get() = isNotEmpty
companion object {
val EMPTY = BoundsBuilder(Rectangle.NIL)
operator fun invoke(): BoundsBuilder = EMPTY
operator fun invoke(p1: Point): BoundsBuilder = BoundsBuilder(Rectangle(p1, Size(0, 0)))
operator fun invoke(p1: Point, p2: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2), Point.maxComponents(p1, p2)))
operator fun invoke(p1: Point, p2: Point, p3: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3), Point.maxComponents(p1, p2, p3)))
operator fun invoke(p1: Point, p2: Point, p3: Point, p4: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3, p4), Point.maxComponents(p1, p2, p3, p4)))
operator fun invoke(size: Int, func: BoundsBuilder.(Int) -> BoundsBuilder): BoundsBuilder {
var bb = BoundsBuilder()
for (n in 0 until size) bb = func(bb, n)
return bb
}
}
fun plus(x: Double, y: Double): BoundsBuilder = this.plus(Point(x, y))
operator fun plus(p: Point): BoundsBuilder {
if (bounds.isNIL) return BoundsBuilder(Rectangle(p, Size(0, 0)))
return BoundsBuilder(Rectangle.fromBounds(Point.minComponents(bounds.topLeft, p), Point.maxComponents(bounds.bottomRight, p)))
}
operator fun plus(bb: BoundsBuilder): BoundsBuilder = this + bb.bounds
operator fun plus(rect: Rectangle?): BoundsBuilder {
if (rect == null) return this
if (rect.isNIL) return this
return this + rect.topLeft + rect.bottomRight
}
operator fun plus(p: IPointList): BoundsBuilder {
var bb = this
for (n in 0 until p.size) bb = bb.plus(p[n])
return bb
}
//operator fun plus(rect: Rectangle): BoundsBuilder = TODO()
operator fun plus(rects: List<Rectangle>): BoundsBuilder {
var bb = this
for (it in rects) bb += it
return bb
}
fun boundsOrNull(): Rectangle? = if (isEmpty) null else bounds
}

View File

@@ -0,0 +1,29 @@
package com.icegps.math.geometry
import com.icegps.math.geometry.shape.*
import kotlin.math.*
data class Circle(override val center: Point, val radius: Double) : SimpleShape2D {
companion object {
inline operator fun invoke(center: Point, radius: Number) = Circle(center, radius.toDouble())
inline operator fun invoke(x: Number, y: Number, radius: Number) = Circle(Point(x.toDouble(), y.toDouble()), radius.toDouble())
}
override val closed: Boolean get() = true
override val area: Double get() = (PI * radius * radius)
override val perimeter: Double get() = (PI * 2.0 * radius)
override fun distance(p: Point): Double = (p - center).length - radius
override fun normalVectorAt(p: Point): Vector2D = (p - center).normalized
val radiusSquared: Double get() = radius * radius
fun distanceToCenterSquared(p: Point): Double = Point.distanceSquared(p, center)
// @TODO: Check if inside the circle
fun distanceClosestSquared(p: Point): Double = distanceToCenterSquared(p) - radiusSquared
// @TODO: Check if inside the circle
fun distanceFarthestSquared(p: Point): Double = distanceToCenterSquared(p) + radiusSquared
override fun projectedPoint(p: Point): Point = Point.polar(center, Angle.between(center, p), radius)
override fun containsPoint(p: Point): Boolean = (p - center).length <= radius
override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius, center.y - radius, center.x + radius, center.y + radius,)
}

View File

@@ -0,0 +1,83 @@
package com.icegps.math.geometry
import com.icegps.math.geometry.shape.*
import kotlin.math.*
data class Ellipse(override val center: Point, val radius: Size) : SimpleShape2D {
override val area: Double get() = (PI * radius.width * radius.height)
override val perimeter: Double get() {
if (radius.width == radius.height) return (PI * 2.0 * radius.width) // Circle formula
val (a, b) = radius
val h = ((a - b) * (a - b)) / ((a + b) * (a + b))
return (PI * (a + b) * (1 + ((3 * h) / (10 + sqrt(4 - (3 * h))))))
}
override fun distance(p: Point): Double {
val p = p - center
val scaledPoint = Vector2D(p.x / radius.width, p.y / radius.height)
val length = scaledPoint.length
return (length - 1) * min(radius.width, radius.height)
}
override fun normalVectorAt(p: Point): Vector2D {
val pointOnEllipse = p - center
val (a, b) = radius
val normal = Vector2D(pointOnEllipse.x / (a * a), pointOnEllipse.y / (b * b))
return normal.normalized
//val d = p - center
//val r2 = radius.toVector() * radius.toVector()
//return (d / r2).normalized
}
override fun projectedPoint(p: Point): Point {
val angle = Angle.between(center, p)
return center + Point(radius.width * angle.cosine, radius.height * angle.sine)
//val k = (radius.width * radius.height) / sqrt()
//return projectPointOntoEllipse(p, center, radius.toVector())
}
override fun containsPoint(p: Point): Boolean {
if (radius.isEmpty()) return false
// Check if the point is inside the ellipse using the ellipse equation:
// (x - centerX)^2 / radiusX^2 + (y - centerY)^2 / radiusY^2 <= 1
return ((p.x - center.x).pow(2) / radius.width.pow(2)) + ((p.y - center.y).pow(2) / radius.height.pow(2)) <= 1
}
override val closed: Boolean get() = true
override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius.width, center.y - radius.height, center.x + radius.width, center.y + radius.height)
companion object {
private fun projectPointOntoEllipse(point: Vector2F, center: Vector2F, radius: Vector2F, tolerance: Double = 1e-6, maxIterations: Int = 100): Vector2F {
var currentPoint = point
var i = 0
while (i < maxIterations) {
val dx = currentPoint.x - center.x
val dy = currentPoint.y - center.y
val rx2 = radius.x * radius.x
val ry2 = radius.y * radius.y
val f = Vector2F(
(dx * rx2 - dy * dx * dy) / (rx2 * ry2),
(dy * ry2 - dx * dy * dx) / (rx2 * ry2)
)
val df = Vector2F(
(ry2 - 2.0 * dy * dy) / (rx2 * ry2),
(rx2 - 2.0 * dx * dx) / (rx2 * ry2)
)
val nextPoint = currentPoint - f / df
val dist = (nextPoint - currentPoint).length
if (dist < tolerance) return nextPoint
currentPoint = nextPoint
i++
}
return currentPoint
}
}
}

View File

@@ -0,0 +1,330 @@
package com.icegps.math.geometry
import com.icegps.math.*
import kotlin.math.*
/**
* Rotations around Z axis, then X axis, then Y axis in that order.
*/
inline class EulerRotation private constructor(val data: Vector4F) : IsAlmostEqualsF<EulerRotation> {
val config: Config get() = Config(data.w.toInt())
val order: Order get() = config.order
val coordinateSystem: CoordinateSystem get() = config.coordinateSystem
enum class Order(
val x: Int, val y: Int, val z: Int, val w: Int, val str: String,
) {
INVALID(0, 0, 0, 0, "XXX"),
XYZ(+1, -1, +1, -1, "XYZ"),
XZY(-1, -1, +1, +1, "XZY"),
YXZ(+1, -1, -1, +1, "YXZ"),
YZX(+1, +1, -1, -1, "YZX"),
ZXY(-1, +1, +1, -1, "ZXY"),
ZYX(-1, +1, -1, +1, "ZYX"),
;
fun withCoordinateSystem(coordinateSystem: CoordinateSystem) = if (coordinateSystem.sign < 0) reversed() else this
fun reversed(): Order = when (this) {
INVALID -> INVALID
XYZ -> ZYX
XZY -> YZX
YXZ -> ZXY
YZX -> XZY
ZXY -> YXZ
ZYX -> XYZ
}
fun indexAt(pos: Int, reversed: Boolean = false): Int = str[(if (reversed) 2 - pos else pos) umod 3] - 'X'
override fun toString(): String = "$name [$x, $y, $z, $w]"
companion object {
val VALUES = values()
val DEFAULT = XYZ
}
}
//enum class Normalized { NO, FULL_ANGLE, HALF_ANGLE }
inline class Config(val id: Int) {
//constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.ordinal * coordinateSystem.sign)
constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.withCoordinateSystem(coordinateSystem).ordinal)
val order: Order get() = Order.VALUES[id.absoluteValue]
val coordinateSystem: CoordinateSystem get() = if (id < 0) CoordinateSystem.LEFT_HANDED else CoordinateSystem.RIGHT_HANDED
override fun toString(): String = "EulerRotation.Config(order=$order, coordinateSystem=$coordinateSystem)"
companion object {
val UNITY get() = Config(Order.ZXY, CoordinateSystem.LEFT_HANDED)
//val UNITY get() = LIBGDX
val UNREAL get() = Config(Order.ZYX, CoordinateSystem.LEFT_HANDED)
//val UNREAL get() = THREEJS
val GODOT get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED)
val LIBGDX get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED)
val THREEJS get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED)
// Same as Three.JS
val DEFAULT get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED)
}
}
enum class CoordinateSystem(val sign: Int) {
LEFT_HANDED(-1), RIGHT_HANDED(+1);
val rsign = -sign
}
val roll: Angle get() = Angle.fromRatio(data.x)
val pitch: Angle get() = Angle.fromRatio(data.y)
val yaw: Angle get() = Angle.fromRatio(data.z)
@Deprecated("", ReplaceWith("roll")) val x: Angle get() = roll
@Deprecated("", ReplaceWith("pitch")) val y: Angle get() = pitch
@Deprecated("", ReplaceWith("yaw")) val z: Angle get() = yaw
override fun toString(): String = "EulerRotation(roll=$roll, pitch=$pitch, yaw=$yaw)"
fun copy(roll: Angle = this.roll, pitch: Angle = this.pitch, yaw: Angle = this.yaw): EulerRotation = EulerRotation(roll, pitch, yaw)
constructor() : this(Angle.ZERO, Angle.ZERO, Angle.ZERO)
constructor(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT)
: this(Vector4F(roll.ratio.toFloat(), pitch.ratio.toFloat(), yaw.ratio.toFloat(), config.id.toFloat()))
fun normalized(): EulerRotation = EulerRotation(roll.normalized, pitch.normalized, yaw.normalized)
fun normalizedHalf(): EulerRotation = EulerRotation(roll.normalizedHalf, pitch.normalizedHalf, yaw.normalizedHalf)
fun toMatrix(): Matrix4 = toQuaternion().toMatrix()
fun toQuaternion(): Quaternion = _toQuaternion(x, y, z, config)
override fun isAlmostEquals(other: EulerRotation, epsilon: Float): Boolean =
this.data.isAlmostEquals(other.data, epsilon)
companion object {
fun toQuaternion(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT): Quaternion {
return _toQuaternion(roll, pitch, yaw, config)
}
// http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m
private fun _toQuaternion(x: Angle, y: Angle, z: Angle, config: Config = Config.DEFAULT): Quaternion {
val order = config.order
val coordinateSystem = config.coordinateSystem
val sign = coordinateSystem.sign
//println("ORDER=$order, coordinateSystem=$coordinateSystem, sign=$sign")
val c1 = cos(x / 2)
val c2 = cos(y / 2)
val c3 = cos(z / 2)
val s1 = sin(x / 2)
val s2 = sin(y / 2)
val s3 = sin(z / 2)
return Quaternion(
((s1 * c2 * c3) + ((c1 * s2 * s3) * order.x * sign)),
((c1 * s2 * c3) + ((s1 * c2 * s3) * order.y * sign)),
((c1 * c2 * s3) + ((s1 * s2 * c3) * order.z * sign)),
((c1 * c2 * c3) + ((s1 * s2 * s3) * order.w * sign)),
)
}
fun fromRotationMatrix(m: Matrix3, config: Config = Config.DEFAULT): EulerRotation {
//val config = if (config == Config.UNITY) Config.LIBGDX else config
val order = config.order
val coordinateSystem = config.coordinateSystem
val sign = coordinateSystem.sign
//val m = if (sign < 0) m.transposed() else m
//val m = m
val m11 = m.v00
val m12 = m.v01
val m13 = m.v02
val m21 = m.v10
val m22 = m.v11
val m23 = m.v12
val m31 = m.v20
val m32 = m.v21
val m33 = m.v22
val x: Angle
val y: Angle
val z: Angle
when (order) {
Order.XYZ -> {
x = if (m13.absoluteNotAlmostOne) Angle.atan2(-m23, m33) else Angle.atan2(m32, m22)
y = Angle.asin(m13.clamp(-1f, +1f))
z = if (m13.absoluteNotAlmostOne) Angle.atan2(-m12, m11) else Angle.ZERO
}
Order.YXZ -> {
x = Angle.asin(-(m23.clamp(-1f, +1f)))
y = if (m23.absoluteNotAlmostOne) Angle.atan2(m13, m33) else Angle.atan2(-m31, m11)
z = if (m23.absoluteNotAlmostOne) Angle.atan2(m21, m22) else Angle.ZERO
}
Order.ZXY -> {
y = Angle.asin(m32.clamp(-1f, +1f))
x = if (m32.absoluteNotAlmostOne) Angle.atan2(-m31, m33) else Angle.ZERO
z = if (m32.absoluteNotAlmostOne) Angle.atan2(-m12, m22) else Angle.atan2(m21, m11)
}
Order.ZYX -> {
x = if (m31.absoluteNotAlmostOne) Angle.atan2(m32, m33) else Angle.ZERO
y = Angle.asin(-(m31.clamp(-1f, +1f)))
z = if (m31.absoluteNotAlmostOne) Angle.atan2(m21, m11) else Angle.atan2(-m12, m22)
}
Order.YZX -> {
x = if (m21.absoluteNotAlmostOne) Angle.atan2(-m23, m22) else Angle.ZERO
y = if (m21.absoluteNotAlmostOne) Angle.atan2(-m31, m11) else Angle.atan2(m13, m33)
z = Angle.asin(m21.clamp(-1f, +1f))
}
Order.XZY -> {
x = if (m12.absoluteNotAlmostOne) Angle.atan2(m32, m22) else Angle.atan2(-m23, m33)
y = if (m12.absoluteNotAlmostOne) Angle.atan2(m13, m11) else Angle.ZERO
z = Angle.asin(-(m12.clamp(-1f, +1f)))
}
Order.INVALID -> error("Invalid")
}
//println("order=$order, coordinateSystem=$coordinateSystem : ${coordinateSystem.sign}, x=$x, y=$y, z=$z")
//val sign = coordinateSystem.sign
//return EulerRotation(x * coordinateSystem.sign, y * coordinateSystem.sign, z * coordinateSystem.sign, config)
//return EulerRotation(x * sign, y * sign, z * sign, config)
return EulerRotation(x, y, z, config)
}
private val Float.absoluteNotAlmostOne: Boolean get() = absoluteValue < 0.9999999
fun fromQuaternion(q: Quaternion, config: Config = Config.DEFAULT): EulerRotation {
return fromRotationMatrix(q.toMatrix3(), config)
/*
//return fromQuaternion(q.x, q.y, q.z, q.w, config)
val extrinsic = false
// intrinsic/extrinsic conversion helpers
val angle_first: Int
val angle_third: Int
val reversed: Boolean
if (extrinsic) {
angle_first = 0
angle_third = 2
reversed = false
} else {
reversed = true
//reversed = false
//seq = seq[:: - 1]
angle_first = 2
angle_third = 0
}
val quat = q
val i = config.order.indexAt(0, reversed = reversed)
val j = config.order.indexAt(1, reversed = reversed)
val symmetric = i == j
var k = if (symmetric) 3 - i - j else config.order.indexAt(2, reversed = reversed)
val sign = (i - j) * (j - k) * (k - i) / 2
println("ORDER: $i, $j, $k")
val eps = 1e-7f
val _angles = FloatArray(3)
//_angles = angles[ind, :]
// Step 1
// Permutate quaternion elements
val a: Float
val b: Float
val c: Float
val d: Float
if (symmetric) {
a = quat[3]
b = quat[i]
c = quat[j]
d = quat[k] * sign
} else {
a = quat[3] - quat[j]
b = quat[i] + quat[k] * sign
c = quat[j] + quat[3]
d = quat[k] * sign - quat[i]
}
// Step 2
// Compute second angle...
_angles[1] = 2 * atan2(hypot(c, d), hypot(a, b))
// ... and check if equal to is 0 or pi, causing a singularity
val case = when {
abs(_angles[1]) <= eps -> 1
abs(_angles[1] - PIF) <= eps -> 2
else -> 0 // normal case
}
// Step 3
// compute first and third angles, according to case
val half_sum = atan2(b, a)
val half_diff = atan2(d, c)
if (case == 0) { // no singularities
_angles[angle_first] = half_sum - half_diff
_angles[angle_third] = half_sum + half_diff
} else { // any degenerate case
_angles[2] = 0f
if (case == 1) {
_angles[0] = 2 * half_sum
} else {
_angles[0] = 2 * half_diff * (if (extrinsic) -1 else 1)
}
}
// for Tait-Bryan angles
if (!symmetric) {
_angles[angle_third] *= sign.toFloat()
_angles[1] -= PIF / 2
}
for (idx in 0 until 3) {
if (_angles[idx] < -PIF) {
_angles[idx] += 2 * PIF
} else if (_angles[idx] > PIF) {
_angles[idx] -= 2 * PIF
}
}
if (case != 0) {
println(
"Gimbal lock detected. Setting third angle to zero " +
"since it is not possible to uniquely determine " +
"all angles."
)
}
return EulerRotation(_angles[0].radians, _angles[2].radians, _angles[1].radians * config.coordinateSystem.sign)
*/
}
fun fromQuaternion(x: Float, y: Float, z: Float, w: Float, config: Config = Config.DEFAULT): EulerRotation {
return fromQuaternion(Quaternion(x, y, z, w), config)
/*
val t = y * x + z * w
// Gimbal lock, if any: positive (+1) for north pole, negative (-1) for south pole, zero (0) when no gimbal lock
val pole = if (t > 0.499f) 1 else if (t < -0.499f) -1 else 0
println("pole=$pole")
println(Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x)))
return EulerRotation(
roll = when (pole) {
0 -> Angle.asin((2f * (w * x - z * y)).clamp(-1f, +1f))
else -> (pole.toFloat() * PIF * .5f).radians
},
pitch = when (pole) {
0 -> Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x))
else -> Angle.ZERO
},
yaw = when (pole) {
0 -> Angle.atan2(2f * (w * z + y * x), 1f - 2f * (x * x + z * z))
else -> Angle.atan2(y, w) * pole.toFloat() * 2f
},
)
*/
}
}
}

View File

@@ -0,0 +1,124 @@
package com.icegps.math.geometry
import com.icegps.math.*
import com.icegps.number.*
import kotlin.math.*
interface IGenericDoubleVector {
val dimensions: Int
operator fun get(dim: Int): Double
operator fun set(dim: Int, value: Double)
}
interface IDoubleVectorList : IsAlmostEquals<IDoubleVectorList> {
fun isEmpty(): Boolean = size == 0
fun isNotEmpty(): Boolean = size != 0
val size: Int
val dimensions: Int
operator fun get(index: Int, dim: Int): Double
override fun isAlmostEquals(other: IDoubleVectorList, epsilon: Double): Boolean {
if (this.size != other.size) return false
if (this.dimensions != other.dimensions) return false
for (dim in 0 until dimensions) for (n in 0 until size) {
if (!this[n, dim].isAlmostEquals(other[n, dim], epsilon)) return false
}
return true
}
}
// @TODO: Potential candidate for value class when multiple values are supported
class GenericDoubleVector(override val dimensions: Int, val data: DoubleArray, val offset: Int = 0) : IGenericDoubleVector {
constructor(vararg data: Double) : this(data.size, data)
constructor(vararg data: Float) : this(data.size, DoubleArray(data.size) { data[it].toDouble() })
constructor(vararg data: Int) : this(data.size, DoubleArray(data.size) { data[it].toDouble() })
override operator fun get(dim: Int): Double = data[offset + dim]
override operator fun set(dim: Int, value: Double) { data[offset + dim] = value }
override fun toString(): String = buildString { toStringBuilder(this) }
}
val IGenericDoubleVector.length: Double get() {
var ssum = 0.0
for (n in 0 until dimensions) ssum += this[n]
return sqrt(ssum)
}
fun IGenericDoubleVector.toStringBuilder(out: StringBuilder) {
out.appendGenericArray(dimensions) { appendNice(this@toStringBuilder[it]) }
}
interface IPointList : IDoubleVectorList, List<Point> {
override val size: Int
override fun isEmpty(): Boolean = size == 0
fun getX(index: Int): Double
fun getY(index: Int): Double
override val dimensions: Int get() = 2
override operator fun get(index: Int): Point = Point(getX(index), getY(index))
override fun contains(element: Point): Boolean = indexOf(element) >= 0
override fun containsAll(elements: Collection<Point>): Boolean = containsAllSet(elements)
override fun indexOf(element: Point): Int = indexOf(this, element)
override fun lastIndexOf(element: Point): Int = lastIndexOf(this, element)
override fun iterator(): Iterator<Point> = listIterator()
override fun listIterator(): ListIterator<Point> = listIterator(0)
override fun listIterator(index: Int): ListIterator<Point> = Sublist(this, 0, size).listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int): List<Point> = Sublist(this, fromIndex, toIndex)
class Sublist(val list: IPointList, val fromIndex: Int, val toIndex: Int) : List<Point> {
override val size: Int = toIndex - fromIndex
override fun get(index: Int): Point = list[index + fromIndex]
override fun isEmpty(): Boolean = size == 0
override fun iterator(): Iterator<Point> = listIterator()
override fun listIterator(): ListIterator<Point> = listIterator(0)
override fun listIterator(index: Int): ListIterator<Point> = object : ListIterator<Point> {
var current = index
override fun hasNext(): Boolean = current >= size
override fun hasPrevious(): Boolean = current > index
override fun next(): Point = this@Sublist[current++]
override fun nextIndex(): Int = current + 1
override fun previous(): Point = this@Sublist[--current]
override fun previousIndex(): Int = current - 1
}
override fun subList(fromIndex: Int, toIndex: Int): List<Point> = Sublist(list, this.fromIndex + fromIndex, this.fromIndex + toIndex)
override fun lastIndexOf(element: Point): Int = lastIndexOf(list, element, fromIndex, toIndex, offset = -fromIndex)
override fun indexOf(element: Point): Int = indexOf(list, element, fromIndex, toIndex, offset = -fromIndex)
override fun containsAll(elements: Collection<Point>): Boolean = containsAllSet(elements)
override fun contains(element: Point): Boolean = indexOf(element) >= 0
}
companion object {
fun <T> Collection<T>.containsAllSet(elements: Collection<T>): Boolean {
val s = elements.toSet()
return all { it in s }
}
fun indexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int {
for (n in fromIndex until toIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset
return -1
}
fun lastIndexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int {
for (n in toIndex - 1 downTo fromIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset
return -1
}
inline fun getPolylineLength(size: Int, crossinline get: (n: Int) -> Point): Double {
var out = 0.0
var prev = Point.ZERO
for (n in 0 until size) {
val p = get(n)
if (n > 0) out += Point.distance(prev, p)
prev = p
}
return out
}
}
}
fun IPointList.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) }
fun List<Point>.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) }

View File

@@ -0,0 +1,175 @@
package com.icegps.math.geometry
import com.icegps.math.*
import com.icegps.math.annotations.*
import com.icegps.math.geometry.shape.*
import kotlin.math.*
typealias Line2 = Line
typealias Line = Line2D
//@KormaValueApi
data class Line2D(val a: Vector2D, val b: Vector2D) : SimpleShape2D {
override val closed: Boolean get() = false
override val area: Double get() = 0.0
override val perimeter: Double get() = length
override fun normalVectorAt(p: Point): Vector2D {
val projected = projectedPoint(p)
return (b - a).toNormal().normalized * Point.crossProduct(projected, p).sign
}
override val center: Point get() = (a + b) * 0.5
fun toRay(): Ray = Ray(a, (b - a).normalized)
val xmin: Double get() = kotlin.math.min(x0, x1)
val xmax: Double get() = kotlin.math.max(x0, x1)
val ymin: Double get() = kotlin.math.min(y0, y1)
val ymax: Double get() = kotlin.math.max(y0, y1)
override fun projectedPoint(p: Point): Point {
return projectedPointOutsideSegment(p).clamp(Point(xmin, ymin), Point(xmax, ymax))
}
fun projectedPointOutsideSegment(p: Point): Point {
val v1x = x0
val v2x = x1
val v1y = y0
val v2y = y1
val px = p.x
val py = p.y
// return this.getIntersectionPoint(Line(point, Point.fromPolar(point, this.angle + 90.degrees)))!!
// get dot product of e1, e2
val e1x = v2x - v1x
val e1y = v2y - v1y
val e2x = px - v1x
val e2y = py - v1y
val valDp = Point.dot(e1x, e1y, e2x, e2y)
// get length of vectors
val lenLineE1 = kotlin.math.hypot(e1x, e1y)
val lenLineE2 = kotlin.math.hypot(e2x, e2y)
// What happens if lenLineE1 or lenLineE2 are zero?, it would be a division by zero.
// Does that mean that the point is on the line, and we should use it?
if (lenLineE1 == 0.0 || lenLineE2 == 0.0) {
return Point(px, py)
}
val cos = valDp / (lenLineE1 * lenLineE2)
// length of v1P'
val projLenOfLine = cos * lenLineE2
return Point((v1x + (projLenOfLine * e1x) / lenLineE1), (v1y + (projLenOfLine * e1y) / lenLineE1))
}
override fun containsPoint(p: Point): Boolean = false
override fun getBounds(): Rectangle {
TODO("Not yet implemented")
}
constructor() : this(Point(), Point())
constructor(x0: Double, y0: Double, x1: Double, y1: Double) : this(Point(x0, y0), Point(x1, y1))
constructor(x0: Float, y0: Float, x1: Float, y1: Float) : this(Point(x0, y0), Point(x1, y1))
constructor(x0: Int, y0: Int, x1: Int, y1: Int) : this(Point(x0, y0), Point(x1, y1))
inline fun flipped(): Line = Line(b, a)
val x0: Double get() = a.x
val y0: Double get() = a.y
val x1: Double get() = b.x
val y1: Double get() = b.y
val dx: Double get() = x1 - x0
val dy: Double get() = y1 - y0
val min: Point get() = Point(minX, minY)
val minX: Double get() = kotlin.math.min(a.x, b.x)
val minY: Double get() = kotlin.math.min(a.y, b.y)
val max: Point get() = Point(maxX, maxY)
val maxX: Double get() = kotlin.math.max(a.x, b.x)
val maxY: Double get() = kotlin.math.max(a.y, b.y)
fun round(): Line = Line(a.round(), b.round())
fun directionVector(): Point = Point(dx, dy)
fun getMinimumDistance(p: Point): Double {
val v = a
val w = b
val l2 = Point.distanceSquared(v, w)
if (l2 == 0.0) return Point.distanceSquared(p, a)
val t = (Point.dot(p - v, w - v) / l2).clamp(0.0, 1.0)
return Point.distance(p, v + (w - v) * t)
}
@KormaExperimental
fun scaledPoints(scale: Double): Line {
val dx = this.dx
val dy = this.dy
return Line(x0 - dx * scale, y0 - dy * scale, x1 + dx * scale, y1 + dy * scale)
}
fun containsX(x: Double): Boolean = (x in x0..x1) || (x in x1..x0) || (almostEquals(x, x0)) || (almostEquals(x, x1))
fun containsY(y: Double): Boolean = (y in y0..y1) || (y in y1..y0) || (almostEquals(y, y0)) || (almostEquals(y, y1))
fun containsBoundsXY(x: Double, y: Double): Boolean = containsX(x) && containsY(y)
val angle: Angle get() = Angle.between(a, b)
val length: Double get() = Point.distance(a, b)
val lengthSquared: Double get() = Point.distanceSquared(a, b)
fun getLineIntersectionPoint(line: Line): Point? =
getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1)
fun getIntersectionPoint(line: Line): Point? = getSegmentIntersectionPoint(line)
fun getSegmentIntersectionPoint(line: Line): Point? {
val out = getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1)
if (out != null && this.containsBoundsXY(out.x, out.y) && line.containsBoundsXY(out.x, out.y)) return out
return null
}
fun intersectsLine(line: Line): Boolean = getLineIntersectionPoint(line) != null
fun intersects(line: Line): Boolean = intersectsSegment(line)
fun intersectsSegment(line: Line): Boolean = getSegmentIntersectionPoint(line) != null
override fun toString(): String = "Line($a, $b)"
val isNIL get() = a.x.isNaN()
fun isNaN(): Boolean = a.y.isNaN()
companion object {
val ZERO = Line(Point.ZERO, Point.ZERO)
val NaN = Line(Point.NaN, Point.NaN)
val NIL: Line get() = NaN
fun fromPointAndDirection(point: Point, direction: Point, scale: Double = 1.0): Line =
Line(point, point + direction * scale)
fun fromPointAngle(point: Point, angle: Angle, length: Double = 1.0): Line =
Line(point, Point.polar(angle, length))
fun length(Ax: Double, Ay: Double, Bx: Double, By: Double): Double = kotlin.math.hypot(Bx - Ax, By - Ay)
inline fun getIntersectXY(Ax: Double, Ay: Double, Bx: Double, By: Double, Cx: Double, Cy: Double, Dx: Double, Dy: Double): Point? {
val a1 = By - Ay
val b1 = Ax - Bx
val c1 = a1 * (Ax) + b1 * (Ay)
val a2 = Dy - Cy
val b2 = Cx - Dx
val c2 = a2 * (Cx) + b2 * (Cy)
val determinant = a1 * b2 - a2 * b1
if (determinant.isAlmostZero()) return null
val x = (b2 * c1 - b1 * c2) / determinant
val y = (a1 * c2 - a2 * c1) / determinant
//if (!x.isFinite() || !y.isFinite()) TODO()
return Point(x, y)
}
fun getIntersectXY(a: Point, b: Point, c: Point, d: Point): Point? =
getIntersectXY(a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y)
}
}

View File

@@ -0,0 +1,3 @@
package com.icegps.math.geometry
data class Line3D(val a: Vector3D, val b: Vector3D)

View File

@@ -0,0 +1,75 @@
package com.icegps.math.geometry
import com.icegps.math.*
import com.icegps.number.*
/**
* A [top], [right], [bottom], [left] pack with FixedShort (16-bit) in the range of +-3275.9 (3.3 integer digits + 1 decimal digit)
*/
data class Margin(
val top: Double,
val right: Double,
val bottom: Double,
val left: Double,
) : IsAlmostEquals<Margin> {
companion object {
val ZERO = Margin(0.0, 0.0, 0.0, 0.0)
inline operator fun invoke(margin: Number): Margin = Margin(margin.toDouble(), margin.toDouble(), margin.toDouble(), margin.toDouble())
inline operator fun invoke(vertical: Number, horizontal: Number): Margin = Margin(vertical.toDouble(), horizontal.toDouble(), vertical.toDouble(), horizontal.toDouble())
inline operator fun invoke(top: Number, right: Number, bottom: Number, left: Number): Margin = Margin(top.toDouble(), right.toDouble(), bottom.toDouble(), left.toDouble())
}
constructor(vertical: Double, horizontal: Double) : this(vertical, horizontal, vertical, horizontal)
constructor(margin: Double) : this(margin, margin, margin, margin)
operator fun plus(other: Margin): Margin = Margin(top + other.top, right + other.right, bottom + other.bottom, left + other.left)
operator fun minus(other: Margin): Margin = Margin(top - other.top, right - other.right, bottom - other.bottom, left - other.left)
val isNotZero: Boolean get() = top != 0.0 || left != 0.0 || right != 0.0 || bottom != 0.0
override fun isAlmostEquals(other: Margin, epsilon: Double): Boolean =
this.left.isAlmostEquals(other.left, epsilon) &&
this.right.isAlmostEquals(other.right, epsilon) &&
this.top.isAlmostEquals(other.top, epsilon) &&
this.bottom.isAlmostEquals(other.bottom, epsilon)
fun isAlmostZero(epsilon: Double = 0.000001): Boolean = isAlmostEquals(ZERO, epsilon)
val leftPlusRight: Double get() = left + right
val topPlusBottom: Double get() = top + bottom
val horizontal: Double get() = (left + right) / 2
val vertical: Double get() = (top + bottom) / 2
override fun toString(): String = "Margin(top=${top.niceStr}, right=${right.niceStr}, bottom=${bottom.niceStr}, left=${left.niceStr})"
}
/**
* A [top], [right], [bottom], [left] pack with Int)
*/
data class MarginInt(
val top: Int,
val right: Int,
val bottom: Int,
val left: Int,
) {
constructor(top: Short, right: Short, bottom: Short, left: Short) : this(top.toInt(), right.toInt(), bottom.toInt(), left.toInt())
constructor(vertical: Int, horizontal: Int) : this(vertical, horizontal, vertical, horizontal)
constructor(margin: Int) : this(margin, margin, margin, margin)
operator fun plus(other: MarginInt): MarginInt = MarginInt(top + other.top, right + other.right, bottom + other.bottom, left + other.left)
operator fun minus(other: MarginInt): MarginInt = MarginInt(top - other.top, right - other.right, bottom - other.bottom, left - other.left)
val isNotZero: Boolean get() = top != 0 || left != 0 || right != 0 || bottom != 0
val leftPlusRight: Int get() = left + right
val topPlusBottom: Int get() = top + bottom
val horizontal: Int get() = (left + right) / 2
val vertical: Int get() = (top + bottom) / 2
companion object {
val ZERO = MarginInt(0, 0, 0, 0)
}
override fun toString(): String = "MarginInt(top=${top}, right=${right}, bottom=${bottom}, left=${left})"
}

View File

@@ -0,0 +1,415 @@
package com.icegps.math.geometry
import com.icegps.math.*
import com.icegps.math.interpolation.*
import com.icegps.number.*
import kotlin.math.*
//@KormaValueApi
//data class Matrix(
// val a: Float,
// val b: Float,
// val c: Float,
// val d: Float,
// val tx: Float,
// val ty: Float,
//) {
// a, b, c, d, tx and ty are BFloat21
data class Matrix(
val a: Double, val b: Double, val c: Double, val d: Double,
val tx: Double = 0.0, val ty: Double = 0.0
) : IsAlmostEquals<Matrix> {
//private val twobits: Int get() = data.twobits
//constructor() : this(1f, 0f, 0f, 1f, 0f, 0f)
constructor(a: Float, b: Float, c: Float, d: Float, tx: Float = 0f, ty: Float = 0f) :
this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble())
constructor(a: Int, b: Int, c: Int, d: Int, tx: Int = 0, ty: Int = 0) :
this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble())
operator fun times(other: Matrix): Matrix = Matrix.multiply(this, other)
operator fun times(scale: Double): Matrix = Matrix(a * scale, b * scale, c * scale, d * scale, tx * scale, ty * scale)
operator fun times(scale: Float): Matrix = times(scale.toDouble())
//val isNIL: Boolean get() = this == NIL
val isNIL: Boolean get() = this.a.isNaN()
val isNotNIL: Boolean get() = !isNIL
val isNaN: Boolean get() = isNIL
val isIdentity: Boolean get() = (a == 1.0 && b == 0.0 && c == 0.0 && d == 1.0 && tx == 0.0 && ty == 0.0)
//val isIdentity: Boolean get() = twobits == 1
val type: MatrixType get() {
val hasRotation = b != 0.0 || c != 0.0
val hasScale = a != 1.0 || d != 1.0
val hasTranslation = tx != 0.0 || ty != 0.0
return when {
hasRotation -> MatrixType.COMPLEX
hasScale && hasTranslation -> MatrixType.SCALE_TRANSLATE
hasScale -> MatrixType.SCALE
hasTranslation -> MatrixType.TRANSLATE
else -> MatrixType.IDENTITY
}
}
inline fun transform(p: Vector2F): Vector2F {
if (this.isNIL) return p
return Vector2F(
this.a * p.x + this.c * p.y + this.tx,
this.d * p.y + this.b * p.x + this.ty
)
}
inline fun transform(p: Vector2D): Vector2D {
if (this.isNIL) return p
return Vector2D(
transformX(p.x, p.y),
transformY(p.x, p.y),
)
}
@Deprecated("", ReplaceWith("transform(p).x")) fun transformX(p: Point): Double = transformX(p.x, p.y)
@Deprecated("", ReplaceWith("transform(p).y")) fun transformY(p: Point): Double = transformY(p.x, p.y)
@Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Float, y: Float): Float = transformX(x.toDouble(), y.toDouble()).toFloat()
@Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Float, y: Float): Float = transformY(x.toDouble(), y.toDouble()).toFloat()
@Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Double, y: Double): Double = this.a * x + this.c * y + this.tx
@Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Double, y: Double): Double = this.d * y + this.b * x + this.ty
@Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Int, y: Int): Double = transformX(x.toDouble(), y.toDouble())
@Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Int, y: Int): Double = transformY(x.toDouble(), y.toDouble())
fun deltaTransform(p: Vector2F): Vector2F = Vector2F((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
fun deltaTransform(p: Vector2D): Vector2D = Vector2D((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
fun rotated(angle: Angle): Matrix {
val cos = cos(angle)
val sin = sin(angle)
val a1 = this.a * cos - this.b * sin
val b = (this.a * sin + this.b * cos)
val a = a1
val c1 = this.c * cos - this.d * sin
val d = (this.c * sin + this.d * cos)
val c = c1
val tx1 = this.tx * cos - this.ty * sin
val ty = (this.tx * sin + this.ty * cos)
val tx = tx1
return Matrix(a, b, c, d, tx, ty)
}
fun skewed(skewX: Angle, skewY: Angle): Matrix {
val sinX = sin(skewX)
val cosX = cos(skewX)
val sinY = sin(skewY)
val cosY = cos(skewY)
return Matrix(
a * cosY - b * sinX,
a * sinY + b * cosX,
c * cosY - d * sinX,
c * sinY + d * cosX,
tx * cosY - ty * sinX,
tx * sinY + ty * cosX
)
}
fun scaled(scaleX: Int, scaleY: Int = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble())
fun scaled(scaleX: Float, scaleY: Float = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble())
fun scaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx * scaleX, ty * scaleY)
fun prescaled(scaleX: Int, scaleY: Int = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble())
fun prescaled(scaleX: Float, scaleY: Float = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble())
fun prescaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx, ty)
fun translated(delta: Point): Matrix = Matrix(a, b, c, d, tx + delta.x, ty + delta.y)
fun translated(x: Int, y: Int): Matrix = translated(Point(x, y))
fun translated(x: Float, y: Float): Matrix = translated(Point(x, y))
fun translated(x: Double, y: Double): Matrix = translated(Point(x, y))
fun pretranslated(delta: Point): Matrix = Matrix(a, b, c, d, tx + (a * delta.x + c * delta.y), ty + (b * delta.x + d * delta.y))
fun pretranslated(deltaX: Int, deltaY: Int): Matrix = pretranslated(Point(deltaX, deltaY))
fun pretranslated(deltaX: Float, deltaY: Float): Matrix = pretranslated(Point(deltaX, deltaY))
fun pretranslated(deltaX: Double, deltaY: Double): Matrix = pretranslated(Point(deltaX, deltaY))
fun prerotated(angle: Angle): Matrix = rotating(angle) * this
fun preskewed(skewX: Angle, skewY: Angle): Matrix = skewing(skewX, skewY) * this
fun premultiplied(m: Matrix): Matrix = m * this
fun multiplied(m: Matrix): Matrix = this * m
/** Transform point without translation */
fun deltaTransformPoint(p: Point): Point = Point((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
@Deprecated("", ReplaceWith("this")) fun clone(): Matrix = this
fun inverted(): Matrix {
if (this.isNIL) return Matrix.IDENTITY
val m = this
val norm = m.a * m.d - m.b * m.c
return when (norm) {
0.0 -> Matrix(0.0, 0.0, 0.0, 0.0, -m.tx, -m.ty)
else -> {
val inorm = 1.0 / norm
val d = m.a * inorm
val a = m.d * inorm
val b = m.b * -inorm
val c = m.c * -inorm
Matrix(a, b, c, d, -a * m.tx - c * m.ty, -b * m.tx - d * m.ty)
}
}
}
fun toTransform(): MatrixTransform = decompose()
fun decompose(): MatrixTransform = MatrixTransform.fromMatrix(this)
fun toArray(value: DoubleArray, offset: Int = 0) {
value[offset + 0] = a
value[offset + 1] = b
value[offset + 2] = c
value[offset + 3] = d
value[offset + 4] = tx
value[offset + 5] = ty
}
fun toArray(value: FloatArray, offset: Int = 0) {
value[offset + 0] = a.toFloat()
value[offset + 1] = b.toFloat()
value[offset + 2] = c.toFloat()
value[offset + 3] = d.toFloat()
value[offset + 4] = tx.toFloat()
value[offset + 5] = ty.toFloat()
}
override fun toString(): String = "Matrix(${a.niceStr}, ${b.niceStr}, ${c.niceStr}, ${d.niceStr}, ${tx.niceStr}, ${ty.niceStr})"
override fun isAlmostEquals(other: Matrix, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon)
fun isAlmostIdentity(epsilon: Double = 0.00001): Boolean = isAlmostEquals(this, IDENTITY, epsilon)
// @TODO: Is this order correct?
fun preconcated(other: Matrix): Matrix = this * other
companion object {
val IDENTITY = Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
val NIL = Matrix(Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN)
val NaN = NIL
//@Deprecated("", ReplaceWith("com.icegps.math.geometry.Matrix.IDENTITY", "com.icegps.math.geometry.Matrix"))
operator fun invoke(): Matrix = IDENTITY
fun isAlmostEquals(a: Matrix, b: Matrix, epsilon: Double = 0.00001): Boolean =
a.tx.isAlmostEquals(b.tx, epsilon)
&& a.ty.isAlmostEquals(b.ty, epsilon)
&& a.a.isAlmostEquals(b.a, epsilon)
&& a.b.isAlmostEquals(b.b, epsilon)
&& a.c.isAlmostEquals(b.c, epsilon)
&& a.d.isAlmostEquals(b.d, epsilon)
fun multiply(l: Matrix, r: Matrix): Matrix {
if (l.isNIL) return r
if (r.isNIL) return l
return Matrix(
l.a * r.a + l.b * r.c,
l.a * r.b + l.b * r.d,
l.c * r.a + l.d * r.c,
l.c * r.b + l.d * r.d,
l.tx * r.a + l.ty * r.c + r.tx,
l.tx * r.b + l.ty * r.d + r.ty
)
}
fun translating(delta: Point): Matrix = Matrix.IDENTITY.copy(tx = delta.x, ty = delta.y)
fun rotating(angle: Angle): Matrix = Matrix.IDENTITY.rotated(angle)
fun skewing(skewX: Angle, skewY: Angle): Matrix = Matrix.IDENTITY.skewed(skewX, skewY)
fun fromArray(value: FloatArray, offset: Int = 0): Matrix = Matrix(
value[offset + 0], value[offset + 1], value[offset + 2],
value[offset + 3], value[offset + 4], value[offset + 5]
)
fun fromArray(value: DoubleArray, offset: Int = 0): Matrix = Matrix(
value[offset + 0], value[offset + 1], value[offset + 2],
value[offset + 3], value[offset + 4], value[offset + 5]
)
fun fromTransform(
transform: MatrixTransform,
pivotX: Double = 0.0,
pivotY: Double = 0.0,
): Matrix = fromTransform(
transform.x,
transform.y,
transform.rotation,
transform.scaleX,
transform.scaleY,
transform.skewX,
transform.skewY,
pivotX,
pivotY,
)
fun fromTransform(
x: Double,
y: Double,
rotation: Angle = Angle.ZERO,
scaleX: Double = 1.0,
scaleY: Double = 1.0,
skewX: Angle = Angle.ZERO,
skewY: Angle = Angle.ZERO,
pivotX: Double = 0.0,
pivotY: Double = 0.0,
): Matrix {
// +0.0 drops the negative -0.0
val a = cos(rotation + skewY) * scaleX + 0f
val b = sin(rotation + skewY) * scaleX + 0f
val c = -sin(rotation - skewX) * scaleY + 0f
val d = cos(rotation - skewX) * scaleY + 0f
val tx: Double
val ty: Double
if (pivotX == 0.0 && pivotY == 0.0) {
tx = x
ty = y
} else {
tx = x - ((pivotX * a) + (pivotY * c))
ty = y - ((pivotX * b) + (pivotY * d))
}
return Matrix(a, b, c, d, tx, ty)
}
fun transform(a: Float, b: Float, c: Float, d: Float, tx: Float, ty: Float, p: Point): Point = Point(
a * p.x + c * p.y + tx,
d * p.y + b * p.x + ty
)
fun interpolated(l: Matrix, r: Matrix, ratio: Ratio): Matrix = Matrix(
ratio.interpolate(l.a, r.a),
ratio.interpolate(l.b, r.b),
ratio.interpolate(l.c, r.c),
ratio.interpolate(l.d, r.d),
ratio.interpolate(l.tx, r.tx),
ratio.interpolate(l.ty, r.ty),
)
}
}
//@KormaValueApi
data class MatrixTransform(
val x: Double = 0.0, val y: Double = 0.0,
val scaleX: Double = 1.0, val scaleY: Double = 1.0,
val skewX: Angle = Angle.ZERO, val skewY: Angle = Angle.ZERO,
val rotation: Angle = Angle.ZERO
) : IsAlmostEquals<MatrixTransform> {
override fun toString(): String = "MatrixTransform(x=${x.niceStr}, y=${y.niceStr}, scaleX=${scaleX}, scaleY=${scaleY}, skewX=${skewX}, skewY=${skewY}, rotation=${rotation})"
constructor() : this(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO)
constructor(
x: Float, y: Float,
scaleX: Float, scaleY: Float,
skewX: Angle, skewY: Angle,
rotation: Angle
) : this(x.toDouble(), y.toDouble(), scaleX.toDouble(), scaleY.toDouble(), skewX, skewY, rotation)
companion object {
val IDENTITY = MatrixTransform(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO)
fun fromMatrix(matrix: Matrix, pivotX: Double = 0.0, pivotY: Double = 0.0): MatrixTransform {
val a = matrix.a
val b = matrix.b
val c = matrix.c
val d = matrix.d
val skewX = -atan2(-c, d)
val skewY = atan2(b, a)
val delta = abs(skewX + skewY)
val trotation: Angle
val tskewX: Angle
val tskewY: Angle
val tx: Double
val ty: Double
if (delta < 0.001f || abs((PI * 2) - delta) < 0.001f) {
trotation = skewY.radians
tskewX = 0.0.radians
tskewY = 0.0.radians
} else {
trotation = 0.radians
tskewX = skewX.radians
tskewY = skewY.radians
}
val tscaleX = hypot(a, b)
val tscaleY = hypot(c, d)
if (pivotX == 0.0 && pivotY == 0.0) {
tx = matrix.tx
ty = matrix.ty
} else {
tx = matrix.tx + ((pivotX * a) + (pivotY * c));
ty = matrix.ty + ((pivotX * b) + (pivotY * d));
}
return MatrixTransform(tx, ty, tscaleX, tscaleY, tskewX, tskewY, trotation)
}
fun interpolated(l: MatrixTransform, r: MatrixTransform, ratio: Ratio): MatrixTransform = MatrixTransform(
ratio.toRatio().interpolate(l.x, r.x),
ratio.toRatio().interpolate(l.y, r.y),
ratio.toRatio().interpolate(l.scaleX, r.scaleX),
ratio.toRatio().interpolate(l.scaleY, r.scaleY),
ratio.toRatio().interpolateAngleDenormalized(l.skewX, r.skewX),
ratio.toRatio().interpolateAngleDenormalized(l.skewY, r.skewY),
ratio.toRatio().interpolateAngleDenormalized(l.rotation, r.rotation),
)
fun isAlmostEquals(a: MatrixTransform, b: MatrixTransform, epsilon: Double = 0.000001): Boolean =
a.x.isAlmostEquals(b.x, epsilon)
&& a.y.isAlmostEquals(b.y, epsilon)
&& a.scaleX.isAlmostEquals(b.scaleX, epsilon)
&& a.scaleY.isAlmostEquals(b.scaleY, epsilon)
&& a.skewX.isAlmostEquals(b.skewX, epsilon)
&& a.skewY.isAlmostEquals(b.skewY, epsilon)
&& a.rotation.isAlmostEquals(b.rotation, epsilon)
}
override fun isAlmostEquals(other: MatrixTransform, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon)
val scaleAvg: Double get() = (scaleX + scaleY) * 0.5
fun toMatrix(pivotX: Double = 0.0, pivotY: Double = 0.0): Matrix = Matrix.fromTransform(this, pivotX, pivotY)
operator fun plus(that: MatrixTransform): MatrixTransform = MatrixTransform(
x + that.x, y + that.y,
scaleX * that.scaleX, scaleY * that.scaleY,
skewX + that.skewX, skewY + that.skewY,
rotation + that.rotation,
)
operator fun minus(that: MatrixTransform): MatrixTransform = MatrixTransform(
x - that.x, y - that.y,
scaleX / that.scaleX, scaleY / that.scaleY,
skewX - that.skewX, skewY - that.skewY,
rotation - that.rotation,
)
}
class MatrixComputed(val matrix: Matrix, val transform: MatrixTransform) {
companion object;
constructor(matrix: Matrix) : this(matrix, MatrixTransform.fromMatrix(matrix))
constructor(transform: MatrixTransform) : this(transform.toMatrix(), transform)
}
enum class MatrixType(val id: Int, val hasRotation: Boolean, val hasScale: Boolean, val hasTranslation: Boolean) {
IDENTITY(1, hasRotation = false, hasScale = false, hasTranslation = false),
TRANSLATE(2, hasRotation = false, hasScale = false, hasTranslation = true),
SCALE(3, hasRotation = false, hasScale = true, hasTranslation = false),
SCALE_TRANSLATE(4, hasRotation = false, hasScale = true, hasTranslation = true),
COMPLEX(5, hasRotation = true, hasScale = true, hasTranslation = true);
}

View File

@@ -0,0 +1,237 @@
@file:Suppress("NOTHING_TO_INLINE")
package com.icegps.math.geometry
import com.icegps.math.*
import kotlin.math.*
/**
* Useful for representing rotations and scales.
*/
data class Matrix3 private constructor(
internal val data: FloatArray,
) : IsAlmostEqualsF<Matrix3> {
override fun equals(other: Any?): Boolean = other is Matrix3 && this.data.contentEquals(other.data)
override fun hashCode(): Int = data.contentHashCode()
private constructor(
v00: Float, v10: Float, v20: Float,
v01: Float, v11: Float, v21: Float,
v02: Float, v12: Float, v22: Float,
) : this(
floatArrayOf(
v00, v10, v20,
v01, v11, v21,
v02, v12, v22,
)
)
init {
check(data.size == 9)
}
val v00: Float get() = data[0]
val v10: Float get() = data[1]
val v20: Float get() = data[2]
val v01: Float get() = data[3]
val v11: Float get() = data[4]
val v21: Float get() = data[5]
val v02: Float get() = data[6]
val v12: Float get() = data[7]
val v22: Float get() = data[8]
val c0: Vector3F get() = Vector3F.fromArray(data, 0)
val c1: Vector3F get() = Vector3F.fromArray(data, 3)
val c2: Vector3F get() = Vector3F.fromArray(data, 6)
fun c(column: Int): Vector3F {
if (column < 0 || column >= 3) error("Invalid column $column")
return Vector3F.fromArray(data, column * 3)
}
val r0: Vector3F get() = Vector3F(v00, v01, v02)
val r1: Vector3F get() = Vector3F(v10, v11, v12)
val r2: Vector3F get() = Vector3F(v20, v21, v22)
fun v(index: Int): Float = data[index]
fun r(row: Int): Vector3F = when (row) {
0 -> r0
1 -> r1
2 -> r2
else -> error("Invalid row $row")
}
operator fun get(row: Int, column: Int): Float {
if (column !in 0..2 || row !in 0..2) error("Invalid index $row,$column")
return data[row * 3 + column]
}
fun transform(v: Vector3F): Vector3F = Vector3F(r0.dot(v), r1.dot(v), r2.dot(v))
operator fun unaryMinus(): Matrix3 = Matrix3(
-v00, -v10, -v20,
-v01, -v11, -v21,
-v02, -v12, -v22,
)
operator fun unaryPlus(): Matrix3 = this
operator fun minus(other: Matrix3): Matrix3 = Matrix3(
v00 - other.v00, v10 - other.v10, v20 - other.v20,
v01 - other.v01, v11 - other.v11, v21 - other.v21,
v02 - other.v02, v12 - other.v12, v22 - other.v22,
)
operator fun plus(other: Matrix3): Matrix3 = Matrix3(
v00 + other.v00, v10 + other.v10, v20 + other.v20,
v01 + other.v01, v11 + other.v11, v21 + other.v21,
v02 + other.v02, v12 + other.v12, v22 + other.v22,
)
operator fun times(other: Matrix3): Matrix3 = Matrix3.multiply(this, other)
operator fun times(scale: Float): Matrix3 = Matrix3(
v00 * scale, v10 * scale, v20 * scale,
v01 * scale, v11 * scale, v21 * scale,
v02 * scale, v12 * scale, v22 * scale,
)
operator fun div(scale: Float): Matrix3 = this * (1f / scale)
fun inv(): Matrix3 = inverted()
val determinant: Float get() = v00 * (v11 * v22 - v21 * v12) -
v01 * (v10 * v22 - v12 * v20) +
v02 * (v10 * v21 - v11 * v20)
fun inverted(): Matrix3 {
val determinant = this.determinant
if (determinant == 0.0f) throw ArithmeticException("Matrix is not invertible")
val invDet = 1.0f / determinant
return fromRows(
(v11 * v22 - v21 * v12) * invDet,
(v02 * v21 - v01 * v22) * invDet,
(v01 * v12 - v02 * v11) * invDet,
(v12 * v20 - v10 * v22) * invDet,
(v00 * v22 - v02 * v20) * invDet,
(v10 * v02 - v00 * v12) * invDet,
(v10 * v21 - v20 * v11) * invDet,
(v20 * v01 - v00 * v21) * invDet,
(v00 * v11 - v10 * v01) * invDet,
)
}
override fun toString(): String = buildString {
append("Matrix3(\n")
for (row in 0 until 3) {
append(" [ ")
for (col in 0 until 3) {
if (col != 0) append(", ")
val v = get(row, col)
if (floor(v) == v) append(v.toInt()) else append(v)
}
append(" ],\n")
}
append(")")
}
fun transposed(): Matrix3 = Matrix3.fromColumns(r0, r1, r2)
override fun isAlmostEquals(other: Matrix3, epsilon: Float): Boolean = c0.isAlmostEquals(other.c0, epsilon)
&& c1.isAlmostEquals(other.c1, epsilon)
&& c2.isAlmostEquals(other.c2, epsilon)
companion object {
const val M00 = 0
const val M10 = 1
const val M20 = 2
const val M01 = 3
const val M11 = 4
const val M21 = 5
const val M02 = 6
const val M12 = 7
const val M22 = 8
const val M03 = 9
const val M13 = 10
const val M23 = 11
val INDICES_BY_COLUMNS = intArrayOf(
M00, M10, M20,
M01, M11, M21,
M02, M12, M22,
)
val INDICES_BY_ROWS = intArrayOf(
M00, M01, M02,
M10, M11, M12,
M20, M21, M22,
)
val IDENTITY = Matrix3(
1f, 0f, 0f,
0f, 1f, 0f,
0f, 0f, 1f,
)
fun fromRows(
r0: Vector3F, r1: Vector3F, r2: Vector3F
): Matrix3 = Matrix3(
r0.x, r1.x, r2.x,
r0.y, r1.y, r2.y,
r0.z, r1.z, r2.z,
)
fun fromColumns(
c0: Vector3F, c1: Vector3F, c2: Vector3F
): Matrix3 = Matrix3(
c0.x, c0.y, c0.z,
c1.x, c1.y, c1.z,
c2.x, c2.y, c2.z,
)
fun fromColumns(
v00: Float, v10: Float, v20: Float,
v01: Float, v11: Float, v21: Float,
v02: Float, v12: Float, v22: Float,
): Matrix3 = Matrix3(
v00, v10, v20,
v01, v11, v21,
v02, v12, v22,
)
fun fromRows(
v00: Float, v01: Float, v02: Float,
v10: Float, v11: Float, v12: Float,
v20: Float, v21: Float, v22: Float,
): Matrix3 = Matrix3(
v00, v10, v20,
v01, v11, v21,
v02, v12, v22,
)
fun multiply(l: Matrix3, r: Matrix3): Matrix3 = Matrix3.fromRows(
(l.v00 * r.v00) + (l.v01 * r.v10) + (l.v02 * r.v20),
(l.v00 * r.v01) + (l.v01 * r.v11) + (l.v02 * r.v21),
(l.v00 * r.v02) + (l.v01 * r.v12) + (l.v02 * r.v22),
(l.v10 * r.v00) + (l.v11 * r.v10) + (l.v12 * r.v20),
(l.v10 * r.v01) + (l.v11 * r.v11) + (l.v12 * r.v21),
(l.v10 * r.v02) + (l.v11 * r.v12) + (l.v12 * r.v22),
(l.v20 * r.v00) + (l.v21 * r.v10) + (l.v22 * r.v20),
(l.v20 * r.v01) + (l.v21 * r.v11) + (l.v22 * r.v21),
(l.v20 * r.v02) + (l.v21 * r.v12) + (l.v22 * r.v22),
)
}
}
fun Matrix3.toMatrix4(): Matrix4 = Matrix4.fromRows(
v00, v01, v02, 0f,
v10, v11, v12, 0f,
v20, v21, v22, 0f,
0f, 0f, 0f, 1f,
)
fun Matrix3.toQuaternion(): Quaternion = Quaternion.fromRotationMatrix(this)

Some files were not shown because too many files have changed in this diff Show More