feat: 重构地图
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -23,6 +24,9 @@ android {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -38,7 +42,12 @@ dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(project(":maps-view"))
|
||||
|
||||
implementation(project(":math"))
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -11,6 +11,16 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Maps"
|
||||
tools:targetApi="31" />
|
||||
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>
|
||||
1
app/src/main/assets/BucketData_1749810551032.json
Normal file
1
app/src/main/assets/BucketData_1749810551032.json
Normal file
File diff suppressed because one or more lines are too long
113
app/src/main/java/com/icegps/maps/MainActivity.kt
Normal file
113
app/src/main/java/com/icegps/maps/MainActivity.kt
Normal file
@@ -0,0 +1,113 @@
|
||||
package com.icegps.maps
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.icegps.maps.databinding.ActivityMainBinding
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import com.icegps.maps.layer.BucketTips
|
||||
import com.icegps.maps.model.MapMode
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
val json = Json {
|
||||
serializersModule = SerializersModule {
|
||||
contextual(
|
||||
Vector3D::class,
|
||||
Vector3DSerializer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val mapView = binding.mapView
|
||||
val gridLayer = mapView.createGridLayer()
|
||||
mapView.addLayer(gridLayer)
|
||||
val boundaryLayer = mapView.createBoundaryLayer()
|
||||
mapView.addLayer(boundaryLayer)
|
||||
val bucketLayer = mapView.createBucketLayer()
|
||||
mapView.addLayer(bucketLayer)
|
||||
|
||||
mapView.mapMode = MapMode.NorthUp
|
||||
|
||||
lifecycleScope.launch {
|
||||
val points = json.decodeFromStream<List<Triple<Vector3D, Vector3D, Vector3D>>>(assets.open("BucketData_1749810551032.json"))
|
||||
var index = 0
|
||||
while (isActive) {
|
||||
val triple = points[index++ % points.size]
|
||||
bucketLayer.updateTipPositions(
|
||||
BucketTips(
|
||||
left = triple.first,
|
||||
center = triple.second,
|
||||
right = triple.third
|
||||
)
|
||||
)
|
||||
Log.d(TAG, "updateTipPositions: ${triple}")
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Vector3DSerializer : KSerializer<Vector3D> {
|
||||
// 描述序列化结构
|
||||
override val descriptor: SerialDescriptor =
|
||||
buildClassSerialDescriptor("Vector3D") {
|
||||
element<Double>("x")
|
||||
element<Double>("y")
|
||||
element<Double>("z")
|
||||
}
|
||||
|
||||
// 序列化(对象 → JSON)
|
||||
override fun serialize(encoder: Encoder, value: Vector3D) {
|
||||
encoder.beginStructure(descriptor).run {
|
||||
encodeDoubleElement(descriptor, 0, value.x)
|
||||
encodeDoubleElement(descriptor, 1, value.y)
|
||||
encodeDoubleElement(descriptor, 2, value.z)
|
||||
endStructure(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
// 反序列化(JSON → 对象)
|
||||
override fun deserialize(decoder: Decoder): Vector3D {
|
||||
return decoder.beginStructure(descriptor).run {
|
||||
var x = 0.0
|
||||
var y = 0.0
|
||||
var z = 0.0
|
||||
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> x = decodeDoubleElement(descriptor, 0)
|
||||
1 -> y = decodeDoubleElement(descriptor, 1)
|
||||
2 -> z = decodeDoubleElement(descriptor, 2)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
|
||||
endStructure(descriptor)
|
||||
Vector3D(x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/main/res/layout/activity_main.xml
Normal file
14
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.icegps.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Maps" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Maps" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Maps" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Maps" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
|
||||
@@ -4,4 +4,5 @@ plugins {
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
}
|
||||
@@ -8,7 +8,9 @@ espressoCore = "3.6.1"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.12.0"
|
||||
jetbrainsKotlinJvm = "2.0.21"
|
||||
|
||||
activity = "1.10.1"
|
||||
constraintlayout = "2.1.4"
|
||||
kotlin-serialization = "1.8.0"
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
@@ -16,9 +18,12 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlin-serialization" }
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
@@ -34,6 +34,8 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(project(":math"))
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
12
maps-view/src/main/java/com/icegps/maps/BaseMapView.kt
Normal file
12
maps-view/src/main/java/com/icegps/maps/BaseMapView.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.icegps.maps
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
interface BaseMapView {
|
||||
val coordinateManager: CoordinateManager
|
||||
fun getContext(): Context
|
||||
}
|
||||
160
maps-view/src/main/java/com/icegps/maps/CoordinateManager.kt
Normal file
160
maps-view/src/main/java/com/icegps/maps/CoordinateManager.kt
Normal file
@@ -0,0 +1,160 @@
|
||||
package com.icegps.maps
|
||||
|
||||
import android.util.Log
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import com.icegps.maps.model.ENUPoint
|
||||
import com.icegps.maps.model.MapMode
|
||||
import com.icegps.maps.model.ScreenPoint
|
||||
import com.icegps.maps.model.Viewport
|
||||
import com.icegps.maps.model.WorldPoint
|
||||
import com.icegps.math.geometry.Scale
|
||||
import com.icegps.math.geometry.SizeInt
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
class CoordinateManager : CoordinateTransform {
|
||||
private val globalScale: Scale = Scale(50f, 50f)
|
||||
var viewport: Viewport = Viewport.Empty
|
||||
private set
|
||||
|
||||
fun update(block: (Viewport) -> Viewport) {
|
||||
val old = viewport
|
||||
val new = block(old)
|
||||
viewport = new
|
||||
|
||||
Log.d(TAG, "old: $old\nnew: $new")
|
||||
}
|
||||
|
||||
private val listeners = mutableSetOf<Listener>()
|
||||
|
||||
interface Listener {
|
||||
fun onBucketPointChange(point: ENUPoint)
|
||||
}
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
val mapMode: MapMode
|
||||
get() = viewport.mapMode
|
||||
val scale: Scale
|
||||
get() = viewport.scale
|
||||
val mapSize: SizeInt
|
||||
get() = viewport.mapSize
|
||||
val gridSize: Float
|
||||
get() = viewport.gridSize
|
||||
val nowPoint: ScreenPoint
|
||||
get() = viewport.nowPoint
|
||||
val angle: Float
|
||||
get() = viewport.angle
|
||||
val degrees: Float
|
||||
get() = viewport.degrees
|
||||
val centerX: Float
|
||||
get() = viewport.centerX
|
||||
val centerY: Float
|
||||
get() = viewport.centerY
|
||||
val direction: Float
|
||||
get() = viewport.direction
|
||||
|
||||
var bucketPoint: ENUPoint by Delegates.observable(ENUPoint()) { _, old, new ->
|
||||
if (old != new) {
|
||||
listeners.forEach {
|
||||
it.onBucketPointChange(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun worldToScreen(point: WorldPoint): ScreenPoint {
|
||||
val (offsetX, offsetY) = mapSize / 2
|
||||
val (scaleX, scaleY) = globalScale
|
||||
|
||||
// 计算屏幕坐标
|
||||
val screenX = (point.x * scaleX + offsetX)
|
||||
val screenY = (-point.y * scaleY + offsetY) // 注意 Y轴取反
|
||||
|
||||
return getScreenPointFromDataPoint(ScreenPoint(screenX, screenY))
|
||||
}
|
||||
|
||||
override fun screenToWorld(point: ScreenPoint): WorldPoint {
|
||||
TODO()
|
||||
}
|
||||
|
||||
fun rotateRadians(angleRadians: Float, centerX: Float, centerY: Float) {
|
||||
update {
|
||||
it.copy(
|
||||
angle = angleRadians,
|
||||
degrees = Math.toDegrees(angleRadians.toDouble()).toFloat(),
|
||||
centerX = centerX,
|
||||
centerY = centerY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun scale(scale: Float, centerX: Float, centerY: Float) {
|
||||
val dataCenter = getDataPointFromScreenPoint(centerX, centerY)
|
||||
|
||||
update { it.copy(scale = Scale(scaleX = scale, scaleY = scale)) }
|
||||
|
||||
val newScreenPoint = getScreenPointFromDataPoint(dataCenter)
|
||||
|
||||
val dx = centerX - newScreenPoint.x
|
||||
val dy = centerY - newScreenPoint.y
|
||||
|
||||
translate(dx, dy)
|
||||
}
|
||||
|
||||
private fun translate(dx: Float, dy: Float) {
|
||||
update {
|
||||
it.copy(
|
||||
nowPoint = ScreenPoint(
|
||||
x = it.nowPoint.x + dx,
|
||||
y = it.nowPoint.y + dy
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDataPointFromScreenPoint(screenX: Float, screenY: Float): ScreenPoint {
|
||||
val angle = angle
|
||||
// 反向平移
|
||||
val translatedX = screenX - nowPoint.x
|
||||
val translatedY = screenY - nowPoint.y
|
||||
|
||||
// 反向旋转
|
||||
val rotatedX = translatedX * cos(-angle) - translatedY * sin(-angle)
|
||||
val rotatedY = translatedX * sin(-angle) + translatedY * cos(-angle)
|
||||
|
||||
// 反向缩放
|
||||
val point = ScreenPoint(x = rotatedX / scale.scaleX, rotatedY / scale.scaleY)
|
||||
|
||||
return point
|
||||
}
|
||||
|
||||
private fun getScreenPointFromDataPoint(dataPoint: ScreenPoint): ScreenPoint {
|
||||
val scale = scale
|
||||
val angle = angle
|
||||
val nowPoint = nowPoint
|
||||
|
||||
// 应用缩放
|
||||
val scaledX = dataPoint.x * scale.scaleX
|
||||
val scaledY = dataPoint.y * scale.scaleY
|
||||
|
||||
// 应用旋转
|
||||
val rotatedX = scaledX * cos(angle) - scaledY * sin(angle)
|
||||
val rotatedY = scaledX * sin(angle) + scaledY * cos(angle)
|
||||
|
||||
// 应用平移
|
||||
val point = ScreenPoint(x = rotatedX + nowPoint.x, y = rotatedY + nowPoint.y)
|
||||
|
||||
return point
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.icegps.maps
|
||||
|
||||
import com.icegps.maps.model.ScreenPoint
|
||||
import com.icegps.maps.model.WorldPoint
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
interface CoordinateTransform {
|
||||
fun worldToScreen(point: WorldPoint): ScreenPoint
|
||||
fun screenToWorld(point: ScreenPoint): WorldPoint
|
||||
}
|
||||
328
maps-view/src/main/java/com/icegps/maps/MapView.kt
Normal file
328
maps-view/src/main/java/com/icegps/maps/MapView.kt
Normal file
@@ -0,0 +1,328 @@
|
||||
package com.icegps.maps
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import android.widget.Scroller
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import com.icegps.maps.layer.LayeredView
|
||||
import com.icegps.maps.model.ENUPoint
|
||||
import com.icegps.maps.model.MapMode
|
||||
import com.icegps.maps.model.ScreenPoint
|
||||
import com.icegps.maps.utils.CoordinateAndTouchUtils
|
||||
import com.icegps.math.geometry.Scale
|
||||
import com.icegps.math.geometry.toVector2D
|
||||
import com.icegps.math.geometry.toVector2F
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
class MapView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LayeredView(context, attrs, defStyleAttr), CoordinateManager.Listener {
|
||||
private val mapGestureDetector = MapGestureDetector(
|
||||
context = context,
|
||||
mapView = this,
|
||||
coordinateManager = coordinateManager,
|
||||
requestInvalidate = { invalidate() }
|
||||
)
|
||||
var mapMode: MapMode
|
||||
get() = coordinateManager.mapMode
|
||||
set(value) {
|
||||
coordinateManager.update { it.copy(mapMode = value) }
|
||||
switchMapMode(value)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
coordinateManager.addListener(this)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return mapGestureDetector.onTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun switchMapMode(mapMode: MapMode) {
|
||||
// 获取当前铲斗方向(弧度)
|
||||
val directionRadians = coordinateManager.direction
|
||||
when (mapMode) {
|
||||
MapMode.HeadingForward -> updateHeadingRadians(directionRadians)
|
||||
|
||||
MapMode.NorthUp -> updateHeadingRadians(0f)
|
||||
}
|
||||
|
||||
val bucketPoint = coordinateManager.bucketPoint
|
||||
|
||||
mapGestureDetector.centerTo(bucketPoint)
|
||||
|
||||
// 更新图层
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun updateHeadingRadians(angleRadians: Float) {
|
||||
val centerX = width / 2f
|
||||
val centerY = height / 2f
|
||||
|
||||
coordinateManager.rotateRadians(angleRadians, centerX, centerY)
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onBucketPointChange(point: ENUPoint) {
|
||||
mapGestureDetector.centerTo(point)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
coordinateManager.removeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
class MapGestureDetector(
|
||||
context: Context,
|
||||
private val mapView: MapView,
|
||||
private val coordinateManager: CoordinateManager,
|
||||
private val requestInvalidate: () -> Unit,
|
||||
) {
|
||||
// 是否禁用自动居中
|
||||
private var disableAutoCenter = false
|
||||
|
||||
// 延迟任务处理器
|
||||
private val autoCenterHandler = Handler(Looper.getMainLooper())
|
||||
private val enableAutoCenterRunnable = Runnable {
|
||||
disableAutoCenter = false
|
||||
Log.d(TAG, "自动居中已恢复")
|
||||
}
|
||||
|
||||
// 惯性滚动
|
||||
private val scroller: Scroller = Scroller(context)
|
||||
private var vTracker: VelocityTracker? = null
|
||||
private var xVelocity = 0f
|
||||
private var yVelocity = 0f
|
||||
|
||||
// 触摸相关变量
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private var lastX = 0f
|
||||
private var lastY = 0f
|
||||
private var touchMode = TOUCH_MODE_SINGLE
|
||||
private var scaleTouchBaseLineLen = 0f
|
||||
private var rotateTouchBaseAngle = 0f
|
||||
private val touchCenterPoint = PointF()
|
||||
private var canMove = false
|
||||
private val touchSlop = 8f
|
||||
private var configLastX = 0f
|
||||
private var configLastY = 0f
|
||||
private var configScale = 1f
|
||||
private var distanceX = 0f
|
||||
private var distanceY = 0f
|
||||
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// 触摸开始,禁用自动居中
|
||||
autoCenterHandler.removeCallbacks(enableAutoCenterRunnable)
|
||||
disableAutoCenter = true
|
||||
|
||||
downX = event.x
|
||||
downY = event.y
|
||||
lastX = downX
|
||||
lastY = downY
|
||||
canMove = false
|
||||
|
||||
scroller.forceFinished(true)
|
||||
scroller.abortAnimation()
|
||||
|
||||
touchMode = TOUCH_MODE_SINGLE
|
||||
|
||||
val nowPoint = coordinateManager.nowPoint
|
||||
configLastX = nowPoint.x
|
||||
configLastY = nowPoint.y
|
||||
configScale = coordinateManager.scale.scaleX
|
||||
|
||||
if (vTracker == null) {
|
||||
vTracker = VelocityTracker.obtain()
|
||||
} else {
|
||||
vTracker?.clear()
|
||||
}
|
||||
xVelocity = 0f
|
||||
yVelocity = 0f
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (event.pointerCount == 2) {
|
||||
rotateTouchBaseAngle = CoordinateAndTouchUtils.rotation(event)
|
||||
scaleTouchBaseLineLen = CoordinateAndTouchUtils.lineLen(event)
|
||||
CoordinateAndTouchUtils.centerPoint(touchCenterPoint, event)
|
||||
touchMode = TOUCH_MODE_MORE
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (touchMode == TOUCH_MODE_MORE) {
|
||||
// 多点触控 - 处理缩放和旋转
|
||||
if (event.pointerCount > 1) {
|
||||
// 处理缩放
|
||||
val scale = (CoordinateAndTouchUtils.lineLen(event) - scaleTouchBaseLineLen) / scaleTouchBaseLineLen
|
||||
val newScale = configScale * (1 + scale)
|
||||
setScale(newScale, touchCenterPoint.x, touchCenterPoint.y)
|
||||
|
||||
// 处理手势旋转 (如果不是车头朝前模式,去掉注释就能处理双指旋转了)
|
||||
if (coordinateManager.mapMode != MapMode.HeadingForward) {
|
||||
val rotate = CoordinateAndTouchUtils.rotation(event) - rotateTouchBaseAngle
|
||||
val rotateRad = Math.toRadians(rotate.toDouble()).toFloat()
|
||||
val nowPoint = coordinateManager.nowPoint
|
||||
rotateData(
|
||||
rotate,
|
||||
nowPoint.x - touchCenterPoint.x,
|
||||
nowPoint.y - touchCenterPoint.y,
|
||||
touchCenterPoint.x,
|
||||
touchCenterPoint.y
|
||||
)
|
||||
}
|
||||
|
||||
requestInvalidate()
|
||||
}
|
||||
} else {
|
||||
// 单点触控 - 处理平移
|
||||
distanceX = event.x - downX
|
||||
distanceY = event.y - downY
|
||||
|
||||
if (!canMove && CoordinateAndTouchUtils.lineLen(0f, 0f, distanceX, distanceY) > touchSlop) {
|
||||
canMove = true
|
||||
}
|
||||
|
||||
if (canMove) {
|
||||
vTracker?.addMovement(event)
|
||||
vTracker?.computeCurrentVelocity(1000)
|
||||
updateCurrentPointByDistance(distanceX, distanceY)
|
||||
xVelocity = vTracker?.xVelocity ?: 0f
|
||||
yVelocity = vTracker?.yVelocity ?: 0f
|
||||
|
||||
requestInvalidate()
|
||||
}
|
||||
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
// 触摸结束,延迟2秒后恢复自动居中
|
||||
autoCenterHandler.removeCallbacks(enableAutoCenterRunnable)
|
||||
autoCenterHandler.postDelayed(enableAutoCenterRunnable, 2000)
|
||||
|
||||
// 处理惯性滚动
|
||||
inertiaScroll()
|
||||
|
||||
// 重置状态
|
||||
canMove = false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun centerTo(point: ENUPoint, animate: Boolean = false, resetTransforms: Boolean = false) {
|
||||
// 如果禁用了自动居中,则不执行居中操作
|
||||
if (disableAutoCenter) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算屏幕中心点
|
||||
val screenCenterX = mapView.width / 2f
|
||||
val screenCenterY = mapView.height / 2f
|
||||
|
||||
// 重置缩放
|
||||
if (resetTransforms) {
|
||||
coordinateManager.update { it.copy(scale = Scale(1f, 1f)) }
|
||||
}
|
||||
|
||||
// 获取点的当前屏幕坐标(考虑当前的缩放和旋转)
|
||||
val pointScreenPoint = coordinateManager.worldToScreen(point.toVector2D().toVector2F())
|
||||
|
||||
// 计算需要平移的距离,使点位于屏幕中心
|
||||
val translateX = screenCenterX - pointScreenPoint.x
|
||||
val translateY = screenCenterY - pointScreenPoint.y
|
||||
|
||||
// 更新 nowPoint,平移视图使点居中
|
||||
val nowPoint = coordinateManager.nowPoint
|
||||
|
||||
if (animate) {
|
||||
// 如果需要动画,使用 scroller 进行平滑滚动
|
||||
scroller.forceFinished(true)
|
||||
scroller.startScroll(
|
||||
nowPoint.x.toInt(), nowPoint.y.toInt(),
|
||||
translateX.toInt(), translateY.toInt(),
|
||||
300 // 动画持续时间(毫秒)
|
||||
)
|
||||
} else {
|
||||
// 直接更新位置
|
||||
coordinateManager.update {
|
||||
it.copy(nowPoint = ScreenPoint(nowPoint.x + translateX, nowPoint.y + translateY))
|
||||
}
|
||||
}
|
||||
requestInvalidate()
|
||||
Log.d(TAG, "将点 (${point.x}, ${point.y}) 居中,平移: (${translateX}, ${translateY}), 缩放: ${coordinateManager.scale}")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据距离更新当前点
|
||||
*/
|
||||
private fun updateCurrentPointByDistance(distanceX: Float, distanceY: Float) {
|
||||
val nowPoint = ScreenPoint(configLastX + distanceX, configLastY + distanceY)
|
||||
coordinateManager.update {
|
||||
it.copy(nowPoint = nowPoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放
|
||||
*/
|
||||
private fun setScale(scale: Float, centerX: Float, centerY: Float): Boolean {
|
||||
coordinateManager.scale(scale, centerX, centerY)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转数据
|
||||
*/
|
||||
private fun rotateData(degrees: Float, dX: Float, dY: Float, centerX: Float, centerY: Float) {
|
||||
val degreesRad = Math.toRadians(degrees.toDouble()).toFloat()
|
||||
coordinateManager.rotateRadians(degreesRad, centerX, centerY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 惯性滚动
|
||||
*/
|
||||
private fun inertiaScroll(): Boolean {
|
||||
if (touchMode == TOUCH_MODE_MORE) return false
|
||||
if ((distanceX != 0f && xVelocity != 0f) || (distanceY != 0f && yVelocity != 0f)) {
|
||||
scroller.fling(
|
||||
distanceX.toInt(), distanceY.toInt(),
|
||||
xVelocity.toInt(), yVelocity.toInt(),
|
||||
if (distanceX >= 0) 0 else (distanceX - 5000).toInt(),
|
||||
if (distanceX >= 0) 5000 else distanceX.toInt(),
|
||||
if (distanceY >= 0) 0 else (distanceY - 5000).toInt(),
|
||||
if (distanceY >= 0) 5000 else distanceY.toInt()
|
||||
)
|
||||
requestInvalidate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TOUCH_MODE_SINGLE = 1
|
||||
private const val TOUCH_MODE_MORE = 2
|
||||
}
|
||||
}
|
||||
7
maps-view/src/main/java/com/icegps/maps/ktx/Any.kt
Normal file
7
maps-view/src/main/java/com/icegps/maps/ktx/Any.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.icegps.maps.ktx
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
val Any.TAG: String get() = this::class.java.simpleName
|
||||
22
maps-view/src/main/java/com/icegps/maps/layer/BaseLayer.kt
Normal file
22
maps-view/src/main/java/com/icegps/maps/layer/BaseLayer.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
abstract class BaseLayer(
|
||||
override val zIndex: Int
|
||||
) : Layer {
|
||||
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
override val visible: Boolean = true
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (visible) onDraw(canvas)
|
||||
}
|
||||
|
||||
protected abstract fun onDraw(canvas: Canvas)
|
||||
}
|
||||
157
maps-view/src/main/java/com/icegps/maps/layer/BoundaryLayer.kt
Normal file
157
maps-view/src/main/java/com/icegps/maps/layer/BoundaryLayer.kt
Normal file
@@ -0,0 +1,157 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import com.icegps.maps.CoordinateManager
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import com.icegps.maps.model.ScreenPoint
|
||||
import com.icegps.math.geometry.Line
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
class BoundaryLayer(
|
||||
private val coordinateManager: CoordinateManager,
|
||||
paintBuilder: (Paint) -> Unit = {
|
||||
it.color = Color.BLACK
|
||||
it.strokeWidth = 2f
|
||||
}
|
||||
) : BaseLayer(zIndex = 1) {
|
||||
private val lines = mutableListOf<Line>()
|
||||
var extendLines = true // 是否延长边界线
|
||||
|
||||
var lineStyle: LineStyle = LineStyle.SOLID
|
||||
set(value) {
|
||||
field = value
|
||||
when (value) {
|
||||
LineStyle.SOLID -> {
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.pathEffect = null
|
||||
}
|
||||
|
||||
LineStyle.DASHED -> {
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.pathEffect = DashPathEffect(floatArrayOf(20f, 10f), 0f)
|
||||
}
|
||||
|
||||
LineStyle.DOTTED -> {
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.pathEffect = DashPathEffect(floatArrayOf(5f, 10f), 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
paintBuilder(paint)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val (width, height) = coordinateManager.mapSize
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
lines.forEachIndexed { index, it ->
|
||||
val start = coordinateManager.worldToScreen(it.a)
|
||||
val end = coordinateManager.worldToScreen(it.b)
|
||||
val line = if (extendLines) extendLine(start, end) else it
|
||||
canvas.drawLine(
|
||||
line.a.x,
|
||||
line.a.y,
|
||||
line.b.x,
|
||||
line.b.y,
|
||||
paint
|
||||
)
|
||||
Log.d(TAG, "绘制边界线: 线$index(${line.a.x}, ${line.a.y}) - (${line.b.x}, ${line.b.y})")
|
||||
}
|
||||
}
|
||||
|
||||
fun setBoundaryLines(lines: List<Line>) {
|
||||
this.lines.clear()
|
||||
this.lines.addAll(lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* 延长线段至屏幕边缘
|
||||
*/
|
||||
private fun extendLine(start: ScreenPoint, end: ScreenPoint): Line {
|
||||
val mapSize = coordinateManager.mapSize
|
||||
val width = mapSize.width.toFloat()
|
||||
val height = mapSize.height.toFloat()
|
||||
// 计算线段方向向量
|
||||
val dx = end.x - start.x
|
||||
val dy = end.y - start.y
|
||||
|
||||
// 如果线段是垂直的
|
||||
if (dx == 0f) {
|
||||
return Line(
|
||||
ScreenPoint(start.x, 0f),
|
||||
ScreenPoint(start.x, height)
|
||||
)
|
||||
}
|
||||
|
||||
// 如果线段是水平的
|
||||
if (dy == 0f) {
|
||||
return Line(
|
||||
ScreenPoint(0f, start.y),
|
||||
ScreenPoint(width, start.y)
|
||||
)
|
||||
}
|
||||
|
||||
// 计算线段与屏幕边界的交点
|
||||
val slope = dy / dx
|
||||
val yIntercept = start.y - slope * start.x
|
||||
|
||||
// 计算与左边界的交点
|
||||
val leftX = 0f
|
||||
val leftY = yIntercept
|
||||
|
||||
// 计算与右边界的交点
|
||||
val rightX = width
|
||||
val rightY = slope * width + yIntercept
|
||||
|
||||
// 计算与上边界的交点
|
||||
val topY = 0f
|
||||
val topX = (topY - yIntercept) / slope
|
||||
|
||||
// 计算与下边界的交点
|
||||
val bottomY = height
|
||||
val bottomX = (bottomY - yIntercept) / slope
|
||||
|
||||
// 收集所有在屏幕内的交点
|
||||
val intersections = mutableListOf<ScreenPoint>()
|
||||
|
||||
if (leftY in 0f..height) {
|
||||
intersections.add(ScreenPoint(leftX, leftY))
|
||||
}
|
||||
|
||||
if (rightY in 0f..height) {
|
||||
intersections.add(ScreenPoint(rightX, rightY))
|
||||
}
|
||||
|
||||
if (topX in 0f..width) {
|
||||
intersections.add(ScreenPoint(topX, topY))
|
||||
}
|
||||
|
||||
if (bottomX in 0f..width) {
|
||||
intersections.add(ScreenPoint(bottomX, bottomY))
|
||||
}
|
||||
|
||||
// 如果找到了两个交点,返回这两个点
|
||||
if (intersections.size >= 2) {
|
||||
return Line(intersections[0], intersections[1])
|
||||
}
|
||||
|
||||
// 如果没有找到足够的交点,返回原始线段
|
||||
return Line(start, end)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class LineStyle {
|
||||
SOLID,
|
||||
DASHED,
|
||||
DOTTED,
|
||||
}
|
||||
405
maps-view/src/main/java/com/icegps/maps/layer/BucketLayer.kt
Normal file
405
maps-view/src/main/java/com/icegps/maps/layer/BucketLayer.kt
Normal file
@@ -0,0 +1,405 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.icegps.maps.CoordinateManager
|
||||
import com.icegps.maps.R
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import com.icegps.maps.model.ENUPoint
|
||||
import com.icegps.maps.model.ScreenPoint
|
||||
import com.icegps.math.geometry.Vector2F
|
||||
import com.icegps.math.geometry.toVector2D
|
||||
import com.icegps.math.geometry.toVector2F
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* 铲斗图标数据
|
||||
*
|
||||
* @property drawable 铲斗图标
|
||||
* @property width 铲斗图标的宽
|
||||
* @property height 铲斗图标的高
|
||||
* @property leftTip 左边斗齿
|
||||
* @property centerTip 中间斗齿
|
||||
* @property rightTip 右边斗齿
|
||||
*/
|
||||
data class BucketDrawable(
|
||||
val drawable: Drawable,
|
||||
val width: Float,
|
||||
val height: Float,
|
||||
val leftTip: Vector2F,
|
||||
val centerTip: Vector2F,
|
||||
val rightTip: Vector2F
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取默认的铲斗图标
|
||||
*/
|
||||
fun defaultBucketDrawable(context: Context): BucketDrawable {
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.bucket_top)
|
||||
return BucketDrawable(
|
||||
drawable = drawable!!,
|
||||
width = 722f,
|
||||
height = 722f,
|
||||
leftTip = Vector2F(208.72f, 70.92f),
|
||||
centerTip = Vector2F(362.02f, 70.92f),
|
||||
rightTip = Vector2F(514.32f, 70.92f),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
class BucketLayer(
|
||||
private val coordinateManager: CoordinateManager,
|
||||
private val bucketDrawable: BucketDrawable,
|
||||
private val tipRadius: Float = 5f,
|
||||
private val selectedTipRadius: Float = 8f,
|
||||
private val crossLineWidth: Float = 3f,
|
||||
private val crossLineColor: Int = Color.GREEN,
|
||||
private val antiAlias: Boolean = true
|
||||
) : BaseLayer(zIndex = 3) {
|
||||
private var bucketTips: BucketTips = BucketTips()
|
||||
private var tipScreenPoints: TipScreenPoints = TipScreenPoints()
|
||||
|
||||
var selectedBucketTip: BucketTip = BucketTip.LEFT
|
||||
|
||||
init {
|
||||
updateTipPositions(bucketTips)
|
||||
}
|
||||
|
||||
fun updateTipPositions(bucketTips: BucketTips) {
|
||||
this.bucketTips = bucketTips
|
||||
|
||||
val directionRadians = calculateDirectionFromTips(bucketTips.left, bucketTips.right)
|
||||
|
||||
coordinateManager.update {
|
||||
it.copy(direction = directionRadians)
|
||||
}
|
||||
|
||||
coordinateManager.bucketPoint = bucketTips.center
|
||||
}
|
||||
|
||||
/**
|
||||
* 从左右铲尖计算方向角度(弧度)
|
||||
* @return 方向角度(弧度)
|
||||
*/
|
||||
private fun calculateDirectionFromTips(leftTip: ENUPoint, rightTip: ENUPoint): Float {
|
||||
val dx = rightTip.x - leftTip.x
|
||||
val dy = rightTip.y - leftTip.y
|
||||
|
||||
// 直接计算弧度角
|
||||
val angleRad = atan2(dy, dx)
|
||||
|
||||
// 方向现在直接基于铲尖之间的角度(弧度)
|
||||
var directionRad = angleRad.toFloat()
|
||||
|
||||
// 标准化到[-π, π]范围
|
||||
while (directionRad > Math.PI) directionRad -= (2 * Math.PI).toFloat()
|
||||
while (directionRad <= -Math.PI) directionRad += (2 * Math.PI).toFloat()
|
||||
|
||||
// 记录日志(同时显示弧度和角度,便于调试)
|
||||
val directionDeg = Math.toDegrees(directionRad.toDouble())
|
||||
Log.d(TAG, "铲尖方向计算: dx=$dx, dy=$dy, 弧度=${directionRad}, 角度=${directionDeg}°")
|
||||
|
||||
return directionRad
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val (width, height) = coordinateManager.mapSize
|
||||
if (width <= 0 || height <= 0) return
|
||||
with(coordinateManager) {
|
||||
tipScreenPoints = TipScreenPoints(
|
||||
left = worldToScreen(bucketTips.left.toVector2D().toVector2F()),
|
||||
center = worldToScreen(bucketTips.center.toVector2D().toVector2F()),
|
||||
right = worldToScreen(bucketTips.right.toVector2D().toVector2F()),
|
||||
)
|
||||
}
|
||||
drawCrossLineAtSelectedTip(canvas)
|
||||
|
||||
drawBucket(canvas, tipScreenPoints)
|
||||
|
||||
drawTipMarkers(canvas, tipScreenPoints)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在选中的铲尖位置绘制十字线
|
||||
*/
|
||||
private fun drawCrossLineAtSelectedTip(canvas: Canvas) {
|
||||
val tipPoint = when (selectedBucketTip) {
|
||||
BucketTip.LEFT -> tipScreenPoints.left
|
||||
BucketTip.CENTER -> tipScreenPoints.center
|
||||
BucketTip.RIGHT -> tipScreenPoints.right
|
||||
}
|
||||
paint.apply {
|
||||
color = crossLineColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = crossLineWidth
|
||||
isAntiAlias = true
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
// 计算铲斗方向向量(从左铲尖到右铲尖)
|
||||
|
||||
val directionX = bucketTips.left.x - bucketTips.right.x
|
||||
val directionY = bucketTips.left.y - bucketTips.right.y
|
||||
val length = sqrt(directionX * directionX + directionY * directionY)
|
||||
|
||||
if (length > 0) {
|
||||
// 获取屏幕上的方向向量
|
||||
val rightTip = tipScreenPoints.right
|
||||
val leftTip = tipScreenPoints.left
|
||||
val screenDirectionX = rightTip.x - leftTip.x
|
||||
val screenDirectionY = rightTip.y - leftTip.y
|
||||
val screenLength = sqrt(
|
||||
(screenDirectionX * screenDirectionX +
|
||||
screenDirectionY * screenDirectionY).toDouble()
|
||||
).toFloat()
|
||||
|
||||
if (screenLength > 0) {
|
||||
val screenUnitX = screenDirectionX / screenLength
|
||||
val screenUnitY = screenDirectionY / screenLength
|
||||
|
||||
// 垂直向量(顺时针旋转90度)
|
||||
val screenPerpX = -screenUnitY
|
||||
val screenPerpY = screenUnitX
|
||||
|
||||
// 计算线与视图边缘的交点
|
||||
val lineIntersections1 = calculateViewIntersections(
|
||||
tipPoint.x, tipPoint.y, screenUnitX, screenUnitY
|
||||
)
|
||||
|
||||
// 绘制与铲尖线重合的线(无限长)
|
||||
canvas.drawLine(
|
||||
lineIntersections1.first.x, lineIntersections1.first.y,
|
||||
lineIntersections1.second.x, lineIntersections1.second.y,
|
||||
paint
|
||||
)
|
||||
|
||||
// 计算垂直线与视图边缘的交点
|
||||
val lineIntersections2 = calculateViewIntersections(
|
||||
tipPoint.x, tipPoint.y, screenPerpX, screenPerpY
|
||||
)
|
||||
|
||||
// 绘制垂直于铲尖线的线(无限长)
|
||||
canvas.drawLine(
|
||||
lineIntersections2.first.x, lineIntersections2.first.y,
|
||||
lineIntersections2.second.x, lineIntersections2.second.y,
|
||||
paint
|
||||
)
|
||||
} else {
|
||||
// 如果无法计算方向,则使用默认的水平和垂直线
|
||||
drawDefaultCrossLine(canvas, tipPoint)
|
||||
}
|
||||
} else {
|
||||
// 如果无法计算方向,则使用默认的水平和垂直线
|
||||
drawDefaultCrossLine(canvas, tipPoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算从给定点沿给定方向延伸的直线与视图边缘的交点
|
||||
* @return 两个交点,分别是线的两个方向与视图边缘的交点
|
||||
*/
|
||||
private fun calculateViewIntersections(
|
||||
x: Float, y: Float, dirX: Float, dirY: Float
|
||||
): Pair<PointF, PointF> {
|
||||
val mapSize = coordinateManager.mapSize
|
||||
val width = mapSize.width.toFloat()
|
||||
val height = mapSize.height.toFloat()
|
||||
|
||||
// 计算线与四条边的交点
|
||||
val intersections = mutableListOf<PointF>()
|
||||
|
||||
// 与左边界的交点
|
||||
if (dirX != 0f) {
|
||||
val t = -x / dirX
|
||||
val intersectY = y + t * dirY
|
||||
if (intersectY in 0f..height) {
|
||||
intersections.add(PointF(0f, intersectY))
|
||||
}
|
||||
}
|
||||
|
||||
// 与右边界的交点
|
||||
if (dirX != 0f) {
|
||||
val t = (width - x) / dirX
|
||||
val intersectY = y + t * dirY
|
||||
if (intersectY in 0f..height) {
|
||||
intersections.add(PointF(width, intersectY))
|
||||
}
|
||||
}
|
||||
|
||||
// 与上边界的交点
|
||||
if (dirY != 0f) {
|
||||
val t = -y / dirY
|
||||
val intersectX = x + t * dirX
|
||||
if (intersectX in 0f..width) {
|
||||
intersections.add(PointF(intersectX, 0f))
|
||||
}
|
||||
}
|
||||
|
||||
// 与下边界的交点
|
||||
if (dirY != 0f) {
|
||||
val t = (height - y) / dirY
|
||||
val intersectX = x + t * dirX
|
||||
if (intersectX in 0f..width) {
|
||||
intersections.add(PointF(intersectX, height))
|
||||
}
|
||||
}
|
||||
|
||||
// 确保有两个交点
|
||||
if (intersections.size < 2) {
|
||||
// 如果找不到两个交点,使用默认值(这种情况不应该发生,但为了健壮性)
|
||||
return Pair(PointF(0f, y), PointF(width, y))
|
||||
}
|
||||
|
||||
// 返回两个交点
|
||||
return Pair(intersections[0], intersections[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制默认的水平和垂直十字线
|
||||
*/
|
||||
private fun drawDefaultCrossLine(canvas: Canvas, tipPoint: ScreenPoint) {
|
||||
val mapSize = coordinateManager.mapSize
|
||||
val width = mapSize.width.toFloat()
|
||||
val height = mapSize.height.toFloat()
|
||||
|
||||
// 绘制水平线(从左边缘到右边缘)
|
||||
canvas.drawLine(
|
||||
0f, tipPoint.y,
|
||||
width, tipPoint.y,
|
||||
paint
|
||||
)
|
||||
|
||||
// 绘制垂直线(从上边缘到下边缘)
|
||||
canvas.drawLine(
|
||||
tipPoint.x, 0f,
|
||||
tipPoint.x, height,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawBucket(canvas: Canvas, tipScreenPoints: TipScreenPoints) {
|
||||
// 计算SVG中左右铲尖之间的距离
|
||||
val svgTipDistance = bucketDrawable.rightTip.x - bucketDrawable.leftTip.x
|
||||
|
||||
// 计算实际屏幕上左右铲尖之间的距离
|
||||
val rightTip = tipScreenPoints.right
|
||||
val leftTip = tipScreenPoints.left
|
||||
val screenTipDistanceX = rightTip.x - leftTip.x
|
||||
val screenTipDistanceY = rightTip.y - leftTip.y
|
||||
val screenTipDistance = sqrt(
|
||||
(screenTipDistanceX * screenTipDistanceX +
|
||||
screenTipDistanceY * screenTipDistanceY).toDouble()
|
||||
).toFloat()
|
||||
|
||||
val scaleFactor = 1.0f // 增加这个值可以放大图标,减小则缩小图标
|
||||
val scale = (screenTipDistance / svgTipDistance) * scaleFactor
|
||||
|
||||
// 计算旋转角度(从左铲尖到右铲尖的方向)
|
||||
val angle = Math.toDegrees(
|
||||
atan2(
|
||||
screenTipDistanceY.toDouble(),
|
||||
screenTipDistanceX.toDouble()
|
||||
)
|
||||
).toFloat()
|
||||
|
||||
// 计算SVG中铲尖的中心点
|
||||
val svgCenterX = (bucketDrawable.rightTip.x + bucketDrawable.leftTip.x) / 2
|
||||
val svgCenterY = bucketDrawable.leftTip.y
|
||||
|
||||
// 计算屏幕上铲尖的中心点
|
||||
val screenCenterX = (leftTip.x + rightTip.x) / 2
|
||||
val screenCenterY = (leftTip.y + rightTip.y) / 2
|
||||
|
||||
// 创建变换矩阵
|
||||
val matrix = Matrix()
|
||||
matrix.postTranslate(-svgCenterX, -svgCenterY)
|
||||
matrix.postScale(scale, scale)
|
||||
matrix.postRotate(angle)
|
||||
matrix.postTranslate(screenCenterX, screenCenterY)
|
||||
|
||||
// 应用变换并绘制
|
||||
canvas.save()
|
||||
try {
|
||||
// 设置抗锯齿
|
||||
val oldPaint = paint.isAntiAlias
|
||||
paint.isAntiAlias = antiAlias
|
||||
|
||||
// 应用矩阵变换
|
||||
canvas.concat(matrix)
|
||||
|
||||
// 设置drawable边界并绘制
|
||||
with(bucketDrawable) {
|
||||
drawable.setBounds(
|
||||
0,
|
||||
0,
|
||||
width.toInt(),
|
||||
height.toInt()
|
||||
)
|
||||
drawable.draw(canvas)
|
||||
}
|
||||
|
||||
// 恢复原来的抗锯齿设置
|
||||
paint.isAntiAlias = oldPaint
|
||||
} finally {
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制铲尖标记
|
||||
*/
|
||||
private fun drawTipMarkers(canvas: Canvas, tipScreenPoints: TipScreenPoints) {
|
||||
BucketTip.entries.forEach { tip ->
|
||||
paint.apply {
|
||||
color = tip.color
|
||||
style = if (tip == selectedBucketTip) Paint.Style.FILL else Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
// 使用实际铲尖的屏幕坐标绘制标记
|
||||
when (tip) {
|
||||
BucketTip.LEFT -> tipScreenPoints.left
|
||||
BucketTip.CENTER -> tipScreenPoints.center
|
||||
BucketTip.RIGHT -> tipScreenPoints.right
|
||||
}.let { tipScreen ->
|
||||
canvas.drawCircle(
|
||||
tipScreen.x,
|
||||
tipScreen.y,
|
||||
if (tip == selectedBucketTip) selectedTipRadius else tipRadius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BucketTip(val color: Int) {
|
||||
LEFT(Color.RED),
|
||||
CENTER(Color.GREEN),
|
||||
RIGHT(Color.BLUE),
|
||||
}
|
||||
|
||||
data class BucketTips(
|
||||
val left: ENUPoint = ENUPoint(-1.0, -1.0, 0.0),
|
||||
val center: ENUPoint = ENUPoint(0.0, 0.0, 0.0),
|
||||
val right: ENUPoint = ENUPoint(1.0, 1.0, 0.0),
|
||||
)
|
||||
|
||||
data class TipScreenPoints(
|
||||
val left: ScreenPoint = ScreenPoint(),
|
||||
val center: ScreenPoint = ScreenPoint(),
|
||||
val right: ScreenPoint = ScreenPoint(),
|
||||
)
|
||||
122
maps-view/src/main/java/com/icegps/maps/layer/GridLayer.kt
Normal file
122
maps-view/src/main/java/com/icegps/maps/layer/GridLayer.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import com.icegps.maps.CoordinateManager
|
||||
import com.icegps.maps.ktx.TAG
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
class GridLayer(
|
||||
private val coordinateManager: CoordinateManager,
|
||||
paintBuilder: (Paint) -> Unit = {
|
||||
it.color = Color.LTGRAY
|
||||
it.strokeWidth = 1f
|
||||
it.style = Paint.Style.STROKE
|
||||
}
|
||||
) : BaseLayer(zIndex = 0) {
|
||||
init {
|
||||
paintBuilder(paint)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
// 获取地图尺寸
|
||||
val (mapWidth, mapHeight) = coordinateManager.mapSize
|
||||
|
||||
// 获取当前缩放比例
|
||||
val (scale) = coordinateManager.scale
|
||||
|
||||
// 获取当前偏移量
|
||||
val nowPoint = coordinateManager.nowPoint
|
||||
|
||||
// 获取旋转角度(弧度)和旋转中心
|
||||
val angle = coordinateManager.angle
|
||||
val centerX = coordinateManager.centerX
|
||||
val centerY = coordinateManager.centerY
|
||||
|
||||
// 计算网格线缩放单位
|
||||
var bgLineScaleUnit = scale
|
||||
while (bgLineScaleUnit > 2) {
|
||||
bgLineScaleUnit /= 2
|
||||
}
|
||||
|
||||
// 应用网格线缩放单位
|
||||
val scaledGridSize = coordinateManager.gridSize * bgLineScaleUnit
|
||||
|
||||
// 计算旋转变换的正弦和余弦值
|
||||
val cosAngle = cos(angle)
|
||||
val sinAngle = sin(angle)
|
||||
|
||||
// 计算网格原点(考虑平移)
|
||||
val gridOriginX = nowPoint.x
|
||||
val gridOriginY = nowPoint.y
|
||||
|
||||
// 计算网格线起始位置
|
||||
var lineStartX = gridOriginX % scaledGridSize
|
||||
var lineStartY = gridOriginY % scaledGridSize
|
||||
|
||||
// 确保起始位置为正值
|
||||
lineStartX = if (lineStartX < 0) lineStartX + scaledGridSize else lineStartX
|
||||
lineStartY = if (lineStartY < 0) lineStartY + scaledGridSize else lineStartY
|
||||
|
||||
// 计算需要绘制的额外线条数量,确保旋转后仍能覆盖整个视图
|
||||
val diagonalLength = Math.sqrt((mapWidth * mapWidth + mapHeight * mapHeight).toDouble()).toFloat()
|
||||
val extraLines = (diagonalLength / scaledGridSize).toInt() + 2
|
||||
|
||||
// 计算网格线的起始和结束点
|
||||
val startX = -extraLines * scaledGridSize + lineStartX
|
||||
val endX = mapWidth + extraLines * scaledGridSize
|
||||
val startY = -extraLines * scaledGridSize + lineStartY
|
||||
val endY = mapHeight + extraLines * scaledGridSize
|
||||
|
||||
// 绘制水平线
|
||||
var y = startY
|
||||
while (y <= endY) {
|
||||
// 计算旋转后的线条起点和终点
|
||||
val x1 = startX
|
||||
val y1 = y
|
||||
val x2 = endX
|
||||
val y2 = y
|
||||
|
||||
// 计算旋转后的坐标
|
||||
val rotatedX1 = centerX + (x1 - centerX) * cosAngle - (y1 - centerY) * sinAngle
|
||||
val rotatedY1 = centerY + (x1 - centerX) * sinAngle + (y1 - centerY) * cosAngle
|
||||
val rotatedX2 = centerX + (x2 - centerX) * cosAngle - (y2 - centerY) * sinAngle
|
||||
val rotatedY2 = centerY + (x2 - centerX) * sinAngle + (y2 - centerY) * cosAngle
|
||||
|
||||
// 绘制旋转后的线条
|
||||
canvas.drawLine(rotatedX1, rotatedY1, rotatedX2, rotatedY2, paint)
|
||||
|
||||
y += scaledGridSize
|
||||
}
|
||||
|
||||
// 绘制垂直线
|
||||
var x = startX
|
||||
while (x <= endX) {
|
||||
// 计算旋转后的线条起点和终点
|
||||
val x1 = x
|
||||
val y1 = startY
|
||||
val x2 = x
|
||||
val y2 = endY
|
||||
|
||||
// 计算旋转后的坐标
|
||||
val rotatedX1 = centerX + (x1 - centerX) * cosAngle - (y1 - centerY) * sinAngle
|
||||
val rotatedY1 = centerY + (x1 - centerX) * sinAngle + (y1 - centerY) * cosAngle
|
||||
val rotatedX2 = centerX + (x2 - centerX) * cosAngle - (y2 - centerY) * sinAngle
|
||||
val rotatedY2 = centerY + (x2 - centerX) * sinAngle + (y2 - centerY) * cosAngle
|
||||
|
||||
// 绘制旋转后的线条
|
||||
canvas.drawLine(rotatedX1, rotatedY1, rotatedX2, rotatedY2, paint)
|
||||
|
||||
x += scaledGridSize
|
||||
}
|
||||
|
||||
Log.d(TAG, "Grid drawn with size: $scaledGridSize, scale: $scale, rotation: ${Math.toDegrees(angle.toDouble())}°")
|
||||
}
|
||||
}
|
||||
13
maps-view/src/main/java/com/icegps/maps/layer/Layer.kt
Normal file
13
maps-view/src/main/java/com/icegps/maps/layer/Layer.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.graphics.Canvas
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
interface Layer {
|
||||
val visible: Boolean
|
||||
val zIndex: Int
|
||||
fun draw(canvas: Canvas)
|
||||
}
|
||||
63
maps-view/src/main/java/com/icegps/maps/layer/LayeredView.kt
Normal file
63
maps-view/src/main/java/com/icegps/maps/layer/LayeredView.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.icegps.maps.layer
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.icegps.maps.BaseMapView
|
||||
import com.icegps.maps.CoordinateManager
|
||||
import com.icegps.math.geometry.SizeInt
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
abstract class LayeredView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr), BaseMapView {
|
||||
final override val coordinateManager: CoordinateManager = CoordinateManager()
|
||||
private val layers = ArrayList<BaseLayer>()
|
||||
|
||||
fun getLayers(): List<BaseLayer> {
|
||||
return layers.toList()
|
||||
}
|
||||
|
||||
fun addLayer(layer: BaseLayer) {
|
||||
layers.add(layer)
|
||||
layers.sortBy { it.zIndex }
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun createGridLayer(): GridLayer {
|
||||
return GridLayer(coordinateManager = coordinateManager)
|
||||
}
|
||||
|
||||
fun createBoundaryLayer(): BoundaryLayer {
|
||||
return BoundaryLayer(coordinateManager = coordinateManager)
|
||||
}
|
||||
|
||||
fun createBucketLayer(): BucketLayer {
|
||||
return BucketLayer(
|
||||
coordinateManager = coordinateManager,
|
||||
bucketDrawable = defaultBucketDrawable(context)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
layers.forEach { layer ->
|
||||
if (layer.visible) layer.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
|
||||
coordinateManager.update {
|
||||
it.copy(mapSize = SizeInt(width = w, height = h))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.icegps.maps.model
|
||||
|
||||
import com.icegps.math.geometry.Point3
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
typealias ENUPoint = Point3
|
||||
10
maps-view/src/main/java/com/icegps/maps/model/MapMode.kt
Normal file
10
maps-view/src/main/java/com/icegps/maps/model/MapMode.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.icegps.maps.model
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
enum class MapMode {
|
||||
HeadingForward,
|
||||
NorthUp,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.icegps.maps.model
|
||||
|
||||
import com.icegps.math.geometry.Vector2F
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
typealias ScreenPoint = Vector2F
|
||||
36
maps-view/src/main/java/com/icegps/maps/model/Viewport.kt
Normal file
36
maps-view/src/main/java/com/icegps/maps/model/Viewport.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.icegps.maps.model
|
||||
|
||||
import com.icegps.math.geometry.Scale
|
||||
import com.icegps.math.geometry.SizeInt
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
data class Viewport(
|
||||
val mapMode: MapMode,
|
||||
val scale: Scale,
|
||||
val mapSize: SizeInt,
|
||||
val gridSize: Float,
|
||||
val nowPoint: ScreenPoint,
|
||||
val angle: Float,
|
||||
val degrees: Float,
|
||||
val centerX: Float,
|
||||
val centerY: Float,
|
||||
val direction: Float
|
||||
) {
|
||||
companion object {
|
||||
val Empty = Viewport(
|
||||
mapMode = MapMode.HeadingForward,
|
||||
scale = Scale(1f, 1f),
|
||||
mapSize = SizeInt(),
|
||||
gridSize = 50f,
|
||||
nowPoint = ScreenPoint(),
|
||||
angle = 0f,
|
||||
degrees = 0f,
|
||||
centerX = 0f,
|
||||
centerY = 0f,
|
||||
direction = 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.icegps.maps.model
|
||||
|
||||
import com.icegps.math.geometry.Vector2F
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/6/13
|
||||
*/
|
||||
typealias WorldPoint = Vector2F
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.icegps.maps.utils
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.view.MotionEvent
|
||||
import java.lang.Math.toDegrees
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* 工具类,用于处理触摸事件中的几何计算,包括计算距离、中点、旋转角度和夹角等。
|
||||
*
|
||||
* @author linmiao
|
||||
* @date 2025/4/14
|
||||
*/
|
||||
object CoordinateAndTouchUtils {
|
||||
|
||||
/**
|
||||
* 根据触摸事件中的两个点计算线段长度。
|
||||
*
|
||||
* @param event 触摸事件
|
||||
* @return 两点之间的线段长度
|
||||
*/
|
||||
@JvmStatic
|
||||
fun lineLen(event: MotionEvent): Float {
|
||||
return lineLen(event.getX(0), event.getY(0), event.getX(1), event.getY(1))
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据两点坐标计算线段长度。
|
||||
*
|
||||
* @param x1 第一个点的 x 坐标
|
||||
* @param y1 第一个点的 y 坐标
|
||||
* @param x2 第二个点的 x 坐标
|
||||
* @param y2 第二个点的 y 坐标
|
||||
* @return 两点之间的线段长度
|
||||
*/
|
||||
@JvmStatic
|
||||
fun lineLen(x1: Float, y1: Float, x2: Float, y2: Float): Float {
|
||||
return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2)).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据触摸事件中的两个点计算它们的中点,并将结果存储到 PointF 对象中。
|
||||
*
|
||||
* @param point 用于存储中点的 PointF 对象
|
||||
* @param event 触摸事件
|
||||
*/
|
||||
@JvmStatic
|
||||
fun centerPoint(point: PointF, event: MotionEvent) {
|
||||
val x = event.getX(0) + event.getX(1)
|
||||
val y = event.getY(0) + event.getY(1)
|
||||
point.set(x / 2, y / 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据触摸事件中的两个点计算它们之间的旋转角度。
|
||||
*
|
||||
* @param event 触摸事件
|
||||
* @return 两点之间的旋转角度(度)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun rotation(event: MotionEvent): Float {
|
||||
val deltaX = event.getX(0) - event.getX(1)
|
||||
val deltaY = event.getY(0) - event.getY(1)
|
||||
|
||||
val radians = atan2(deltaY, deltaX)
|
||||
return toDegrees(radians.toDouble()).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的夹角。
|
||||
*
|
||||
* @param x1 第一个点的 x 坐标
|
||||
* @param y1 第一个点的 y 坐标
|
||||
* @param x2 第二个点的 x 坐标
|
||||
* @param y2 第二个点的 y 坐标
|
||||
* @return 两点之间的夹角(度)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getIntersectionAngleFrom2Point(x1: Float, y1: Float, x2: Float, y2: Float): Double {
|
||||
if (x1 == x2 && y1 == y2) return 0.0
|
||||
|
||||
val dX = x2 - x1
|
||||
val dY = y2 - y1
|
||||
|
||||
if (x1 == x2) return if (dY > 0) 0.0 else 180.0
|
||||
if (y1 == y2) return if (dX > 0) 0.0 else 180.0
|
||||
|
||||
var angle = abs(atan(dX / dY) / PI * 180)
|
||||
|
||||
if (dX > 0 && dY < 0) {
|
||||
angle += 90
|
||||
} else if (dX < 0 && dY < 0) {
|
||||
angle += 180
|
||||
} else if (dX < 0 && dY > 0) {
|
||||
angle += 270
|
||||
}
|
||||
|
||||
return angle
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的欧几里得距离。
|
||||
*
|
||||
* @param x 第一个点的 x 坐标
|
||||
* @param y 第一个点的 y 坐标
|
||||
* @param x1 第二个点的 x 坐标
|
||||
* @param y1 第二个点的 y 坐标
|
||||
* @return 两点之间的距离
|
||||
*/
|
||||
@JvmStatic
|
||||
fun calculateDistance(x: Double, y: Double, x1: Double, y1: Double): Double {
|
||||
return sqrt((x - x1).pow(2) + (y - y1).pow(2))
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的方向角度。
|
||||
*
|
||||
* @param x 第一个点的 x 坐标
|
||||
* @param y 第一个点的 y 坐标
|
||||
* @param x1 第二个点的 x 坐标
|
||||
* @param y1 第二个点的 y 坐标
|
||||
* @return 从点 (x1, y1) 到点 (x, y) 的方向角度(度),范围为 [0, 360)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun calculateAngle(x: Double, y: Double, x1: Double, y1: Double): Double {
|
||||
var degrees = toDegrees(atan2(y - y1, x - x1))
|
||||
degrees = (degrees + 270) % 360
|
||||
return degrees
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user