Commit 745a80a3 authored by p x's avatar p x
Browse files

first

parent a8643e25
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://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.
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "commons.util"
compileSdk = 35
defaultConfig {
minSdk = 29
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
// packaging {
// jniLibs {
// excludes.add("META-INF/*******")
// }
// resources {
// excludes.addAll(
// listOf(
// "META-INF/*******",
// "META-INF/INDEX.LIST",
// "META-INF/io.netty.versions.properties"
// )
// )
// }
// }
}
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)
// GeoTools
api("org.geotools:gt-main:28.2"){
exclude("org.eclipse.emf", "*")
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package commons.gis
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
/**基础工具**/
object BasicTools {
/**
* 计算2点距离
* @return 返回米
* **/
fun calculateHaversineDistance(lng1: Double, lat1: Double, lng2: Double,lat2: Double): Double {
val earthRadius = 6371000.0 // 地球半径,单位:米
val lat1 = Math.toRadians(lat1)
val lon1 = Math.toRadians(lng1)
val lat2 = Math.toRadians(lat2)
val lon2 = Math.toRadians(lng2)
val dlat = lat2 - lat1
val dlon = lon2 - lon1
val a = sin(dlat / 2).pow(2) +
cos(lat1) * cos(lat2) * sin(dlon / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}
/**
* 根据两个坐标点计算航向角
* @param fromLon 起始点经度
* @param fromLat 起始点纬度
* @param toLon 终点经度
* @param toLat 终点纬度
* @return 航向角(度),范围 0-360
*/
fun calculateBearing(fromLon: Double, fromLat: Double, toLon: Double, toLat: Double): Double {
// 将度转换为弧度
val lat1 = Math.toRadians(fromLat)
val lat2 = Math.toRadians(toLat)
val deltaLon = Math.toRadians(toLon - fromLon)
// 计算航向角
val y = sin(deltaLon) * cos(lat2)
val x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon)
val bearing = Math.toDegrees(atan2(y, x))
// 确保航向角在 0-360 度范围内
return (bearing + 360) % 360
}
}
\ No newline at end of file
package commons.gis
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.PI
object GeoRectangleUtils {
/**
* 根据中心点、宽度和高度生成矩形(米为单位)不带方向
*/
fun createRectangleFromCenter(
centerLng: Double,
centerLat: Double,
widthMeters: Double = 6.0,
heightMeters: Double = 8.0
): List<List<Double>> {
val halfWidth = widthMeters / 2
val halfHeight = heightMeters / 2
// 计算四个角点
val topLeft = calculateDestinationPoint(
centerLng,
centerLat,
315.0,
sqrt(halfWidth * halfWidth + halfHeight * halfHeight)
)
val topRight = calculateDestinationPoint(
centerLng,
centerLat,
45.0,
sqrt(halfWidth * halfWidth + halfHeight * halfHeight)
)
val bottomRight = calculateDestinationPoint(
centerLng,
centerLat,
135.0,
sqrt(halfWidth * halfWidth + halfHeight * halfHeight)
)
val bottomLeft = calculateDestinationPoint(
centerLng,
centerLat,
225.0,
sqrt(halfWidth * halfWidth + halfHeight * halfHeight)
)
return listOf(topLeft, topRight, bottomRight, bottomLeft, topLeft) // 闭合
}
// 矩形尺寸:半宽1000米,半高500米
/**根据中心的生产矩形,带方向***/
fun generateCenterRect(
centerLng: Double,
centerLat: Double,
direction: Double,
halfWidth: Double = 5.0,
halfHeight: Double = 5.0
): List<List<Double>> {
// 将方向角转换为数学角度(逆时针从x轴正方向)
val angleRad = Math.toRadians(90 - direction)
// 计算四个顶点的偏移量(米)
val offsets = listOf(
// 右上顶点
Pair(
halfWidth * cos(angleRad) - halfHeight * sin(angleRad),
halfWidth * sin(angleRad) + halfHeight * cos(angleRad)
),
// 右下顶点
Pair(
halfWidth * cos(angleRad) + halfHeight * sin(angleRad),
halfWidth * sin(angleRad) - halfHeight * cos(angleRad)
),
// 左下顶点
Pair(
-halfWidth * cos(angleRad) + halfHeight * sin(angleRad),
-halfWidth * sin(angleRad) - halfHeight * cos(angleRad)
),
// 左上顶点
Pair(
-halfWidth * cos(angleRad) - halfHeight * sin(angleRad),
-halfWidth * sin(angleRad) + halfHeight * cos(angleRad)
),
// 右上顶点
Pair(
halfWidth * cos(angleRad) - halfHeight * sin(angleRad),
halfWidth * sin(angleRad) + halfHeight * cos(angleRad)
),
)
// 将米偏移量转换为经纬度坐标
var ps = offsets.map { (dx, dy) ->
metersToGeoPoint(centerLng, centerLat, dx, dy)
}
return ps
}
/**
* 将米单位的偏移量转换为经纬度坐标
*/
private fun metersToGeoPoint(
centerLng: Double,
centerLat: Double, dx: Double, dy: Double
): List<Double> {
// 地球半径(米)
val EARTH_RADIUS = 6378137.0
val latRad = Math.toRadians(centerLat)
// 纬度每度对应的米数
val latPerMeter = 1.0 / (PI * EARTH_RADIUS / 180.0)
// 经度每度对应的米数(随纬度变化)
val lonPerMeter = 1.0 / (PI * EARTH_RADIUS * cos(latRad) / 180.0)
val newLat = centerLat + dy * latPerMeter
val newLon = centerLng + dx * lonPerMeter
return listOf(newLon, newLat)
}
/**
* 根据前方点和方向生成矩形
*/
fun createRectangleInFront(
frontPoint: List<Double>,
bearingDegrees: Double, // 方向角度(0-360,0表示北)
widthMeters: Double = 3.0,
depthMeters: Double = 8.0
): List<List<Double>> {
// 计算矩形中心点(在前方 depthMeters/2 处)
val center = calculateDestinationPoint(
frontPoint[0],
frontPoint[1],
bearingDegrees,
depthMeters / 2
)
// 垂直于方向的宽度
val perpendicularBearing1 = (bearingDegrees + 90) % 360
val perpendicularBearing2 = (bearingDegrees - 90 + 360) % 360
// 前方点
val frontCenter = calculateDestinationPoint(
center[0], center[1], bearingDegrees, depthMeters / 2
)
// 计算四个角点
val frontLeft = calculateDestinationPoint(
frontCenter[0], frontCenter[1], perpendicularBearing1, widthMeters / 2
)
val frontRight = calculateDestinationPoint(
frontCenter[0], frontCenter[1], perpendicularBearing2, widthMeters / 2
)
val backLeft = calculateDestinationPoint(
frontPoint[0], frontCenter[1], perpendicularBearing1, widthMeters / 2
)
val backRight = calculateDestinationPoint(
frontPoint[0], frontCenter[1], perpendicularBearing2, widthMeters / 2
)
return listOf(backLeft, frontLeft, frontRight, backRight, backLeft)
}
/**
* 根据中心的生产矩形,带方向
* @param rotation 航向角
*/
fun createRotatedRectangleFromCenter(
centerLon: Double, centerLat: Double,
rotation: Double = 0.0,
widthMeters: Double = 100.0,
heightMeters: Double = 80.0
): List<List<Double>> {
// 地球半径(米)
val EARTH_RADIUS = 6371000.0
// 将米转换为经纬度(近似计算)
val latDelta = (heightMeters / 2) / EARTH_RADIUS * (180 / Math.PI)
val lonDelta =
(widthMeters / 2) / (EARTH_RADIUS * cos(Math.toRadians(centerLat))) * (180 / Math.PI)
var result = mutableListOf<List<Double>>()
// 左上角
result.add(listOf(centerLon - lonDelta, centerLat + latDelta))
// 右上角
result.add(listOf(centerLon + lonDelta, centerLat + latDelta))
// 右下角
result.add(listOf(centerLon + lonDelta, centerLat - latDelta))
// 左下角
result.add(listOf(centerLon - lonDelta, centerLat - latDelta))
result.add(listOf(centerLon - lonDelta, centerLat + latDelta))
return result
}
/**
* 计算目标点(根据起点、方向和距离)
*/
private fun calculateDestinationPoint(
startLng: Double,
startLat: Double,
bearing: Double,
distanceMeters: Double
): List<Double> {
val earthRadius = 6371000.0 // 地球半径(米)
val startLatRad = Math.toRadians(startLat)
val startLngRad = Math.toRadians(startLng)
val bearingRad = Math.toRadians(bearing)
val endLatRad = asin(
sin(startLatRad) * cos(distanceMeters / earthRadius) +
cos(startLatRad) * sin(distanceMeters / earthRadius) * cos(bearingRad)
)
val endLngRad = startLngRad + atan2(
sin(bearingRad) * sin(distanceMeters / earthRadius) * cos(startLatRad),
cos(distanceMeters / earthRadius) - sin(startLatRad) * sin(endLatRad)
)
return listOf(Math.toDegrees(endLngRad), Math.toDegrees(endLatRad))
}
}
\ No newline at end of file
package commons.gis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.LineString
import org.locationtech.jts.geom.LinearRing
import org.locationtech.jts.geom.Polygon
/**线面交叉判断***/
object LineCrossPolygonChecker {
/**
* 计算、面与线相交
* @param 路径点串
* @param area 多边形的面
* @return 线与面相交的点 起点和终点
*/
suspend fun cauLineAreaCross(
routes: List<List<Double>>, area: List<List<Double>>
): List<List<Double>> {
return withContext(Dispatchers.Default) {
// 方法1:直接使用坐标数组
var lineString = MGeoTools.createLine(routes)
//生成多边形
// 创建 LinearRing
val lRing: LinearRing = MGeoTools.createLinearRing(area)
// 创建多边形(无孔洞)
val polygon = MGeoTools.createPolygonFromLinearRing(lRing)
var crossRet = lineCrossArea(polygon, lineString)
// 计算相交部分
return@withContext crossRet
}
}
/***线和面相交部分**/
fun lineCrossArea(polygon: Polygon?, lineString: LineString?): List<List<Double>> {
if (polygon == null || lineString == null) {
return emptyList()
}
// 计算相交部分
val intersection = polygon.intersection(lineString)
if (intersection.isEmpty) {
return emptyList()
}
// val coordinates = mutableListOf<Coordinate>()
var crossP = intersection.coordinates
var list = crossP.map {
listOf(it.x, it.y)
}
return list
}
}
\ No newline at end of file
package commons.gis
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.LineString
import org.locationtech.jts.geom.LinearRing
import org.locationtech.jts.geom.Point
import org.locationtech.jts.geom.Polygon
object MGeoTools {
// 创建 GeometryFactory
private var geometryFactory: GeometryFactory = GeometryFactory()
/**
* 创建点对象
* @param x 经度
*/
fun createPoint(x: Double, y: Double): Point {
val coord = Coordinate(x, y)
return geometryFactory.createPoint(coord)
}
/**
* 生成线段
* @param points 经纬度线段
*/
fun createLine(points: List<List<Double>>): LineString {
val lines = points.map {
Coordinate(it[0], it[1])
}.toTypedArray()
var lineString = geometryFactory.createLineString(lines)
return lineString
}
/**
* 创建 LinearRing
* @param points 经纬度线段
*/
fun createLinearRing(points: List<List<Double>>): LinearRing {
val lines = points.map {
Coordinate(it[0], it[1])
}.toTypedArray()
var linearRing = geometryFactory.createLinearRing(lines)
return linearRing
}
// 创建多边形(无孔洞)
fun createPolygonFromLinearRing(linearRing: LinearRing): Polygon {
return geometryFactory.createPolygon(linearRing, null)
}
/***根据坐标生成前方矩形***/
fun createRectangleFont(
lng: Double, lat: Double, heading: Double, width: Double = 3.0, length: Double = 10.0,
): Polygon {
var rectanglePoints =
GeoRectangleUtils.createRectangleInFront(listOf(lng, lat), heading, width, length)
val coordinates = rectanglePoints.map {
Coordinate(it[0], it[1])
}.toMutableList()
var polygon = geometryFactory.createPolygon(coordinates.toTypedArray())
return polygon
}
/***根据中心点生成指定方向的矩形**/
fun createRectangleCenter(
lng: Double, lat: Double, heading: Double, halfWidth: Double = 2.0,
halfHeight: Double = 5.0
): Polygon {
var rectanglePoints =
GeoRectangleUtils.generateCenterRect(lng, lat, heading, halfWidth, halfHeight)
val coordinates = rectanglePoints.map {
Coordinate(it[0], it[1])
}.toMutableList()
// var one = myRectangle.corners.first()
// coordinates.add(Coordinate(one[0], one[1]))
var polygon = geometryFactory.createPolygon(coordinates.toTypedArray())
return polygon
}
/**
* 计算线段里的点是否在面里
* @return 返回线段里点的下标
*/
fun lineContainsArea(polygon: Polygon?, lineString: LineString?): Int {
if (polygon == null || lineString == null) {
return 0
}
lineString.coordinates.forEachIndexed { index, cood ->
val sinPoint = geometryFactory.createPoint(cood)
if (polygon.contains(sinPoint)) {
return index
}
}
return 0
}
}
\ No newline at end of file
package commons.gis
import org.locationtech.jts.geom.Point
import org.locationtech.jts.geom.Polygon
/**点面关系判断*/
object PointInPolygonChecker {
//老点
// private var oldPointX = 0.0
// private var oldPointY = 0.0
private var oldPoint: Point? = null
// private var oldPointY = 0.0
//生成的矩形
// private var pRectangle: Polygon? = null
/***点是否在多边形内:*/
fun isPointInPolygon(point: Point, polygon: Polygon): Boolean {
return polygon.contains(point)
}
/***点是否在多边形内:*/
fun isPointInPolygon(lng: Double, lat: Double, polygon: Polygon): Boolean {
// 创建点
val point = MGeoTools.createPoint(lng, lat)
return polygon.contains(point)
}
/**
* 判断点是否在多边形内
* @param cLat 当前经纬度
* @param cLng
* @param pointX 待判断点经度
* @param pointY 待判断点纬度
*/
/* fun isPointInPolygonRectCenter(
cLng: Double,
cLat: Double,
pointX: Double,
pointY: Double,
head: Double = 0.0
): Boolean {
val point = MGeoTools.createPoint(cLng, cLat)
*//* if (oldPoint != null) {
// 变化的2点距离
var pdis =
BasicTools.calculateHaversineDistance(pointX, pointY, oldPoint!!.x, oldPoint!!.y)
println("-------中心点的变化距离 pdis = ${pdis}")
if (pdis < 40) {
return true
}
}
oldPoint = point*//*
//以自己为中心点生成矩形
var pRectangle = MGeoTools.createRectangleCenter(cLng, cLat, head, 50.0, 50.0)
return pRectangle.contains(point)
}*/
}
\ No newline at end of file
package commons.gis
import kotlin.math.PI
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
/**
* 带时间戳的地理坐标+航向角模型
* @property lat 纬度
* @property lng 经度
* @property heading 航向角(0-360°,正北为0°,顺时针)
* @property timestamp 时间戳(毫秒)
*/
data class TimedGeoPoint(
val lat: Double,
val lng: Double,
val heading: Float,
val timestamp: Long
)
/**
* 三维笛卡尔坐标数据类
* @param x x轴坐标
* @param y y轴坐标
* @param z z轴坐标
*/
data class CartesianPoint(val x: Double, val y: Double, val z: Double)
/**
* 基于时间的地理坐标插值工具类
*
* 核心功能:根据起始点和结束点的时间戳、经纬度、航向角,计算任意时间点的插值结果
* 适用于:轨迹回放、导航预测、运动轨迹模拟等场景
*
* 支持两种插值模式:
* - 线性插值:适用于短距离(<10km)场景,计算效率高
* - 球面插值:适用于长距离(>10km)场景,考虑地球曲率,精度更高
*/
object TimeBasedGeoInterpolator {
// 地球半径(米)
private const val EARTH_RADIUS = 6371000.0
/**
* 角度转弧度
*/
private fun Double.toRadians(): Double = this * PI / 180.0
/**
* 弧度转角度
*/
private fun Double.toDegrees(): Double = this * 180.0 / PI
/**
* 根据目标时间,插值计算对应时刻的地理坐标和航向角
*
* @param start 起始点(包含经纬度、航向角、时间戳)
* @param end 结束点(包含经纬度、航向角、时间戳)
* @param targetTime 目标时间戳(毫秒)
* @param useSpherical 是否使用球面插值(true:球面插值,false:线性插值)
* @return 目标时间对应的插值点(包含计算出的经纬度、航向角和目标时间戳)
*
* @throws IllegalArgumentException 当起止时间戳无效时(end.timestamp <= start.timestamp)
* @sample
* ```kotlin
* val startTime = System.currentTimeMillis()
* val endTime = startTime + 5000 // 5秒后
* val startPoint = TimedGeoPoint(39.908823, 116.397470, 90.0f, startTime)
* val endPoint = TimedGeoPoint(39.916404, 116.397096, 100.0f, endTime)
*
* // 计算2秒后的位置
* val interpolatedPoint = TimeBasedGeoInterpolator.interpolateAtTime(
* start = startPoint,
* end = endPoint,
* targetTime = startTime + 2000,
* useSpherical = false
* )
* ```
*/
fun interpolateAtTime(
start: TimedGeoPoint,
end: TimedGeoPoint,
targetTime: Long,
useSpherical: Boolean = false
): TimedGeoPoint {
// 边界处理:目标时间超出范围,直接返回起止点
if (targetTime <= start.timestamp) return start.copy(timestamp = targetTime)
if (targetTime >= end.timestamp) return end.copy(timestamp = targetTime)
// 1. 计算时间比例(0~1)
val totalDuration = end.timestamp - start.timestamp
val timeRatio = (targetTime - start.timestamp).toDouble() / totalDuration
// 2. 经纬度插值
val (interpolatedLat, interpolatedLng) = if (useSpherical) {
// interpolateSphericalLatLng(start, end, timeRatio)
slerp(start, end, timeRatio)
} else {
interpolateLinearLatLng(start, end, timeRatio)
}
// 3. 航向角插值(处理360°环绕)
val interpolatedHeading = interpolateHeading(start.heading, end.heading, timeRatio)
return TimedGeoPoint(
lat = interpolatedLat,
lng = interpolatedLng,
heading = interpolatedHeading,
timestamp = targetTime
)
}
/**
* 按固定时间间隔生成等分的插值点列表
*
* @param start 起始点(包含经纬度、航向角、时间戳)
* @param end 结束点(包含经纬度、航向角、时间戳)
* @param intervalMs 时间间隔(毫秒),例如:1000表示每秒生成一个点
* @param useSpherical 是否使用球面插值
* @return 按时间升序排列的插值点列表,包含起始点和结束点
*
* @throws IllegalArgumentException 当时间间隔小于等于0或起止时间无效时
* @sample
* ```kotlin
* val startTime = System.currentTimeMillis()
* val endTime = startTime + 10000 // 10秒后
* val startPoint = TimedGeoPoint(39.908823, 116.397470, 90.0f, startTime)
* val endPoint = TimedGeoPoint(39.916404, 116.397096, 100.0f, endTime)
*
* // 每秒生成一个插值点
* val points = TimeBasedGeoInterpolator.interpolateByTimeInterval(
* start = startPoint,
* end = endPoint,
* intervalMs = 1000,
* useSpherical = false
* )
* // points.size 将是 11(包含起始点和结束点)
* ```
*/
fun interpolateByTimeInterval(
start: TimedGeoPoint,
end: TimedGeoPoint,
intervalMs: Long,
useSpherical: Boolean = false
): List<TimedGeoPoint> {
if (intervalMs <= 0 || end.timestamp <= start.timestamp) return listOf(start, end)
val interpolatedPoints = mutableListOf<TimedGeoPoint>()
interpolatedPoints.add(start)
var currentTime = start.timestamp + intervalMs
while (currentTime < end.timestamp) {
val interpolatedPoint = interpolateAtTime(start, end, currentTime, useSpherical)
interpolatedPoints.add(interpolatedPoint)
currentTime += intervalMs
}
// 确保最后一个点是结束点(避免时间误差)
if (interpolatedPoints.last().timestamp != end.timestamp) {
interpolatedPoints.add(end)
}
return interpolatedPoints
}
/**
* 线性插值经纬度
*
* 适用场景:短距离(<10km),地球曲率影响可忽略
* 计算方式:直接按时间比例线性计算中间点坐标
*
* @param start 起始点
* @param end 结束点
* @param timeRatio 时间比例(0~1)
* @return 插值后的(纬度,经度)对
* @see interpolateSphericalLatLng 球面插值方法(长距离场景)
*/
private fun interpolateLinearLatLng(
start: TimedGeoPoint,
end: TimedGeoPoint,
timeRatio: Double
): Pair<Double, Double> {
val lat = start.lat + (end.lat - start.lat) * timeRatio
val lng = start.lng + (end.lng - start.lng) * timeRatio
return Pair(lat, lng)
}
/**
* 球面插值(SLERP)
* @param start 起始经纬度点
* @param end 结束经纬度点
* @param t 插值因子(0~1,0=起始点,1=结束点)
* @param radius 球半径(默认地球半径)
* @return 插值后的经纬度点
*/
fun slerp(
start: TimedGeoPoint,
end: TimedGeoPoint,
t: Double,
radius: Double = EARTH_RADIUS
): Pair<Double, Double> {
// 1. 转换为笛卡尔坐标
val startCart = sphericalToCartesian(start, radius)
val endCart = sphericalToCartesian(end, radius)
// 2. 计算向量夹角
val theta = vectorAngle(startCart, endCart)
// 处理夹角为0的情况(两点重合)
if (theta < 1e-6) {
return Pair(start.lat, start.lng)
}
// 3. SLERP核心计算
val sinTheta = sin(theta)
val sinTTheta = sin(t * theta)
val sin1TTheta = sin((1 - t) * theta)
val x = (sin1TTheta / sinTheta) * startCart.x + (sinTTheta / sinTheta) * endCart.x
val y = (sin1TTheta / sinTheta) * startCart.y + (sinTTheta / sinTheta) * endCart.y
val z = (sin1TTheta / sinTheta) * startCart.z + (sinTTheta / sinTheta) * endCart.z
// 4. 转回球面坐标
return cartesianToSpherical(CartesianPoint(x, y, z), radius)
}
/**
* 计算两个三维向量的夹角(弧度)
* @param a 向量a
* @param b 向量b
* @return 夹角(0 ~ π)
*/
private fun vectorAngle(a: CartesianPoint, b: CartesianPoint): Double {
// 点积
val dotProduct = a.x * b.x + a.y * b.y + a.z * b.z
// 向量模长
val magA = sqrt(a.x.pow(2) + a.y.pow(2) + a.z.pow(2))
val magB = sqrt(b.x.pow(2) + b.y.pow(2) + b.z.pow(2))
// 防止浮点误差导致acos参数超出[-1,1]
val cosTheta = max(min(dotProduct / (magA * magB), 1.0), -1.0)
return acos(cosTheta)
}
/**
* 球面坐标(经纬度)转笛卡尔坐标
* @param spherical 经纬度点
* @param radius 球半径(默认地球半径)
* @return 三维笛卡尔坐标
*/
fun sphericalToCartesian(
spherical: TimedGeoPoint,
radius: Double = EARTH_RADIUS
): CartesianPoint {
val lonRad = spherical.lng.toRadians()
val latRad = spherical.lat.toRadians()
val x = radius * cos(latRad) * cos(lonRad)
val y = radius * cos(latRad) * sin(lonRad)
val z = radius * sin(latRad)
return CartesianPoint(x, y, z)
}
/**
* 笛卡尔坐标转球面坐标(经纬度)
* @param cartesian 三维笛卡尔坐标
* @param radius 球半径(默认地球半径)
* @return 经纬度点
*/
fun cartesianToSpherical(
cartesian: CartesianPoint,
radius: Double = EARTH_RADIUS
): Pair<Double, Double> {
// 计算纬度(弧度):arcsin(z / 半径)
val latRad = asin(cartesian.z / radius)
// 计算经度(弧度):arctan2(y, x)
val lonRad = atan2(cartesian.y, cartesian.x)
return Pair(latRad.toDegrees(),lonRad.toDegrees())
}
/**
* 航向角时间插值
*
* 特殊处理:自动处理360°环绕问题
* 例如:350°到10°的插值会按350°→360°/0°→10°的路径计算,而非直接350°→10°
*
* @param startHeading 起始航向角(0-360°)
* @param endHeading 结束航向角(0-360°)
* @param timeRatio 时间比例(0~1)
* @return 插值后的航向角(0-360°)
*/
private fun interpolateHeading(
startHeading: Float,
endHeading: Float,
timeRatio: Double
): Float {
var diff = endHeading - startHeading
// 调整差值到[-180, 180]区间(避免350°→10°时差值为-340°)
if (diff > 180) diff -= 360
else if (diff < -180) diff += 360
// 按时间比例计算插值
var interpolated = startHeading + diff * timeRatio
// 修正到0~360°范围
interpolated %= 360
if (interpolated < 0) interpolated += 360
return interpolated.toFloat()
}
/**
* 计算两点间的平均移动速度
*
* @param start 起始点
* @param end 结束点
* @return 平均速度(米/秒)
*
* @throws ArithmeticException 当起止时间相同时(除零错误)
* @sample
* ```kotlin
* val startTime = System.currentTimeMillis()
* val endTime = startTime + 10000 // 10秒
* val startPoint = TimedGeoPoint(39.908823, 116.397470, 90.0f, startTime)
* val endPoint = TimedGeoPoint(39.916404, 116.397096, 100.0f, endTime)
*
* val speed = TimeBasedGeoInterpolator.calculateSpeed(startPoint, endPoint)
* // speed 约为 8.5 米/秒(假设两点距离约85米)
* ```
*/
fun calculateSpeed(start: TimedGeoPoint, end: TimedGeoPoint): Double {
val distance = calculateDistance(start.lat, start.lng, end.lat, end.lng)
val durationSec = (end.timestamp - start.timestamp) / 1000.0
return if (durationSec == 0.0) 0.0 else distance / durationSec
}
/**
* 使用哈辛公式(Haversine Formula)计算两点间地表距离
*
* 精度:适用于大多数应用场景,误差约为0.5%
* 适用范围:全球范围内的两点距离计算
*
* @param lat1 第一点纬度(度)
* @param lng1 第一点经度(度)
* @param lat2 第二点纬度(度)
* @param lng2 第二点经度(度)
* @return 两点间距离(米)
* @see <a href="https://en.wikipedia.org/wiki/Haversine_formula">Haversine formula on Wikipedia</a>
*/
private fun calculateDistance(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
val dLat = Math.toRadians(lat2 - lat1)
val dLng = Math.toRadians(lng2 - lng1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLng / 2) * sin(dLng / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return EARTH_RADIUS * c
}
}
// ====================== 使用示例 ======================
fun main() {
/* // 示例1:定义起止点(带时间戳)
val startTime = System.currentTimeMillis() // 起始时间
val endTime = startTime + 5000 // 结束时间(5秒后)
val startPoint = TimedGeoPoint(
lat = 39.908823, // 北京天安门
lng = 116.397470,
heading = 90.0f, // 航向角90°(正东)
timestamp = startTime
)
val endPoint = TimedGeoPoint(
lat = 39.916404, // 北京故宫
lng = 116.397096,
heading = 100.0f, // 航向角100°
timestamp = endTime
)
// 示例2:插值计算「2秒后」的坐标+航向角
val targetTime = startTime + 2000 // 目标时间:2秒后
val interpolatedAt2s = TimeBasedGeoInterpolator.interpolateAtTime(
start = startPoint,
end = endPoint,
targetTime = targetTime,
useSpherical = false // 短距离用线性插值
)
println("2秒后插值结果:")
println("纬度:${interpolatedAt2s.lat.format(6)}")
println("经度:${interpolatedAt2s.lng.format(6)}")
println("航向角:${interpolatedAt2s.heading}°")
println("时间戳:${interpolatedAt2s.timestamp}")
// 示例3:按1秒间隔生成5秒内的所有插值点
val intervalPoints = TimeBasedGeoInterpolator.interpolateByTimeInterval(
start = startPoint,
end = endPoint,
intervalMs = 1000, // 每秒一个点
useSpherical = false
)
println("\n按1秒间隔插值结果:")
intervalPoints.forEachIndexed { index, point ->
val timeElapsed = (point.timestamp - startTime) / 1000.0
println("第${timeElapsed}秒:纬度=${point.lat.format(6)}, 经度=${point.lng.format(6)}, 航向=${point.heading}°")
}
// 示例4:计算移动速度
val speed = TimeBasedGeoInterpolator.calculateSpeed(startPoint, endPoint)
println("\n移动速度:${speed.format(2)} 米/秒")*/
}
// 扩展函数:Double保留指定位数小数
fun Double.format(digits: Int): String = "%.${digits}f".format(this)
\ No newline at end of file
package commons.stand
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
/**Assets 文件夹工具类***/
object AssetsFileUtils {
/**
* 逐行读取asset 文件内容,并加入列表中
*/
suspend fun getAssetMock(context: Context, fileName: String, dst: MutableList<String>): Int {
return withContext(Dispatchers.IO) {
val assetManager = context.assets
var bf: BufferedReader? = null
try {
val inputReader = InputStreamReader(assetManager.open(fileName))
bf = BufferedReader(inputReader)
var line = ""
while (!bf.run {
line = readLine()
return@run line
}.isNullOrEmpty()) {
dst.add(line)
}
bf.close()
return@withContext 1
} catch (e: IOException) {
e.printStackTrace()
} finally {
bf?.close()
}
return@withContext 0
}
}
/**
* 一次性读取asset 文件内容
*/
suspend fun getAsset(context: Context, fileName: String): String {
return withContext(Dispatchers.IO) {
val assetManager = context.assets
var inputStream: InputStream? = null
var str = ""
try {
inputStream = assetManager.open(fileName)
val size = inputStream.available()
val bytes = ByteArray(size)
inputStream.read(bytes)
str = String(bytes)
} catch (e: IOException) {
e.printStackTrace()
} finally {
inputStream?.close()
}
return@withContext str
}
}
}
\ No newline at end of file
package commons.stand
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.provider.Settings
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View
import android.view.View.MeasureSpec
/**
*author:pc-20171125
*data:2019/11/7 16:08
*/
object DisplayUtil {
fun getDpi(): Int {
return Resources.getSystem().displayMetrics.densityDpi
}
fun px2dp(pxValue: Float): Int {
val scale = Resources.getSystem().displayMetrics.density
return (pxValue / scale + 0.5f).toInt()
}
fun dp2px(dipValue: Float): Int {
val scale = Resources.getSystem().displayMetrics.density
return (dipValue * scale + 0.5f).toInt()
}
fun px2sp(pxValue: Float): Int {
val fontScale = Resources.getSystem().displayMetrics.scaledDensity
return (pxValue / fontScale + 0.5f).toInt()
}
fun sp2px(spValue: Float): Int {
val fontScale = Resources.getSystem().displayMetrics.scaledDensity
return (spValue * fontScale + 0.5f).toInt()
}
fun getScreenWidthDp(): Int {
val displayMetrics = Resources.getSystem().displayMetrics
val widthPixels = displayMetrics.widthPixels
val density = displayMetrics.density
return Math.round(widthPixels / density)
}
fun getScreenWidthPx(): Int {
val displayMetrics = Resources.getSystem().displayMetrics
return displayMetrics.widthPixels
}
fun getScreenHeightDp(): Int {
val displayMetrics = Resources.getSystem().displayMetrics
val heightPixels = displayMetrics.heightPixels
val density = displayMetrics.density
return Math.round(heightPixels / density)
}
fun getScreenHeightPx(): Int {
val displayMetrics = Resources.getSystem().displayMetrics
return displayMetrics.heightPixels
}
fun forceMeasure(view: View) {
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(widthMeasureSpec, heightMeasureSpec)
}
private val mTmpValue = TypedValue()
fun getXmlDef(context: Context, id: Int): Int {
synchronized(mTmpValue) {
val value: TypedValue = mTmpValue
context.resources.getValue(id, value, true)
return TypedValue.complexToFloat(value.data).toInt()
}
}
fun getNavigationBarHeight(context: Context): Int {
val mInPortrait =
context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
val result = 0
if (hasNavBar(context as Activity)) {
val key: String
if (mInPortrait) {
key = "navigation_bar_height"
} else {
key = "navigation_bar_height_landscape"
}
return getInternalDimensionSize(context, key)
}
return result
}
private fun hasNavBar(activity: Activity): Boolean {
//判断小米手机是否开启了全面屏,开启了,直接返回false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Settings.Global.getInt(activity.contentResolver, "force_fsg_nav_bar", 0) != 0) {
return false
}
}
//其他手机根据屏幕真实高度与显示高度是否相同来判断
val windowManager = activity.windowManager
val d = windowManager.defaultDisplay
val realDisplayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
d.getRealMetrics(realDisplayMetrics)
}
val realHeight = realDisplayMetrics.heightPixels
val realWidth = realDisplayMetrics.widthPixels
val displayMetrics = DisplayMetrics()
d.getMetrics(displayMetrics)
val displayHeight = displayMetrics.heightPixels
val displayWidth = displayMetrics.widthPixels
return realWidth - displayWidth > 0 || realHeight - displayHeight > 0
}
private fun getInternalDimensionSize(context: Context, key: String): Int {
var result = 0
try {
val resourceId = context.resources.getIdentifier(key, "dimen", "android")
if (resourceId > 0) {
result =
Math.round(context.resources.getDimensionPixelSize(resourceId) * Resources.getSystem().displayMetrics.density / context.resources.displayMetrics.density)
}
} catch (ignored: Resources.NotFoundException) {
return 0
}
return result
}
}
\ No newline at end of file
package commons.stand
import android.os.Environment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
object FileSdCardUtils {
/**
* 写文件到Download目录,需要配置FileProvider和申请权限
* 追加模式
*/
suspend fun writeFileToDownload(message: String, fileName: String) {
withContext(Dispatchers.IO) {
try {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
// 核心:appendText 默认为追加模式,指定编码更安全
file.appendText("$message\n", Charsets.UTF_8)
} catch (e: IOException) {
e.printStackTrace()
}
}
/**
* 写文件到Download目录,需要配置FileProvider和申请权限
* 追加模式
*/
suspend fun writeFileToDownload(message: String, file: File) {
withContext(Dispatchers.IO) {
try {
// 核心:appendText 默认为追加模式,指定编码更安全
file.appendText("$message\n", Charsets.UTF_8)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
}
\ No newline at end of file
package commons.stand
object StringUtils {
//极简方案(无边界处理,适合已知合法输入)
fun simpleHexToByteArray(hexStr: String): ByteArray {
require(hexStr.length % 2 == 0) { "16进制字符串长度必须为偶数" }
return hexStr.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
//兼容所有输入格式,无崩溃风险
fun hexStringToByteArray(hexStr: String): ByteArray {
// 1. 预处理:去除前缀(0x/0X)、空格,转为大写
val cleanedHex = hexStr.trim()
.replaceFirst("^0x|^0X".toRegex(), "") // 移除前缀
.replace("\\s+".toRegex(), "") // 移除所有空格(如 "A1 B2" → "A1B2")
.uppercase()
// 2. 处理空字符串
if (cleanedHex.isEmpty()) return ByteArray(0)
// 3. 处理奇数长度:末尾补0(如 "A1B" → "A1B0")
val length = cleanedHex.length
val paddedHex = if (length % 2 != 0) cleanedHex + "0" else cleanedHex
// 4. 批量转换:每2个字符为一组,转成1个字节
return paddedHex.chunked(2) // 按2个字符分割(如 "A1B0" → ["A1", "B0"])
.map { chunk ->
// 16进制字符串转字节(radix=16),toByte() 会自动处理 0-255 范围
chunk.toInt(radix = 16).toByte()
}
.toByteArray()
}
// 性能优化(大数据量场景)
fun largeHexToByteArray(hexStr: String): ByteArray {
val cleanedHex = hexStr.trim().replaceFirst("^0x|^0X".toRegex(), "").uppercase()
if (cleanedHex.isEmpty()) return ByteArray(0)
val length = cleanedHex.length
val resultLength = (length + 1) / 2 // 向上取整(奇数长度也适配)
val byteArray = ByteArray(resultLength)
for (i in 0 until resultLength) {
val high = cleanedHex[i * 2].digitToInt(16) // 高位字符(第1个)
val low =
if (i * 2 + 1 < length) cleanedHex[i * 2 + 1].digitToInt(16) else 0 // 低位字符(第2个,无则补0)
byteArray[i] = (high * 16 + low).toByte()
}
return byteArray
}
/**
* ByteArray 转 16进制字符串
* @param withPrefix 是否带 0x 前缀(默认 false)
* @param withSpace 是否用空格分隔字节(默认 true,如 "A1 B2")
* @param uppercase 是否大写(默认 true)
*/
fun byteToHexString(
byteArray: ByteArray?,
withPrefix: Boolean = false,
withSpace: Boolean = false,
uppercase: Boolean = true
): String {
if (byteArray == null)
return ""
val hexChars = byteArray.joinToString(separator = if (withSpace) " " else "") {
val hex = "%02x".format(it) // 每个字节转 2 位 16进制(不足补 0)
if (uppercase) hex.uppercase() else hex
}
return if (withPrefix) "0x$hexChars" else hexChars
/* val sb = StringBuilder()
for (b in bytes) {
sb.append(String.format("%02x", b))
}
return sb.toString()*/
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment