initial commit
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)
|
||||
}
|
||||
}
|
||||