initial commit
11
.gitignore
vendored
Normal 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
@@ -0,0 +1 @@
|
||||
/build
|
||||
49
app/build.gradle
Normal 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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
30
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
274
app/src/main/java/com/icegps/common/helper/BlhToEnu.kt
Normal 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] }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
419
app/src/main/java/com/icegps/common/helper/GeoHelper.kt
Normal 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)"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
83
app/src/main/java/com/icegps/geotools/GeoJsonUtils.kt
Normal 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 {
|
||||
// 生成三角形(Polygon)FeatureCollection
|
||||
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)
|
||||
}
|
||||
|
||||
// 生成边(LineString)FeatureCollection(每条边只输出一次)
|
||||
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,q)和(q,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)
|
||||
}
|
||||
}
|
||||
529
app/src/main/java/com/icegps/geotools/GridCell.kt
Normal 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 targetH(filter=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 传给 mapbox(imageSource 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)
|
||||
}
|
||||
}
|
||||
594
app/src/main/java/com/icegps/geotools/MainActivity.kt
Normal 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) 添加 GeoJsonSource(id = "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,类型为 raster,sourceId 必须和上面一致
|
||||
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)
|
||||
}
|
||||
|
||||
165
app/src/main/java/com/icegps/geotools/RasterUtils.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
7
app/src/main/java/com/icegps/geotools/ktx/Any.kt
Normal 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
|
||||
17
app/src/main/java/com/icegps/geotools/ktx/Vector3D.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/com/icegps/geotools/model/DPoint.kt
Normal 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
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
29
app/src/main/res/drawable/ic_pile_marker.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/test_radar.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/main/res/drawable/voroni4.jpg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
10
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal 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>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
16
app/src/main/res/values-night/themes.xml
Normal 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>
|
||||
10
app/src/main/res/values/colors.xml
Normal 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>
|
||||
4
app/src/main/res/values/mapbox_access_token.xml
Normal 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>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">geotools</string>
|
||||
</resources>
|
||||
16
app/src/main/res/values/themes.xml
Normal 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>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||
17
app/src/test/java/com/icegps/geotools/ExampleUnitTest.kt
Normal 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
@@ -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
@@ -0,0 +1 @@
|
||||
/build
|
||||
40
delaunator/build.gradle
Normal 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
|
||||
}
|
||||
0
delaunator/consumer-rules.pro
Normal file
21
delaunator/proguard-rules.pro
vendored
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
4
delaunator/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
625
delaunator/src/main/java/com/icegps/geotools/Delaunator.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
class Edge(
|
||||
override val index: Int,
|
||||
override val p: IPoint,
|
||||
override val q: IPoint
|
||||
) : IEdge
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
interface IEdge {
|
||||
val p: IPoint
|
||||
val q: IPoint
|
||||
val index: Int
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
interface IPoint {
|
||||
var x: Double
|
||||
var y: Double
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
interface ITriangle {
|
||||
val points: List<IPoint>
|
||||
val Index: Int
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
interface IVoronoiCell {
|
||||
val points: List<IPoint>
|
||||
val index: Int
|
||||
}
|
||||
19
delaunator/src/main/java/com/icegps/geotools/model/Point.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
class Triangle(
|
||||
override val points: List<IPoint>,
|
||||
override val Index: Int
|
||||
) : ITriangle
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.icegps.geotools.model
|
||||
|
||||
class VoronoiCell(
|
||||
override val index: Int,
|
||||
override val points: List<IPoint>
|
||||
) : IVoronoiCell
|
||||
@@ -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
@@ -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
@@ -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
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
/build
|
||||
17
math/build.gradle
Normal 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
|
||||
}
|
||||
75
math/src/main/java/com/icegps/io/util/NumberExt.kt
Normal 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()
|
||||
78
math/src/main/java/com/icegps/io/util/NumberParser.kt
Normal 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
|
||||
}
|
||||
47
math/src/main/java/com/icegps/math/Alignment.kt
Normal 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
|
||||
}
|
||||
|
||||
9
math/src/main/java/com/icegps/math/BooleanConversion.kt
Normal 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
|
||||
38
math/src/main/java/com/icegps/math/Clamp.kt
Normal 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()
|
||||
24
math/src/main/java/com/icegps/math/ConvertRange.kt
Normal 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)
|
||||
|
||||
19
math/src/main/java/com/icegps/math/Division.kt
Normal 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
|
||||
}
|
||||
4
math/src/main/java/com/icegps/math/Fract.kt
Normal 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()
|
||||
9
math/src/main/java/com/icegps/math/ILog.kt
Normal 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()
|
||||
14
math/src/main/java/com/icegps/math/IsAlmostEquals.kt
Normal 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
|
||||
4
math/src/main/java/com/icegps/math/IsAlmostZero.kt
Normal 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
|
||||
9
math/src/main/java/com/icegps/math/IsEven.kt
Normal 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
|
||||
11
math/src/main/java/com/icegps/math/IsNanOrInfinite.kt
Normal 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()
|
||||
133
math/src/main/java/com/icegps/math/Math.kt
Normal 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)
|
||||
7
math/src/main/java/com/icegps/math/NormalizeZero.kt
Normal 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
|
||||
22
math/src/main/java/com/icegps/math/PowerOfTwo.kt
Normal 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)
|
||||
|
||||
|
||||
16
math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt
Normal 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
|
||||
}
|
||||
|
||||
29
math/src/main/java/com/icegps/math/ToIntegerConverters.kt
Normal 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()
|
||||
35
math/src/main/java/com/icegps/math/Umod.kt
Normal 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
|
||||
}
|
||||
}
|
||||
13
math/src/main/java/com/icegps/math/Unsigned.kt
Normal 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
|
||||
@@ -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
|
||||
55
math/src/main/java/com/icegps/math/geometry/AABB3D.kt
Normal 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
|
||||
}
|
||||
}
|
||||
103
math/src/main/java/com/icegps/math/geometry/Anchor.kt
Normal 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),
|
||||
)
|
||||
}
|
||||
250
math/src/main/java/com/icegps/math/geometry/Angle.kt
Normal 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
|
||||
}
|
||||
}
|
||||
60
math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt
Normal 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
|
||||
}
|
||||
29
math/src/main/java/com/icegps/math/geometry/Circle.kt
Normal 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,)
|
||||
}
|
||||
83
math/src/main/java/com/icegps/math/geometry/Ellipse.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
330
math/src/main/java/com/icegps/math/geometry/EulerRotation.kt
Normal 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
|
||||
},
|
||||
)
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
124
math/src/main/java/com/icegps/math/geometry/IPointList.kt
Normal 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) }
|
||||
175
math/src/main/java/com/icegps/math/geometry/Line.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
3
math/src/main/java/com/icegps/math/geometry/Line3D.kt
Normal file
@@ -0,0 +1,3 @@
|
||||
package com.icegps.math.geometry
|
||||
|
||||
data class Line3D(val a: Vector3D, val b: Vector3D)
|
||||
75
math/src/main/java/com/icegps/math/geometry/Margin.kt
Normal 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})"
|
||||
}
|
||||
415
math/src/main/java/com/icegps/math/geometry/Matrix.kt
Normal 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);
|
||||
}
|
||||
237
math/src/main/java/com/icegps/math/geometry/Matrix3.kt
Normal 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)
|
||||
|
||||